From cf0747ba3e1f781d9f56e0f7aa8a01235025edcc Mon Sep 17 00:00:00 2001 From: krahets Date: Tue, 14 Apr 2026 18:06:19 +0800 Subject: [PATCH] deploy --- assets/javascripts/bundle.c2b142ea.min.js | 2 +- en/assets/javascripts/bundle.c2b142ea.min.js | 2 +- en/javascripts/animation_player.js | 2 +- en/javascripts/katex.js | 2 +- en/javascripts/mathjax.js | 2 +- en/javascripts/starfield.js | 2 +- en/stylesheets/animation_player.css | 2 +- en/stylesheets/extra.css | 2 +- en/stylesheets/giscus-dark.css | 2 +- en/stylesheets/giscus-light.css | 2 +- ja/assets/javascripts/bundle.c2b142ea.min.js | 2 +- ja/javascripts/animation_player.js | 2 +- ja/javascripts/katex.js | 2 +- ja/javascripts/mathjax.js | 2 +- ja/javascripts/starfield.js | 2 +- ja/stylesheets/animation_player.css | 2 +- ja/stylesheets/extra.css | 2 +- ja/stylesheets/giscus-dark.css | 2 +- ja/stylesheets/giscus-light.css | 2 +- javascripts/animation_player.js | 2 +- javascripts/katex.js | 2 +- javascripts/mathjax.js | 2 +- javascripts/starfield.js | 2 +- ru/assets/javascripts/bundle.c2b142ea.min.js | 2 +- ru/chapter_appendix/contribution/index.html | 8 +-- ru/chapter_appendix/installation/index.html | 4 +- .../array/index.html | 12 ++-- .../linked_list/index.html | 10 ++-- .../list/index.html | 6 +- .../ram_and_cache/index.html | 4 +- .../summary/index.html | 20 +++---- .../backtracking_algorithm/index.html | 36 ++++++------ .../n_queens_problem/index.html | 4 +- .../permutations_problem/index.html | 10 ++-- .../subset_sum_problem/index.html | 14 ++--- ru/chapter_backtracking/summary/index.html | 8 +-- .../iteration_and_recursion/index.html | 28 ++++----- .../performance_evaluation/index.html | 8 +-- .../space_complexity/index.html | 14 ++--- .../summary/index.html | 4 +- .../time_complexity/index.html | 40 ++++++------- .../basic_data_types/index.html | 18 +++--- .../character_encoding/index.html | 26 ++++----- .../index.html | 16 ++--- .../number_encoding/index.html | 52 ++++++++--------- ru/chapter_data_structure/summary/index.html | 28 ++++----- .../binary_search_recur/index.html | 22 +++---- .../build_binary_tree_problem/index.html | 22 +++---- .../divide_and_conquer/index.html | 58 +++++++++---------- .../hanota_problem/index.html | 6 +- ru/chapter_divide_and_conquer/index.html | 2 +- .../summary/index.html | 12 ++-- .../dp_problem_features/index.html | 14 ++--- .../dp_solution_pipeline/index.html | 20 +++---- .../edit_distance_problem/index.html | 12 ++-- .../intro_to_dynamic_programming/index.html | 20 +++---- .../knapsack_problem/index.html | 8 +-- .../summary/index.html | 12 ++-- .../unbounded_knapsack_problem/index.html | 14 ++--- ru/chapter_graph/graph/index.html | 12 ++-- ru/chapter_graph/graph_operations/index.html | 26 ++++----- ru/chapter_graph/graph_traversal/index.html | 20 +++---- ru/chapter_graph/summary/index.html | 12 ++-- .../fractional_knapsack_problem/index.html | 2 +- ru/chapter_greedy/greedy_algorithm/index.html | 2 +- .../max_capacity_problem/index.html | 2 +- ru/chapter_hashing/hash_algorithm/index.html | 14 ++--- ru/chapter_hashing/hash_collision/index.html | 14 ++--- ru/chapter_hashing/hash_map/index.html | 8 +-- ru/chapter_hashing/summary/index.html | 6 +- ru/chapter_heap/build_heap/index.html | 8 +-- ru/chapter_heap/heap/index.html | 14 ++--- ru/chapter_heap/summary/index.html | 4 +- ru/chapter_heap/top_k/index.html | 10 ++-- ru/chapter_hello_algo/index.html | 12 ++-- .../algorithms_are_everywhere/index.html | 24 ++++---- ru/chapter_introduction/summary/index.html | 8 +-- .../what_is_dsa/index.html | 2 +- ru/chapter_preface/about_the_book/index.html | 15 ++--- ru/chapter_preface/suggestions/index.html | 16 ++--- ru/chapter_reference/index.html | 2 +- ru/chapter_searching/binary_search/index.html | 6 +- .../binary_search_insertion/index.html | 4 +- .../replace_linear_by_hashing/index.html | 4 +- .../searching_algorithm_revisited/index.html | 14 ++--- ru/chapter_searching/summary/index.html | 2 +- ru/chapter_sorting/bubble_sort/index.html | 18 +++--- ru/chapter_sorting/bucket_sort/index.html | 8 +-- ru/chapter_sorting/counting_sort/index.html | 10 ++-- ru/chapter_sorting/heap_sort/index.html | 10 ++-- ru/chapter_sorting/insertion_sort/index.html | 14 ++--- ru/chapter_sorting/merge_sort/index.html | 12 ++-- ru/chapter_sorting/quick_sort/index.html | 26 ++++----- ru/chapter_sorting/radix_sort/index.html | 6 +- ru/chapter_sorting/selection_sort/index.html | 4 +- .../sorting_algorithm/index.html | 2 +- ru/chapter_sorting/summary/index.html | 18 +++--- ru/chapter_stack_and_queue/deque/index.html | 6 +- ru/chapter_stack_and_queue/index.html | 2 +- ru/chapter_stack_and_queue/queue/index.html | 10 ++-- ru/chapter_stack_and_queue/stack/index.html | 8 +-- ru/chapter_stack_and_queue/summary/index.html | 14 ++--- .../array_representation_of_tree/index.html | 2 +- ru/chapter_tree/avl_tree/index.html | 30 +++++----- ru/chapter_tree/binary_search_tree/index.html | 18 +++--- ru/chapter_tree/binary_tree/index.html | 18 +++--- .../binary_tree_traversal/index.html | 14 ++--- ru/chapter_tree/index.html | 2 +- ru/chapter_tree/summary/index.html | 14 ++--- ru/javascripts/animation_player.js | 2 +- ru/javascripts/katex.js | 2 +- ru/javascripts/mathjax.js | 2 +- ru/javascripts/starfield.js | 2 +- ru/search.json | 2 +- ru/stylesheets/animation_player.css | 2 +- ru/stylesheets/extra.css | 2 +- ru/stylesheets/giscus-dark.css | 2 +- ru/stylesheets/giscus-light.css | 2 +- stylesheets/animation_player.css | 2 +- stylesheets/extra.css | 2 +- stylesheets/giscus-dark.css | 2 +- stylesheets/giscus-light.css | 2 +- .../assets/javascripts/bundle.c2b142ea.min.js | 2 +- zh-hant/javascripts/animation_player.js | 2 +- zh-hant/javascripts/katex.js | 2 +- zh-hant/javascripts/mathjax.js | 2 +- zh-hant/javascripts/starfield.js | 2 +- zh-hant/stylesheets/animation_player.css | 2 +- zh-hant/stylesheets/extra.css | 2 +- zh-hant/stylesheets/giscus-dark.css | 2 +- zh-hant/stylesheets/giscus-light.css | 2 +- 131 files changed, 604 insertions(+), 609 deletions(-) diff --git a/assets/javascripts/bundle.c2b142ea.min.js b/assets/javascripts/bundle.c2b142ea.min.js index 44dc5af91..c46ebce33 100644 --- a/assets/javascripts/bundle.c2b142ea.min.js +++ b/assets/javascripts/bundle.c2b142ea.min.js @@ -1,4 +1,4 @@ "use strict";(()=>{var xc=Object.create;var kn=Object.defineProperty,wc=Object.defineProperties,Ec=Object.getOwnPropertyDescriptor,Tc=Object.getOwnPropertyDescriptors,Sc=Object.getOwnPropertyNames,Dr=Object.getOwnPropertySymbols,Oc=Object.getPrototypeOf,An=Object.prototype.hasOwnProperty,Fo=Object.prototype.propertyIsEnumerable;var jo=(e,t,r)=>t in e?kn(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,H=(e,t)=>{for(var r in t||(t={}))An.call(t,r)&&jo(e,r,t[r]);if(Dr)for(var r of Dr(t))Fo.call(t,r)&&jo(e,r,t[r]);return e},He=(e,t)=>wc(e,Tc(t));var gr=(e,t)=>{var r={};for(var n in e)An.call(e,n)&&t.indexOf(n)<0&&(r[n]=e[n]);if(e!=null&&Dr)for(var n of Dr(e))t.indexOf(n)<0&&Fo.call(e,n)&&(r[n]=e[n]);return r};var Cn=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var Lc=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of Sc(t))!An.call(e,o)&&o!==r&&kn(e,o,{get:()=>t[o],enumerable:!(n=Ec(t,o))||n.enumerable});return e};var _r=(e,t,r)=>(r=e!=null?xc(Oc(e)):{},Lc(t||!e||!e.__esModule?kn(r,"default",{value:e,enumerable:!0}):r,e));var Uo=(e,t,r)=>new Promise((n,o)=>{var i=c=>{try{s(r.next(c))}catch(l){o(l)}},a=c=>{try{s(r.throw(c))}catch(l){o(l)}},s=c=>c.done?n(c.value):Promise.resolve(c.value).then(i,a);s((r=r.apply(e,t)).next())});var Do=Cn((Hn,No)=>{(function(e,t){typeof Hn=="object"&&typeof No!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(Hn,(function(){"use strict";function e(r){var n=!0,o=!1,i=null,a={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function s(_){return!!(_&&_!==document&&_.nodeName!=="HTML"&&_.nodeName!=="BODY"&&"classList"in _&&"contains"in _.classList)}function c(_){var de=_.type,be=_.tagName;return!!(be==="INPUT"&&a[de]&&!_.readOnly||be==="TEXTAREA"&&!_.readOnly||_.isContentEditable)}function l(_){_.classList.contains("focus-visible")||(_.classList.add("focus-visible"),_.setAttribute("data-focus-visible-added",""))}function u(_){_.hasAttribute("data-focus-visible-added")&&(_.classList.remove("focus-visible"),_.removeAttribute("data-focus-visible-added"))}function p(_){_.metaKey||_.altKey||_.ctrlKey||(s(r.activeElement)&&l(r.activeElement),n=!0)}function d(_){n=!1}function m(_){s(_.target)&&(n||c(_.target))&&l(_.target)}function h(_){s(_.target)&&(_.target.classList.contains("focus-visible")||_.target.hasAttribute("data-focus-visible-added"))&&(o=!0,window.clearTimeout(i),i=window.setTimeout(function(){o=!1},100),u(_.target))}function v(_){document.visibilityState==="hidden"&&(o&&(n=!0),x())}function x(){document.addEventListener("mousemove",E),document.addEventListener("mousedown",E),document.addEventListener("mouseup",E),document.addEventListener("pointermove",E),document.addEventListener("pointerdown",E),document.addEventListener("pointerup",E),document.addEventListener("touchmove",E),document.addEventListener("touchstart",E),document.addEventListener("touchend",E)}function w(){document.removeEventListener("mousemove",E),document.removeEventListener("mousedown",E),document.removeEventListener("mouseup",E),document.removeEventListener("pointermove",E),document.removeEventListener("pointerdown",E),document.removeEventListener("pointerup",E),document.removeEventListener("touchmove",E),document.removeEventListener("touchstart",E),document.removeEventListener("touchend",E)}function E(_){_.target.nodeName&&_.target.nodeName.toLowerCase()==="html"||(n=!1,w())}document.addEventListener("keydown",p,!0),document.addEventListener("mousedown",d,!0),document.addEventListener("pointerdown",d,!0),document.addEventListener("touchstart",d,!0),document.addEventListener("visibilitychange",v,!0),x(),r.addEventListener("focus",m,!0),r.addEventListener("blur",h,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)}))});var So=Cn((M0,vs)=>{"use strict";var Gu=/["'&<>]/;vs.exports=Ju;function Ju(e){var t=""+e,r=Gu.exec(t);if(!r)return t;var n,o="",i=0,a=0;for(i=r.index;i{(function(t,r){typeof jr=="object"&&typeof Lo=="object"?Lo.exports=r():typeof define=="function"&&define.amd?define([],r):typeof jr=="object"?jr.ClipboardJS=r():t.ClipboardJS=r()})(jr,function(){return(function(){var e={686:(function(n,o,i){"use strict";i.d(o,{default:function(){return vr}});var a=i(279),s=i.n(a),c=i(370),l=i.n(c),u=i(817),p=i.n(u);function d(B){try{return document.execCommand(B)}catch(C){return!1}}var m=function(C){var k=p()(C);return d("cut"),k},h=m;function v(B){var C=document.documentElement.getAttribute("dir")==="rtl",k=document.createElement("textarea");k.style.fontSize="12pt",k.style.border="0",k.style.padding="0",k.style.margin="0",k.style.position="absolute",k.style[C?"right":"left"]="-9999px";var D=window.pageYOffset||document.documentElement.scrollTop;return k.style.top="".concat(D,"px"),k.setAttribute("readonly",""),k.value=B,k}var x=function(C,k){var D=v(C);k.container.appendChild(D);var W=p()(D);return d("copy"),D.remove(),W},w=function(C){var k=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},D="";return typeof C=="string"?D=x(C,k):C instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(C==null?void 0:C.type)?D=x(C.value,k):(D=p()(C),d("copy")),D},E=w;function _(B){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?_=function(k){return typeof k}:_=function(k){return k&&typeof Symbol=="function"&&k.constructor===Symbol&&k!==Symbol.prototype?"symbol":typeof k},_(B)}var de=function(){var C=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},k=C.action,D=k===void 0?"copy":k,W=C.container,Z=C.target,We=C.text;if(D!=="copy"&&D!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(Z!==void 0)if(Z&&_(Z)==="object"&&Z.nodeType===1){if(D==="copy"&&Z.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(D==="cut"&&(Z.hasAttribute("readonly")||Z.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if(We)return E(We,{container:W});if(Z)return D==="cut"?h(Z):E(Z,{container:W})},be=de;function M(B){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?M=function(k){return typeof k}:M=function(k){return k&&typeof Symbol=="function"&&k.constructor===Symbol&&k!==Symbol.prototype?"symbol":typeof k},M(B)}function O(B,C){if(!(B instanceof C))throw new TypeError("Cannot call a class as a function")}function N(B,C){for(var k=0;k0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof W.action=="function"?W.action:this.defaultAction,this.target=typeof W.target=="function"?W.target:this.defaultTarget,this.text=typeof W.text=="function"?W.text:this.defaultText,this.container=M(W.container)==="object"?W.container:document.body}},{key:"listenClick",value:function(W){var Z=this;this.listener=l()(W,"click",function(We){return Z.onClick(We)})}},{key:"onClick",value:function(W){var Z=W.delegateTarget||W.currentTarget,We=this.action(Z)||"copy",Gt=be({action:We,container:this.container,target:this.target(Z),text:this.text(Z)});this.emit(Gt?"success":"error",{action:We,text:Gt,trigger:Z,clearSelection:function(){Z&&Z.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(W){return Yt("action",W)}},{key:"defaultTarget",value:function(W){var Z=Yt("target",W);if(Z)return document.querySelector(Z)}},{key:"defaultText",value:function(W){return Yt("text",W)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(W){var Z=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return E(W,Z)}},{key:"cut",value:function(W){return h(W)}},{key:"isSupported",value:function(){var W=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],Z=typeof W=="string"?[W]:W,We=!!document.queryCommandSupported;return Z.forEach(function(Gt){We=We&&!!document.queryCommandSupported(Gt)}),We}}]),k})(s()),vr=Mt}),828:(function(n){var o=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function a(s,c){for(;s&&s.nodeType!==o;){if(typeof s.matches=="function"&&s.matches(c))return s;s=s.parentNode}}n.exports=a}),438:(function(n,o,i){var a=i(828);function s(u,p,d,m,h){var v=l.apply(this,arguments);return u.addEventListener(d,v,h),{destroy:function(){u.removeEventListener(d,v,h)}}}function c(u,p,d,m,h){return typeof u.addEventListener=="function"?s.apply(null,arguments):typeof d=="function"?s.bind(null,document).apply(null,arguments):(typeof u=="string"&&(u=document.querySelectorAll(u)),Array.prototype.map.call(u,function(v){return s(v,p,d,m,h)}))}function l(u,p,d,m){return function(h){h.delegateTarget=a(h.target,p),h.delegateTarget&&m.call(u,h)}}n.exports=c}),879:(function(n,o){o.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},o.nodeList=function(i){var a=Object.prototype.toString.call(i);return i!==void 0&&(a==="[object NodeList]"||a==="[object HTMLCollection]")&&"length"in i&&(i.length===0||o.node(i[0]))},o.string=function(i){return typeof i=="string"||i instanceof String},o.fn=function(i){var a=Object.prototype.toString.call(i);return a==="[object Function]"}}),370:(function(n,o,i){var a=i(879),s=i(438);function c(d,m,h){if(!d&&!m&&!h)throw new Error("Missing required arguments");if(!a.string(m))throw new TypeError("Second argument must be a String");if(!a.fn(h))throw new TypeError("Third argument must be a Function");if(a.node(d))return l(d,m,h);if(a.nodeList(d))return u(d,m,h);if(a.string(d))return p(d,m,h);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function l(d,m,h){return d.addEventListener(m,h),{destroy:function(){d.removeEventListener(m,h)}}}function u(d,m,h){return Array.prototype.forEach.call(d,function(v){v.addEventListener(m,h)}),{destroy:function(){Array.prototype.forEach.call(d,function(v){v.removeEventListener(m,h)})}}}function p(d,m,h){return s(document.body,d,m,h)}n.exports=c}),817:(function(n){function o(i){var a;if(i.nodeName==="SELECT")i.focus(),a=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var s=i.hasAttribute("readonly");s||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),s||i.removeAttribute("readonly"),a=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var c=window.getSelection(),l=document.createRange();l.selectNodeContents(i),c.removeAllRanges(),c.addRange(l),a=c.toString()}return a}n.exports=o}),279:(function(n){function o(){}o.prototype={on:function(i,a,s){var c=this.e||(this.e={});return(c[i]||(c[i]=[])).push({fn:a,ctx:s}),this},once:function(i,a,s){var c=this;function l(){c.off(i,l),a.apply(s,arguments)}return l._=a,this.on(i,l,s)},emit:function(i){var a=[].slice.call(arguments,1),s=((this.e||(this.e={}))[i]||[]).slice(),c=0,l=s.length;for(c;c0&&i[i.length-1])&&(l[0]===6||l[0]===2)){r=0;continue}if(l[0]===3&&(!i||l[1]>i[0]&&l[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function te(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var n=r.call(e),o,i=[],a;try{for(;(t===void 0||t-- >0)&&!(o=n.next()).done;)i.push(o.value)}catch(s){a={error:s}}finally{try{o&&!o.done&&(r=n.return)&&r.call(n)}finally{if(a)throw a.error}}return i}function ne(e,t,r){if(r||arguments.length===2)for(var n=0,o=t.length,i;n1||c(m,v)})},h&&(o[m]=h(o[m])))}function c(m,h){try{l(n[m](h))}catch(v){d(i[0][3],v)}}function l(m){m.value instanceof kt?Promise.resolve(m.value.v).then(u,p):d(i[0][2],m)}function u(m){c("next",m)}function p(m){c("throw",m)}function d(m,h){m(h),i.shift(),i.length&&c(i[0][0],i[0][1])}}function zo(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],r;return t?t.call(e):(e=typeof $e=="function"?$e(e):e[Symbol.iterator](),r={},n("next"),n("throw"),n("return"),r[Symbol.asyncIterator]=function(){return this},r);function n(i){r[i]=e[i]&&function(a){return new Promise(function(s,c){a=e[i](a),o(s,c,a.done,a.value)})}}function o(i,a,s,c){Promise.resolve(c).then(function(l){i({value:l,done:s})},a)}}function F(e){return typeof e=="function"}function Jt(e){var t=function(n){Error.call(n),n.stack=new Error().stack},r=e(t);return r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r}var Vr=Jt(function(e){return function(r){e(this),this.message=r?r.length+` errors occurred during unsubscription: `+r.map(function(n,o){return o+1+") "+n.toString()}).join(` `):"",this.name="UnsubscriptionError",this.errors=r}});function ct(e,t){if(e){var r=e.indexOf(t);0<=r&&e.splice(r,1)}}var rt=(function(){function e(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var t,r,n,o,i;if(!this.closed){this.closed=!0;var a=this._parentage;if(a)if(this._parentage=null,Array.isArray(a))try{for(var s=$e(a),c=s.next();!c.done;c=s.next()){var l=c.value;l.remove(this)}}catch(v){t={error:v}}finally{try{c&&!c.done&&(r=s.return)&&r.call(s)}finally{if(t)throw t.error}}else a.remove(this);var u=this.initialTeardown;if(F(u))try{u()}catch(v){i=v instanceof Vr?v.errors:[v]}var p=this._finalizers;if(p){this._finalizers=null;try{for(var d=$e(p),m=d.next();!m.done;m=d.next()){var h=m.value;try{qo(h)}catch(v){i=i!=null?i:[],v instanceof Vr?i=ne(ne([],te(i)),te(v.errors)):i.push(v)}}}catch(v){n={error:v}}finally{try{m&&!m.done&&(o=d.return)&&o.call(d)}finally{if(n)throw n.error}}}if(i)throw new Vr(i)}},e.prototype.add=function(t){var r;if(t&&t!==this)if(this.closed)qo(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(r=this._finalizers)!==null&&r!==void 0?r:[]).push(t)}},e.prototype._hasParent=function(t){var r=this._parentage;return r===t||Array.isArray(r)&&r.includes(t)},e.prototype._addParent=function(t){var r=this._parentage;this._parentage=Array.isArray(r)?(r.push(t),r):r?[r,t]:t},e.prototype._removeParent=function(t){var r=this._parentage;r===t?this._parentage=null:Array.isArray(r)&&ct(r,t)},e.prototype.remove=function(t){var r=this._finalizers;r&&ct(r,t),t instanceof e&&t._removeParent(this)},e.EMPTY=(function(){var t=new e;return t.closed=!0,t})(),e})();var Pn=rt.EMPTY;function zr(e){return e instanceof rt||e&&"closed"in e&&F(e.remove)&&F(e.add)&&F(e.unsubscribe)}function qo(e){F(e)?e():e.unsubscribe()}var Je={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var Xt={setTimeout:function(e,t){for(var r=[],n=2;n0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var n=this,o=this,i=o.hasError,a=o.isStopped,s=o.observers;return i||a?Pn:(this.currentObservers=null,s.push(r),new rt(function(){n.currentObservers=null,ct(s,r)}))},t.prototype._checkFinalizedStatuses=function(r){var n=this,o=n.hasError,i=n.thrownError,a=n.isStopped;o?r.error(i):a&&r.complete()},t.prototype.asObservable=function(){var r=new U;return r.source=this,r},t.create=function(r,n){return new Qo(r,n)},t})(U);var Qo=(function(e){ue(t,e);function t(r,n){var o=e.call(this)||this;return o.destination=r,o.source=n,o}return t.prototype.next=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.next)===null||o===void 0||o.call(n,r)},t.prototype.error=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.error)===null||o===void 0||o.call(n,r)},t.prototype.complete=function(){var r,n;(n=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||n===void 0||n.call(r)},t.prototype._subscribe=function(r){var n,o;return(o=(n=this.source)===null||n===void 0?void 0:n.subscribe(r))!==null&&o!==void 0?o:Pn},t})(I);var Un=(function(e){ue(t,e);function t(r){var n=e.call(this)||this;return n._value=r,n}return Object.defineProperty(t.prototype,"value",{get:function(){return this.getValue()},enumerable:!1,configurable:!0}),t.prototype._subscribe=function(r){var n=e.prototype._subscribe.call(this,r);return!n.closed&&r.next(this._value),n},t.prototype.getValue=function(){var r=this,n=r.hasError,o=r.thrownError,i=r._value;if(n)throw o;return this._throwIfClosed(),i},t.prototype.next=function(r){e.prototype.next.call(this,this._value=r)},t})(I);var xr={now:function(){return(xr.delegate||Date).now()},delegate:void 0};var wr=(function(e){ue(t,e);function t(r,n,o){r===void 0&&(r=1/0),n===void 0&&(n=1/0),o===void 0&&(o=xr);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=n,i._timestampProvider=o,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=n===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,n),i}return t.prototype.next=function(r){var n=this,o=n.isStopped,i=n._buffer,a=n._infiniteTimeWindow,s=n._timestampProvider,c=n._windowTime;o||(i.push(r),!a&&i.push(s.now()+c)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var n=this._innerSubscribe(r),o=this,i=o._infiniteTimeWindow,a=o._buffer,s=a.slice(),c=0;c0?e.prototype.schedule.call(this,r,n):(this.delay=n,this.state=r,this.scheduler.flush(this),this)},t.prototype.execute=function(r,n){return n>0||this.closed?e.prototype.execute.call(this,r,n):this._execute(r,n)},t.prototype.requestAsyncId=function(r,n,o){return o===void 0&&(o=0),o!=null&&o>0||o==null&&this.delay>0?e.prototype.requestAsyncId.call(this,r,n,o):(r.flush(this),0)},t})(tr);var ri=(function(e){ue(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t})(rr);var Wn=new ri(ti);var ni=(function(e){ue(t,e);function t(r,n){var o=e.call(this,r,n)||this;return o.scheduler=r,o.work=n,o}return t.prototype.requestAsyncId=function(r,n,o){return o===void 0&&(o=0),o!==null&&o>0?e.prototype.requestAsyncId.call(this,r,n,o):(r.actions.push(this),r._scheduled||(r._scheduled=er.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,n,o){var i;if(o===void 0&&(o=0),o!=null?o>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,n,o);var a=r.actions;n!=null&&n===r._scheduled&&((i=a[a.length-1])===null||i===void 0?void 0:i.id)!==n&&(er.cancelAnimationFrame(n),r._scheduled=void 0)},t})(tr);var oi=(function(e){ue(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var n;r?n=r.id:(n=this._scheduled,this._scheduled=void 0);var o=this.actions,i;r=r||o.shift();do if(i=r.execute(r.state,r.delay))break;while((r=o[0])&&r.id===n&&o.shift());if(this._active=!1,i){for(;(r=o[0])&&r.id===n&&o.shift();)r.unsubscribe();throw i}},t})(rr);var je=new oi(ni);var y=new U(function(e){return e.complete()});function Br(e){return e&&F(e.schedule)}function Vn(e){return e[e.length-1]}function _t(e){return F(Vn(e))?e.pop():void 0}function qe(e){return Br(Vn(e))?e.pop():void 0}function Yr(e,t){return typeof Vn(e)=="number"?e.pop():t}var nr=(function(e){return e&&typeof e.length=="number"&&typeof e!="function"});function Gr(e){return F(e==null?void 0:e.then)}function Jr(e){return F(e[Qt])}function Xr(e){return Symbol.asyncIterator&&F(e==null?void 0:e[Symbol.asyncIterator])}function Zr(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function Rc(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var Qr=Rc();function en(e){return F(e==null?void 0:e[Qr])}function tn(e){return Vo(this,arguments,function(){var r,n,o,i;return Wr(this,function(a){switch(a.label){case 0:r=e.getReader(),a.label=1;case 1:a.trys.push([1,,9,10]),a.label=2;case 2:return[4,kt(r.read())];case 3:return n=a.sent(),o=n.value,i=n.done,i?[4,kt(void 0)]:[3,5];case 4:return[2,a.sent()];case 5:return[4,kt(o)];case 6:return[4,a.sent()];case 7:return a.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function rn(e){return F(e==null?void 0:e.getReader)}function q(e){if(e instanceof U)return e;if(e!=null){if(Jr(e))return jc(e);if(nr(e))return Fc(e);if(Gr(e))return Uc(e);if(Xr(e))return ii(e);if(en(e))return Nc(e);if(rn(e))return Dc(e)}throw Zr(e)}function jc(e){return new U(function(t){var r=e[Qt]();if(F(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function Fc(e){return new U(function(t){for(var r=0;r=2;return function(n){return n.pipe(e?L(function(o,i){return e(o,i,n)}):Oe,Me(1),r?ot(t):wi(function(){return new on}))}}function Gn(e){return e<=0?function(){return y}:S(function(t,r){var n=[];t.subscribe(T(r,function(o){n.push(o),e=2,!0))}function xe(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new I}:t,n=e.resetOnError,o=n===void 0?!0:n,i=e.resetOnComplete,a=i===void 0?!0:i,s=e.resetOnRefCountZero,c=s===void 0?!0:s;return function(l){var u,p,d,m=0,h=!1,v=!1,x=function(){p==null||p.unsubscribe(),p=void 0},w=function(){x(),u=d=void 0,h=v=!1},E=function(){var _=u;w(),_==null||_.unsubscribe()};return S(function(_,de){m++,!v&&!h&&x();var be=d=d!=null?d:r();de.add(function(){m--,m===0&&!v&&!h&&(p=Jn(E,c))}),be.subscribe(de),!u&&m>0&&(u=new Ct({next:function(M){return be.next(M)},error:function(M){v=!0,x(),p=Jn(w,o,M),be.error(M)},complete:function(){h=!0,x(),p=Jn(w,a),be.complete()}}),q(_).subscribe(u))})(l)}}function Jn(e,t){for(var r=[],n=2;ne.next(document)),e}function P(e,t=document){return Array.from(t.querySelectorAll(e))}function G(e,t=document){let r=Le(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function Le(e,t=document){return t.querySelector(e)||void 0}function xt(){var e,t,r,n;return(n=(r=(t=(e=document.activeElement)==null?void 0:e.shadowRoot)==null?void 0:t.activeElement)!=null?r:document.activeElement)!=null?n:void 0}var il=R(b(document.body,"focusin"),b(document.body,"focusout")).pipe(Be(1),J(void 0),f(()=>xt()||document.body),se(1));function ir(e){return il.pipe(f(t=>e.contains(t)),ie())}function Ft(e,t){let{matches:r}=matchMedia("(hover)");return j(()=>(r?R(b(e,"mouseenter").pipe(f(()=>!0)),b(e,"mouseleave").pipe(f(()=>!1))):R(b(e,"touchstart").pipe(f(()=>!0)),b(e,"touchend").pipe(f(()=>!1)),b(e,"touchcancel").pipe(f(()=>!1)))).pipe(t?Tr(o=>Ve(+!o*t)):Oe,J(!0,e.matches(":hover"))))}function Oi(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)Oi(e,r)}function A(e,t,...r){let n=document.createElement(e);if(t)for(let o of Object.keys(t))typeof t[o]!="undefined"&&(typeof t[o]!="boolean"?n.setAttribute(o,t[o]):n.setAttribute(o,""));for(let o of r)Oi(n,o);return n}function Li(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function ar(e){let t=A("script",{src:e});return j(()=>(document.head.appendChild(t),R(b(t,"load"),b(t,"error").pipe(g(()=>zn(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(f(()=>{}),V(()=>document.head.removeChild(t)),Me(1))))}var Mi=new I,al=j(()=>typeof ResizeObserver=="undefined"?ar("https://unpkg.com/resize-observer-polyfill"):Y(void 0)).pipe(f(()=>new ResizeObserver(e=>e.forEach(t=>Mi.next(t)))),g(e=>R(Ke,Y(e)).pipe(V(()=>e.disconnect()))),se(1));function Ae(e){return{width:e.offsetWidth,height:e.offsetHeight}}function Re(e){let t=e;for(;t.clientWidth===0&&t.parentElement;)t=t.parentElement;return al.pipe($(r=>r.observe(t)),g(r=>Mi.pipe(L(n=>n.target===t),V(()=>r.unobserve(t)))),f(()=>Ae(e)),J(Ae(e)))}function Mr(e){return{width:e.scrollWidth,height:e.scrollHeight}}function ki(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}function Ai(e){let t=[],r=e.parentElement;for(;r;)(e.clientWidth>r.clientWidth||e.clientHeight>r.clientHeight)&&t.push(r),r=(e=r).parentElement;return t.length===0&&t.push(document.documentElement),t}function wt(e){return{x:e.offsetLeft,y:e.offsetTop}}function Ci(e){let t=e.getBoundingClientRect();return{x:t.x+window.scrollX,y:t.y+window.scrollY}}function Hi(e){return R(b(window,"load"),b(window,"resize")).pipe(Xe(0,je),f(()=>wt(e)),J(wt(e)))}function ln(e){return{x:e.scrollLeft,y:e.scrollTop}}function Ut(e){return R(b(e,"scroll"),b(window,"scroll"),b(window,"resize")).pipe(Xe(0,je),f(()=>ln(e)),J(ln(e)))}var $i=new I,sl=j(()=>Y(new IntersectionObserver(e=>{for(let t of e)$i.next(t)},{threshold:0}))).pipe(g(e=>R(Ke,Y(e)).pipe(V(()=>e.disconnect()))),se(1));function Et(e){return sl.pipe($(t=>t.observe(e)),g(t=>$i.pipe(L(({target:r})=>r===e),V(()=>t.unobserve(e)),f(({isIntersecting:r})=>r))))}var cl=Object.create,la=Object.defineProperty,ll=Object.getOwnPropertyDescriptor,ul=Object.getOwnPropertyNames,pl=Object.getPrototypeOf,fl=Object.prototype.hasOwnProperty,ml=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),dl=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of ul(t))!fl.call(e,o)&&o!==r&&la(e,o,{get:()=>t[o],enumerable:!(n=ll(t,o))||n.enumerable});return e},hl=(e,t,r)=>(r=e!=null?cl(pl(e)):{},dl(t||!e||!e.__esModule?la(r,"default",{value:e,enumerable:!0}):r,e)),vl=ml((e,t)=>{var r="Expected a function",n=NaN,o="[object Symbol]",i=/^\s+|\s+$/g,a=/^[-+]0x[0-9a-f]+$/i,s=/^0b[01]+$/i,c=/^0o[0-7]+$/i,l=parseInt,u=typeof global=="object"&&global&&global.Object===Object&&global,p=typeof self=="object"&&self&&self.Object===Object&&self,d=u||p||Function("return this")(),m=Object.prototype,h=m.toString,v=Math.max,x=Math.min,w=function(){return d.Date.now()};function E(O,N,ee){var le,ce,Ne,bt,De,st,tt=0,Yt=!1,Mt=!1,vr=!0;if(typeof O!="function")throw new TypeError(r);N=M(N)||0,_(ee)&&(Yt=!!ee.leading,Mt="maxWait"in ee,Ne=Mt?v(M(ee.maxWait)||0,N):Ne,vr="trailing"in ee?!!ee.trailing:vr);function B(Te){var gt=le,br=ce;return le=ce=void 0,tt=Te,bt=O.apply(br,gt),bt}function C(Te){return tt=Te,De=setTimeout(W,N),Yt?B(Te):bt}function k(Te){var gt=Te-st,br=Te-tt,Ro=N-gt;return Mt?x(Ro,Ne-br):Ro}function D(Te){var gt=Te-st,br=Te-tt;return st===void 0||gt>=N||gt<0||Mt&&br>=Ne}function W(){var Te=w();if(D(Te))return Z(Te);De=setTimeout(W,k(Te))}function Z(Te){return De=void 0,vr&&le?B(Te):(le=ce=void 0,bt)}function We(){De!==void 0&&clearTimeout(De),tt=0,le=st=ce=De=void 0}function Gt(){return De===void 0?bt:Z(w())}function Nr(){var Te=w(),gt=D(Te);if(le=arguments,ce=this,st=Te,gt){if(De===void 0)return C(st);if(Mt)return De=setTimeout(W,N),B(st)}return De===void 0&&(De=setTimeout(W,N)),bt}return Nr.cancel=We,Nr.flush=Gt,Nr}function _(O){var N=typeof O;return!!O&&(N=="object"||N=="function")}function de(O){return!!O&&typeof O=="object"}function be(O){return typeof O=="symbol"||de(O)&&h.call(O)==o}function M(O){if(typeof O=="number")return O;if(be(O))return n;if(_(O)){var N=typeof O.valueOf=="function"?O.valueOf():O;O=_(N)?N+"":N}if(typeof O!="string")return O===0?O:+O;O=O.replace(i,"");var ee=s.test(O);return ee||c.test(O)?l(O.slice(2),ee?2:8):a.test(O)?n:+O}t.exports=E}),yn,K,ua,pa,Nt,Pi,fa,ma,da,lo,to,ro,bl,Ar={},ha=[],gl=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,Pr=Array.isArray;function pt(e,t){for(var r in t)e[r]=t[r];return e}function uo(e){e&&e.parentNode&&e.parentNode.removeChild(e)}function Wt(e,t,r){var n,o,i,a={};for(i in t)i=="key"?n=t[i]:i=="ref"?o=t[i]:a[i]=t[i];if(arguments.length>2&&(a.children=arguments.length>3?yn.call(arguments,2):r),typeof e=="function"&&e.defaultProps!=null)for(i in e.defaultProps)a[i]===void 0&&(a[i]=e.defaultProps[i]);return fn(e,a,n,o,null)}function fn(e,t,r,n,o){var i={type:e,props:t,key:r,ref:n,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:o!=null?o:++ua,__i:-1,__u:0};return o==null&&K.vnode!=null&&K.vnode(i),i}function ft(e){return e.children}function at(e,t){this.props=e,this.context=t}function cr(e,t){if(t==null)return e.__?cr(e.__,e.__i+1):null;for(var r;ts&&Nt.sort(ma),e=Nt.shift(),s=Nt.length,e.__d&&(r=void 0,n=void 0,o=(n=(t=e).__v).__e,i=[],a=[],t.__P&&((r=pt({},n)).__v=n.__v+1,K.vnode&&K.vnode(r),po(t.__P,r,n,t.__n,t.__P.namespaceURI,32&n.__u?[o]:null,i,o!=null?o:cr(n),!!(32&n.__u),a),r.__v=n.__v,r.__.__k[r.__i]=r,_a(i,r,a),n.__e=n.__=null,r.__e!=o&&va(r)));vn.__r=0}function ba(e,t,r,n,o,i,a,s,c,l,u){var p,d,m,h,v,x,w,E=n&&n.__k||ha,_=t.length;for(c=_l(r,t,E,c,_),p=0;p<_;p++)(m=r.__k[p])!=null&&(d=m.__i==-1?Ar:E[m.__i]||Ar,m.__i=p,x=po(e,m,d,o,i,a,s,c,l,u),h=m.__e,m.ref&&d.ref!=m.ref&&(d.ref&&fo(d.ref,null,m),u.push(m.ref,m.__c||h,m)),v==null&&h!=null&&(v=h),(w=!!(4&m.__u))||d.__k===m.__k?c=ga(m,c,e,w):typeof m.type=="function"&&x!==void 0?c=x:h&&(c=h.nextSibling),m.__u&=-7);return r.__e=v,c}function _l(e,t,r,n,o){var i,a,s,c,l,u=r.length,p=u,d=0;for(e.__k=new Array(o),i=0;i0?fn(a.type,a.props,a.key,a.ref?a.ref:null,a.__v):a).__=e,a.__b=e.__b+1,s=null,(l=a.__i=yl(a,r,c,p))!=-1&&(p--,(s=r[l])&&(s.__u|=2)),s==null||s.__v==null?(l==-1&&(o>u?d--:oc?d--:d++,a.__u|=4))):e.__k[i]=null;if(p)for(i=0;i(u?1:0)){for(o=r-1,i=r+1;o>=0||i=0?o--:i++])!=null&&!(2&l.__u)&&s==l.key&&c==l.type)return a}return-1}function Ri(e,t,r){t[0]=="-"?e.setProperty(t,r!=null?r:""):e[t]=r==null?"":typeof r!="number"||gl.test(t)?r:r+"px"}function un(e,t,r,n,o){var i,a;e:if(t=="style")if(typeof r=="string")e.style.cssText=r;else{if(typeof n=="string"&&(e.style.cssText=n=""),n)for(t in n)r&&t in r||Ri(e.style,t,"");if(r)for(t in r)n&&r[t]==n[t]||Ri(e.style,t,r[t])}else if(t[0]=="o"&&t[1]=="n")i=t!=(t=t.replace(da,"$1")),a=t.toLowerCase(),t=a in e||t=="onFocusOut"||t=="onFocusIn"?a.slice(2):t.slice(2),e.l||(e.l={}),e.l[t+i]=r,r?n?r.u=n.u:(r.u=lo,e.addEventListener(t,i?ro:to,i)):e.removeEventListener(t,i?ro:to,i);else{if(o=="http://www.w3.org/2000/svg")t=t.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if(t!="width"&&t!="height"&&t!="href"&&t!="list"&&t!="form"&&t!="tabIndex"&&t!="download"&&t!="rowSpan"&&t!="colSpan"&&t!="role"&&t!="popover"&&t in e)try{e[t]=r!=null?r:"";break e}catch(s){}typeof r=="function"||(r==null||r===!1&&t[4]!="-"?e.removeAttribute(t):e.setAttribute(t,t=="popover"&&r==1?"":r))}}function ji(e){return function(t){if(this.l){var r=this.l[t.type+e];if(t.t==null)t.t=lo++;else if(t.t0?e:Pr(e)?e.map(ya):pt({},e)}function xl(e,t,r,n,o,i,a,s,c){var l,u,p,d,m,h,v,x=r.props,w=t.props,E=t.type;if(E=="svg"?o="http://www.w3.org/2000/svg":E=="math"?o="http://www.w3.org/1998/Math/MathML":o||(o="http://www.w3.org/1999/xhtml"),i!=null){for(l=0;l=r.__.length&&r.__.push({}),r.__[e]}function bn(e){return $r=1,Tl(Ta,e)}function Tl(e,t,r){var n=mo(Hr++,2);if(n.t=e,!n.__c&&(n.__=[r?r(t):Ta(void 0,t),function(s){var c=n.__N?n.__N[0]:n.__[0],l=n.t(c,s);c!==l&&(n.__N=[l,n.__[1]],n.__c.setState({}))}],n.__c=ve,!ve.__f)){var o=function(s,c,l){if(!n.__c.__H)return!0;var u=n.__c.__H.__.filter(function(d){return!!d.__c});if(u.every(function(d){return!d.__N}))return!i||i.call(this,s,c,l);var p=n.__c.props!==s;return u.forEach(function(d){if(d.__N){var m=d.__[0];d.__=d.__N,d.__N=void 0,m!==d.__[0]&&(p=!0)}}),i&&i.call(this,s,c,l)||p};ve.__f=!0;var i=ve.shouldComponentUpdate,a=ve.componentWillUpdate;ve.componentWillUpdate=function(s,c,l){if(this.__e){var u=i;i=void 0,o(s,c,l),i=u}a&&a.call(this,s,c,l)},ve.shouldComponentUpdate=o}return n.__N||n.__}function mt(e,t){var r=mo(Hr++,3);!we.__s&&Ea(r.__H,t)&&(r.__=e,r.u=t,ve.__H.__h.push(r))}function Vt(e){return $r=5,ur(function(){return{current:e}},[])}function ur(e,t){var r=mo(Hr++,7);return Ea(r.__H,t)&&(r.__=e(),r.__H=t,r.__h=e),r.__}function Sl(e,t){return $r=8,ur(function(){return e},t)}function Ol(){for(var e;e=wa.shift();)if(e.__P&&e.__H)try{e.__H.__h.forEach(mn),e.__H.__h.forEach(oo),e.__H.__h=[]}catch(t){e.__H.__h=[],we.__e(t,e.__v)}}we.__b=function(e){ve=null,Ui&&Ui(e)},we.__=function(e,t){e&&t.__k&&t.__k.__m&&(e.__m=t.__k.__m),zi&&zi(e,t)},we.__r=function(e){Ni&&Ni(e),Hr=0;var t=(ve=e.__c).__H;t&&(Zn===ve?(t.__h=[],ve.__h=[],t.__.forEach(function(r){r.__N&&(r.__=r.__N),r.u=r.__N=void 0})):(t.__h.forEach(mn),t.__h.forEach(oo),t.__h=[],Hr=0)),Zn=ve},we.diffed=function(e){Di&&Di(e);var t=e.__c;t&&t.__H&&(t.__H.__h.length&&(wa.push(t)!==1&&Fi===we.requestAnimationFrame||((Fi=we.requestAnimationFrame)||Ll)(Ol)),t.__H.__.forEach(function(r){r.u&&(r.__H=r.u),r.u=void 0})),Zn=ve=null},we.__c=function(e,t){t.some(function(r){try{r.__h.forEach(mn),r.__h=r.__h.filter(function(n){return!n.__||oo(n)})}catch(n){t.some(function(o){o.__h&&(o.__h=[])}),t=[],we.__e(n,r.__v)}}),Wi&&Wi(e,t)},we.unmount=function(e){Vi&&Vi(e);var t,r=e.__c;r&&r.__H&&(r.__H.__.forEach(function(n){try{mn(n)}catch(o){t=o}}),r.__H=void 0,t&&we.__e(t,r.__v))};var qi=typeof requestAnimationFrame=="function";function Ll(e){var t,r=function(){clearTimeout(n),qi&&cancelAnimationFrame(t),setTimeout(e)},n=setTimeout(r,35);qi&&(t=requestAnimationFrame(r))}function mn(e){var t=ve,r=e.__c;typeof r=="function"&&(e.__c=void 0,r()),ve=t}function oo(e){var t=ve;e.__c=e.__(),ve=t}function Ea(e,t){return!e||e.length!==t.length||t.some(function(r,n){return r!==e[n]})}function Ta(e,t){return typeof t=="function"?t(e):t}function Ml(e,t){for(var r in t)e[r]=t[r];return e}function Ki(e,t){for(var r in e)if(r!=="__source"&&!(r in t))return!0;for(var n in t)if(n!=="__source"&&e[n]!==t[n])return!0;return!1}function Bi(e,t){this.props=e,this.context=t}(Bi.prototype=new at).isPureReactComponent=!0,Bi.prototype.shouldComponentUpdate=function(e,t){return Ki(this.props,e)||Ki(this.state,t)};var Yi=K.__b;K.__b=function(e){e.type&&e.type.__f&&e.ref&&(e.props.ref=e.ref,e.ref=null),Yi&&Yi(e)};var Yx=typeof Symbol<"u"&&Symbol.for&&Symbol.for("react.forward_ref")||3911,kl=K.__e;K.__e=function(e,t,r,n){if(e.then){for(var o,i=t;i=i.__;)if((o=i.__c)&&o.__c)return t.__e==null&&(t.__e=r.__e,t.__k=r.__k),o.__c(e,t)}kl(e,t,r,n)};var Gi=K.unmount;function Sa(e,t,r){return e&&(e.__c&&e.__c.__H&&(e.__c.__H.__.forEach(function(n){typeof n.__c=="function"&&n.__c()}),e.__c.__H=null),(e=Ml({},e)).__c!=null&&(e.__c.__P===r&&(e.__c.__P=t),e.__c.__e=!0,e.__c=null),e.__k=e.__k&&e.__k.map(function(n){return Sa(n,t,r)})),e}function Oa(e,t,r){return e&&r&&(e.__v=null,e.__k=e.__k&&e.__k.map(function(n){return Oa(n,t,r)}),e.__c&&e.__c.__P===t&&(e.__e&&r.appendChild(e.__e),e.__c.__e=!0,e.__c.__P=r)),e}function Qn(){this.__u=0,this.o=null,this.__b=null}function La(e){var t=e.__.__c;return t&&t.__a&&t.__a(e)}function pn(){this.i=null,this.l=null}K.unmount=function(e){var t=e.__c;t&&t.__R&&t.__R(),t&&32&e.__u&&(e.type=null),Gi&&Gi(e)},(Qn.prototype=new at).__c=function(e,t){var r=t.__c,n=this;n.o==null&&(n.o=[]),n.o.push(r);var o=La(n.__v),i=!1,a=function(){i||(i=!0,r.__R=null,o?o(s):s())};r.__R=a;var s=function(){if(!--n.__u){if(n.state.__a){var c=n.state.__a;n.__v.__k[0]=Oa(c,c.__c.__P,c.__c.__O)}var l;for(n.setState({__a:n.__b=null});l=n.o.pop();)l.forceUpdate()}};n.__u++||32&t.__u||n.setState({__a:n.__b=n.__v.__k[0]}),e.then(a,a)},Qn.prototype.componentWillUnmount=function(){this.o=[]},Qn.prototype.render=function(e,t){if(this.__b){if(this.__v.__k){var r=document.createElement("div"),n=this.__v.__k[0].__c;this.__v.__k[0]=Sa(this.__b,r,n.__O=n.__P)}this.__b=null}var o=t.__a&&Wt(ft,null,e.fallback);return o&&(o.__u&=-33),[Wt(ft,null,t.__a?null:e.children),o]};var Ji=function(e,t,r){if(++r[1]===r[0]&&e.l.delete(t),e.props.revealOrder&&(e.props.revealOrder[0]!=="t"||!e.l.size))for(r=e.i;r;){for(;r.length>3;)r.pop()();if(r[1]Object.freeze({get current(){return t.current}}),[])}var Nl=typeof globalThis<"u"&&typeof navigator<"u"&&typeof document<"u";function Dl(e,...t){var r;(r=e==null?void 0:e.addEventListener)==null||r.call(e,...t)}function Wl(e,...t){var r;(r=e==null?void 0:e.removeEventListener)==null||r.call(e,...t)}var Vl=(e,t)=>Object.hasOwn(e,t),zl=()=>!0,ql=()=>!1;function Kl(e=!1){let t=Vt(e),r=Sl(()=>t.current,[]);return mt(()=>(t.current=!0,()=>{t.current=!1}),[]),r}function Bl(e,...t){let r=Kl(),n=ka(t[1]),o=ur(()=>function(...i){r()&&(typeof n.current=="function"?n.current.apply(this,i):typeof n.current.handleEvent=="function"&&n.current.handleEvent.apply(this,i))},[]);mt(()=>{let i=Yl(e)?e.current:e;if(!i)return;let a=t.slice(2);return Dl(i,t[0],o,...a),()=>{Wl(i,t[0],o,...a)}},[e,t[0]])}function Yl(e){return e!==null&&typeof e=="object"&&Vl(e,"current")}var Gl=e=>typeof e=="function"?e:typeof e=="string"?t=>t.key===e:e?zl:ql,Jl=Nl?globalThis:null;function Aa(e,t,r=[],n={}){let{event:o="keydown",target:i=Jl,eventOptions:a}=n,s=ka(t),c=ur(()=>{let l=Gl(e);return function(u){l(u)&&s.current.call(this,u)}},r);Bl(i,o,c,a)}function Ca(e){var t,r,n="";if(typeof e=="string"||typeof e=="number")n+=e;else if(typeof e=="object")if(Array.isArray(e)){var o=e.length;for(t=0;t1)St--;else{for(var e,t=!1;kr!==void 0;){var r=kr;for(kr=void 0,io++;r!==void 0;){var n=r.o;if(r.o=void 0,r.f&=-3,!(8&r.f)&&Pa(r))try{r.c()}catch(o){t||(e=o,t=!0)}r=n}}if(io=0,St--,t)throw e}}function Ql(e){if(St>0)return e();St++;try{return e()}finally{xn()}}var ae=void 0;function Ha(e){var t=ae;ae=void 0;try{return e()}finally{ae=t}}var kr=void 0,St=0,io=0,gn=0;function $a(e){if(ae!==void 0){var t=e.n;if(t===void 0||t.t!==ae)return t={i:0,S:e,p:ae.s,n:void 0,t:ae,e:void 0,x:void 0,r:t},ae.s!==void 0&&(ae.s.n=t),ae.s=t,e.n=t,32&ae.f&&e.S(t),t;if(t.i===-1)return t.i=0,t.n!==void 0&&(t.n.p=t.p,t.p!==void 0&&(t.p.n=t.n),t.p=ae.s,t.n=void 0,ae.s.n=t,ae.s=t),t}}function Ce(e,t){this.v=e,this.i=0,this.n=void 0,this.t=void 0,this.W=t==null?void 0:t.watched,this.Z=t==null?void 0:t.unwatched,this.name=t==null?void 0:t.name}Ce.prototype.brand=Zl;Ce.prototype.h=function(){return!0};Ce.prototype.S=function(e){var t=this,r=this.t;r!==e&&e.e===void 0&&(e.x=r,this.t=e,r!==void 0?r.e=e:Ha(function(){var n;(n=t.W)==null||n.call(t)}))};Ce.prototype.U=function(e){var t=this;if(this.t!==void 0){var r=e.e,n=e.x;r!==void 0&&(r.x=n,e.e=void 0),n!==void 0&&(n.e=r,e.x=void 0),e===this.t&&(this.t=n,n===void 0&&Ha(function(){var o;(o=t.Z)==null||o.call(t)}))}};Ce.prototype.subscribe=function(e){var t=this;return qt(function(){var r=t.value,n=ae;ae=void 0;try{e(r)}finally{ae=n}},{name:"sub"})};Ce.prototype.valueOf=function(){return this.value};Ce.prototype.toString=function(){return this.value+""};Ce.prototype.toJSON=function(){return this.value};Ce.prototype.peek=function(){var e=ae;ae=void 0;try{return this.value}finally{ae=e}};Object.defineProperty(Ce.prototype,"value",{get:function(){var e=$a(this);return e!==void 0&&(e.i=this.i),this.v},set:function(e){if(e!==this.v){if(io>100)throw new Error("Cycle detected");this.v=e,this.i++,gn++,St++;try{for(var t=this.t;t!==void 0;t=t.x)t.t.N()}finally{xn()}}}});function Ot(e,t){return new Ce(e,t)}function Pa(e){for(var t=e.s;t!==void 0;t=t.n)if(t.S.i!==t.i||!t.S.h()||t.S.i!==t.i)return!0;return!1}function Ia(e){for(var t=e.s;t!==void 0;t=t.n){var r=t.S.n;if(r!==void 0&&(t.r=r),t.S.n=t,t.i=-1,t.n===void 0){e.s=t;break}}}function Ra(e){for(var t=e.s,r=void 0;t!==void 0;){var n=t.p;t.i===-1?(t.S.U(t),n!==void 0&&(n.n=t.n),t.n!==void 0&&(t.n.p=n)):r=t,t.S.n=t.r,t.r!==void 0&&(t.r=void 0),t=n}e.s=r}function Kt(e,t){Ce.call(this,void 0),this.x=e,this.s=void 0,this.g=gn-1,this.f=4,this.W=t==null?void 0:t.watched,this.Z=t==null?void 0:t.unwatched,this.name=t==null?void 0:t.name}Kt.prototype=new Ce;Kt.prototype.h=function(){if(this.f&=-3,1&this.f)return!1;if((36&this.f)==32||(this.f&=-5,this.g===gn))return!0;if(this.g=gn,this.f|=1,this.i>0&&!Pa(this))return this.f&=-2,!0;var e=ae;try{Ia(this),ae=this;var t=this.x();(16&this.f||this.v!==t||this.i===0)&&(this.v=t,this.f&=-17,this.i++)}catch(r){this.v=r,this.f|=16,this.i++}return ae=e,Ra(this),this.f&=-2,!0};Kt.prototype.S=function(e){if(this.t===void 0){this.f|=36;for(var t=this.s;t!==void 0;t=t.n)t.S.S(t)}Ce.prototype.S.call(this,e)};Kt.prototype.U=function(e){if(this.t!==void 0&&(Ce.prototype.U.call(this,e),this.t===void 0)){this.f&=-33;for(var t=this.s;t!==void 0;t=t.n)t.S.U(t)}};Kt.prototype.N=function(){if(!(2&this.f)){this.f|=6;for(var e=this.t;e!==void 0;e=e.x)e.t.N()}};Object.defineProperty(Kt.prototype,"value",{get:function(){if(1&this.f)throw new Error("Cycle detected");var e=$a(this);if(this.h(),e!==void 0&&(e.i=this.i),16&this.f)throw this.v;return this.v}});function ta(e,t){return new Kt(e,t)}function ja(e){var t=e.u;if(e.u=void 0,typeof t=="function"){St++;var r=ae;ae=void 0;try{t()}catch(n){throw e.f&=-2,e.f|=8,ho(e),n}finally{ae=r,xn()}}}function ho(e){for(var t=e.s;t!==void 0;t=t.n)t.S.U(t);e.x=void 0,e.s=void 0,ja(e)}function eu(e){if(ae!==this)throw new Error("Out-of-order effect");Ra(this),ae=e,this.f&=-2,8&this.f&&ho(this),xn()}function pr(e,t){this.x=e,this.u=void 0,this.s=void 0,this.o=void 0,this.f=32,this.name=t==null?void 0:t.name}pr.prototype.c=function(){var e=this.S();try{if(8&this.f||this.x===void 0)return;var t=this.x();typeof t=="function"&&(this.u=t)}finally{e()}};pr.prototype.S=function(){if(1&this.f)throw new Error("Cycle detected");this.f|=1,this.f&=-9,ja(this),Ia(this),St++;var e=ae;return ae=this,eu.bind(this,e)};pr.prototype.N=function(){2&this.f||(this.f|=2,this.o=kr,kr=this)};pr.prototype.d=function(){this.f|=8,1&this.f||ho(this)};pr.prototype.dispose=function(){this.d()};function qt(e,t){var r=new pr(e,t);try{r.c()}catch(o){throw r.d(),o}var n=r.d.bind(r);return n[Symbol.dispose]=n,n}var Fa,vo,eo,Ua=[];qt(function(){Fa=this.N})();function fr(e,t){K[e]=t.bind(null,K[e]||function(){})}function _n(e){eo&&eo(),eo=e&&e.S()}function Na(e){var t=this,r=e.data,n=ru(r);n.value=r;var o=ur(function(){for(var s=t,c=t.__v;c=c.__;)if(c.__c){c.__c.__$f|=4;break}var l=ta(function(){var m=n.value.value;return m===0?0:m===!0?"":m||""}),u=ta(function(){return!Array.isArray(l.value)&&!pa(l.value)}),p=qt(function(){if(this.N=Da,u.value){var m=l.value;s.__v&&s.__v.__e&&s.__v.__e.nodeType===3&&(s.__v.__e.data=m)}}),d=t.__$u.d;return t.__$u.d=function(){p(),d.call(this)},[u,l]},[]),i=o[0],a=o[1];return i.value?a.peek():a.value}Na.displayName="ReactiveTextNode";Object.defineProperties(Ce.prototype,{constructor:{configurable:!0,value:void 0},type:{configurable:!0,value:Na},props:{configurable:!0,get:function(){return{data:this}}},__b:{configurable:!0,value:1}});fr("__b",function(e,t){if(typeof t.type=="function"&&typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.exitComponent(),typeof t.type=="string"){var r,n=t.props;for(var o in n)if(o!=="children"){var i=n[o];i instanceof Ce&&(r||(t.__np=r={}),r[o]=i,n[o]=i.peek())}}e(t)});fr("__r",function(e,t){if(typeof t.type=="function"&&typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.enterComponent(t),t.type!==ft){_n();var r,n=t.__c;n&&(n.__$f&=-2,(r=n.__$u)===void 0&&(n.__$u=r=(function(o){var i;return qt(function(){i=this}),i.c=function(){n.__$f|=1,n.setState({})},i})())),vo=n,_n(r)}e(t)});fr("__e",function(e,t,r,n){typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.exitComponent(),_n(),vo=void 0,e(t,r,n)});fr("diffed",function(e,t){typeof t.type=="function"&&typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.exitComponent(),_n(),vo=void 0;var r;if(typeof t.type=="string"&&(r=t.__e)){var n=t.__np,o=t.props;if(n){var i=r.U;if(i)for(var a in i){var s=i[a];s!==void 0&&!(a in n)&&(s.d(),i[a]=void 0)}else i={},r.U=i;for(var c in n){var l=i[c],u=n[c];l===void 0?(l=tu(r,c,u,o),i[c]=l):l.o(u,o)}}}e(t)});function tu(e,t,r,n){var o=t in e&&e.ownerSVGElement===void 0,i=Ot(r);return{o:function(a,s){i.value=a,n=s},d:qt(function(){this.N=Da;var a=i.value.value;n[t]!==a&&(n[t]=a,o?e[t]=a:a?e.setAttribute(t,a):e.removeAttribute(t))})}}fr("unmount",function(e,t){if(typeof t.type=="string"){var r=t.__e;if(r){var n=r.U;if(n){r.U=void 0;for(var o in n){var i=n[o];i&&i.d()}}}}else{var a=t.__c;if(a){var s=a.__$u;s&&(a.__$u=void 0,s.d())}}e(t)});fr("__h",function(e,t,r,n){(n<3||n===9)&&(t.__$f|=2),e(t,r,n)});at.prototype.shouldComponentUpdate=function(e,t){var r=this.__$u,n=r&&r.s!==void 0;for(var o in t)return!0;if(this.__f||typeof this.u=="boolean"&&this.u===!0){var i=2&this.__$f;if(!(n||i||4&this.__$f)||1&this.__$f)return!0}else if(!(n||4&this.__$f)||3&this.__$f)return!0;for(var a in e)if(a!=="__source"&&e[a]!==this.props[a])return!0;for(var s in this.props)if(!(s in e))return!0;return!1};function ru(e,t){return bn(function(){return Ot(e,t)})[0]}var nu=function(e){queueMicrotask(function(){queueMicrotask(e)})};function ou(){Ql(function(){for(var e;e=Ua.shift();)Fa.call(e)})}function Da(){Ua.push(this)===1&&(K.requestAnimationFrame||nu)(ou)}var ao=[0];for(let e=0;e<32;e++)ao.push(ao[e]|1<>>5]>>>e&1}set(e){this.data[e>>>5]|=1<<(e&31)}forEach(e){let t=this.size&31;for(let r=0;r{var r;return(r=t.tags)==null?void 0:r.length})&&(matchMedia("(max-width: 768px)").matches||Wa())}function Dt(){Qe.value=He(H({},Qe.value),{hideSearch:!Qe.value.hideSearch})}function Wa(){Qe.value=He(H({},Qe.value),{hideFilters:!Qe.value.hideFilters})}function dn(){return Qe.value.selectedItem}function so(e){Qe.value=He(H({},Qe.value),{selectedItem:e})}function su(){var e,t;return(t=(e=lr.value)==null?void 0:e.items)!=null?t:[]}function wn(){return typeof Se.value.input=="string"?Se.value.input:""}function Va(e){let t=za();e.length&&!t.length?Se.value=He(H({},Se.value),{page:void 0,input:e}):!e.length&&t.length?Se.value=He(H({},Se.value),{page:void 0,input:{type:"operator",data:{operator:"not",operands:[]}}}):Se.value=He(H({},Se.value),{page:void 0,input:e})}function cu(){typeof it.value.pagination.next<"u"&&(Se.value=He(H({},Se.value),{page:it.value.pagination.next}))}function lu(e){let t=Se.value.filter.input;if("type"in t&&t.type==="operator"){for(let r of t.data.operands)if("type"in r&&r.type==="value"&&typeof r.data.value=="string"&&r.data.value===e)return!0}return!1}function za(){let e=Se.value.filter.input,t=[];if("type"in e&&e.type==="operator")for(let r of e.data.operands)"type"in r&&r.type==="value"&&typeof r.data.value=="string"&&t.push(r.data.value);return t}function uu(e){let t=Se.value.filter.input,r=[];if("type"in t&&t.type==="operator")for(let n of t.data.operands)"type"in n&&n.type==="value"&&typeof n.data.value=="string"&&r.push(n.data.value);if(r.includes(e)){let n=r.indexOf(e);n>-1&&r.splice(n,1)}else r.push(e);Se.value=He(H({},Se.value),{page:void 0,filter:He(H({},Se.value.filter),{input:{type:"operator",data:{operator:"and",operands:r.map(n=>({type:"value",data:{field:"tags",value:n}}))}}})}),Va(wn())}function pu(){return it.value.items}function fu(){return it.value.total}function mu(){var e;for(let t of(e=it.value.aggregations)!=null?e:[])if(t.type==="term")return t.data.value;return[]}function sr(){return Qe.value.hideSearch}function du(){return Qe.value.hideFilters}function qa(){var e;return(e=Ka.value.highlight)!=null?e:!1}var Qe=Ot({hideSearch:!0,hideFilters:!0,selectedItem:0}),Ka=Ot({}),lr=Ot(),na=Ot(),Se=Ot({input:"",filter:{input:{type:"operator",data:{operator:"and",operands:[]}},aggregation:{input:[{type:"term",data:{field:"tags"}}]}}}),it=Ot({items:[],query:{select:{documents:new ra(0),terms:new ra(0)},values:[]},pagination:{total:0}});function hu(e,t,r){for(let n=0;tr&&t(0,o,r,r=i);continue;case 62:e.charCodeAt(r+1)===47?t(2,--o,r,r=i+1):hu(e,r,n)?t(3,o,r,r=i+1):t(1,o++,r,r=i+1)}i>r&&t(0,o,r,i)}function bu(e,t=0,r=e.length){let n=++t;e:for(let l=0;n{let i=[],a=[],{onElement:s,onText:c=gu}=typeof r=="function"?{onElement:r}:r,l=0,u=0;return e(t,(p,d,m,h)=>{if(p===0)i[l++]=c(t,m,h),a[u++]={value:null,depth:d};else if(p&1&&(a[u++]={value:bu(t,m,h),depth:d}),p&2)for(let v=0;u>=0;v++){let{value:x,depth:w}=a[--u];if(w>d)continue;let E=i.slice(l-=v,l+v);i[l++]=s(x,E),u++;break}},n,o),i.slice(0,l)}}function yu(e){return e.replace(/[&<>]/g,t=>{switch(t.charCodeAt(0)){case 38:return"&";case 60:return"<";case 62:return">"}})}function hn(e){return e.replace(/&(amp|[lg]t);/g,t=>{switch(t.charCodeAt(1)){case 97:return"&";case 108:return"<";case 103:return">"}})}function xu(e,t){return{start:e.start+t,end:e.end+t,value:e.value}}function wu(e,t,r){return e.slice(t,r)}function Eu(e){let{onHighlight:t,onText:r=wu}=typeof e=="function"?{onHighlight:e}:e;return(n,o,i=0,a=n.length)=>{var l;let s=[],c=(l=o==null?void 0:o.ranges)!=null?l:[];for(let u=0,p=i;ua)break;let m=c[u].end;if(mi&&s.push(r(n,i,d));let{value:h}=c[u];s.push(t(n,{start:d,end:i=m,value:h}))}return i{let o=n.data;switch(o.type){case 1:na.value=!0;break;case 3:typeof o.data.pagination.prev<"u"?it.value=He(H({},it.value),{pagination:o.data.pagination,items:[...it.value.items,...o.data.items]}):(it.value=o.data,so(0));break}},qt(()=>{lr.value&&r.postMessage({type:0,data:lr.value})}),qt(()=>{na.value&&r.postMessage({type:2,data:Se.value})})}var oa={container:"p",hidden:"m"};function ku(e){return z("div",{class:zt(oa.container,{[oa.hidden]:e.hidden}),onClick:()=>Dt()})}var ia={container:"r",disabled:"c"};function co(e){return z("button",{class:zt(ia.container,{[ia.disabled]:!e.onClick}),onClick:e.onClick,children:e.children})}var aa=e=>e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase(),Au=e=>e.replace(/^([A-Z])|[\s-_]+(\w)/g,(t,r,n)=>n?n.toUpperCase():r.toLowerCase()),sa=e=>{let t=Au(e);return t.charAt(0).toUpperCase()+t.slice(1)},Cu=(...e)=>e.filter((t,r,n)=>!!t&&t.trim()!==""&&n.indexOf(t)===r).join(" ").trim(),Hu={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"},$u=c=>{var l=c,{color:e="currentColor",size:t=24,strokeWidth:r=2,absoluteStrokeWidth:n,children:o,iconNode:i,class:a=""}=l,s=gr(l,["color","size","strokeWidth","absoluteStrokeWidth","children","iconNode","class"]);return Wt("svg",H(He(H({},Hu),{width:String(t),height:t,stroke:e,"stroke-width":n?Number(r)*24/Number(t):r,class:["lucide",a].join(" ")}),s),[...i.map(([u,p])=>Wt(u,p)),...Cr(o)])},bo=(e,t)=>{let r=a=>{var s=a,{class:n="",children:o}=s,i=gr(s,["class","children"]);return Wt($u,He(H({},i),{iconNode:t,class:Cu(`lucide-${aa(sa(e))}`,`lucide-${aa(e)}`,n)}),o)};return r.displayName=sa(e),r},Pu=bo("corner-down-left",[["path",{d:"M20 4v7a4 4 0 0 1-4 4H4",key:"6o5b7l"}],["path",{d:"m9 10-5 5 5 5",key:"1kshq7"}]]),Iu=bo("list-filter",[["path",{d:"M2 5h20",key:"1fs1ex"}],["path",{d:"M6 12h12",key:"8npq4p"}],["path",{d:"M9 19h6",key:"456am0"}]]),Ru=bo("search",[["path",{d:"m21 21-4.34-4.34",key:"14j7rj"}],["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}]]),Gx=hl(vl(),1);function ju({threshold:e=0,root:t=null,rootMargin:r="0%",freezeOnceVisible:n=!1,initialIsIntersecting:o=!1,onChange:i}={}){var a;let[s,c]=bn(null),[l,u]=bn(()=>({isIntersecting:o,entry:void 0})),p=Vt();p.current=i;let d=((a=l.entry)==null?void 0:a.isIntersecting)&&n;mt(()=>{if(!s||!("IntersectionObserver"in window)||d)return;let v,x=new IntersectionObserver(w=>{let E=Array.isArray(x.thresholds)?x.thresholds:[x.thresholds];w.forEach(_=>{let de=_.isIntersecting&&E.some(be=>_.intersectionRatio>=be);u({isIntersecting:de,entry:_}),p.current&&p.current(de,_),de&&n&&v&&(v(),v=void 0)})},{threshold:e,root:t,rootMargin:r});return x.observe(s),()=>{x.disconnect()}},[s,JSON.stringify(e),t,r,d,n]);let m=Vt(null);mt(()=>{var v;!s&&(v=l.entry)!=null&&v.target&&!n&&!d&&m.current!==l.entry.target&&(m.current=l.entry.target,u({isIntersecting:o,entry:void 0}))},[s,l.entry,n,d,o]);let h=[c,!!l.isIntersecting,l.entry];return h.ref=h[0],h.isIntersecting=h[1],h.entry=h[2],h}var lt={container:"n",hidden:"l",content:"u",pop:"d",badge:"y",sidebar:"i",controls:"w",results:"k",loadmore:"z"};function Fu(e){let{isIntersecting:t,ref:r}=ju({threshold:0});mt(()=>{t&&cu()},[t]);let n=Vt(null);mt(()=>{n.current&&typeof Se.value.page>"u"&&n.current.scrollTo({top:0,behavior:"smooth"})},[Se.value]);let o=za();return z("div",{class:zt(lt.container,{[lt.hidden]:e.hidden}),children:[z("div",{class:lt.content,children:[z("div",{class:lt.controls,children:[z(co,{onClick:Dt,children:z(Ru,{})}),z(Nu,{focus:!e.hidden}),z(co,{onClick:Wa,children:[z(Iu,{}),o.length>0&&z("span",{class:lt.badge,children:o.length})]})]}),z("div",{class:lt.results,ref:n,children:[z(Du,{keyboard:!e.hidden}),z("div",{class:lt.loadmore,ref:r})]})]}),z("div",{class:zt(lt.sidebar,{[lt.hidden]:du()}),children:z(Uu,{})})]})}var Tt={container:"X",list:"j",heading:"F",title:"I",item:"o",active:"g",value:"R",count:"q"};function Uu(e){let t=mu();return t.sort((r,n)=>n.node.count-r.node.count),z("div",{class:Tt.container,children:[z("h3",{class:Tt.heading,children:"Filters"}),z("h4",{class:Tt.title,children:"Tags"}),z("ol",{class:Tt.list,children:t.map(r=>z("li",{class:zt(Tt.item,{[Tt.active]:lu(r.node.value)}),onClick:()=>uu(r.node.value),children:[z("span",{class:Tt.value,children:r.node.value}),z("span",{class:Tt.count,children:r.node.count})]}))})]})}var ca={container:"f"};function Nu(e){let t=Vt(null);return mt(()=>{var r,n;e.focus?(r=t.current)==null||r.focus():(n=t.current)==null||n.blur()},[e.focus]),z("div",{class:ca.container,children:z("input",{ref:t,type:"text",class:ca.content,value:hn(wn()),onInput:r=>Va(yu(r.currentTarget.value)),autocapitalize:"off",autocomplete:"off",autocorrect:"off",placeholder:"Search",spellcheck:!1,role:"combobox"})})}var ut={container:"b",heading:"A",item:"a",active:"h",wrapper:"B",actions:"s",title:"x",path:"t"};function Ga(){let[e,t]=bn(!1);return mt(()=>{let r=()=>t(!0),n=()=>t(!1);return document.addEventListener("compositionstart",r),document.addEventListener("compositionend",n),()=>{document.removeEventListener("compositionstart",r),document.removeEventListener("compositionend",n)}},[]),e}function Du(e){var s;let t=su(),r=pu(),n=dn(),o=Vt([]),i=Ga();mt(()=>{let c=o.current[n];c&&c.scrollIntoView({block:"center",behavior:"smooth"})},[n]),Aa(e.keyboard,c=>{if(i)return;let l=dn();c.key==="ArrowDown"?(c.preventDefault(),so(Math.min(l+1,r.length-1))):c.key==="ArrowUp"&&(c.preventDefault(),so(Math.max(l-1,0)))},[e.keyboard,i]);let a=(s=fu())!=null?s:0;return z(ft,{children:[r.length>0&&z("h3",{class:ut.heading,children:[z("span",{class:ut.bubble,children:new Intl.NumberFormat("en-US").format(a)})," ","results"]}),z("ol",{class:ut.container,children:r.map((c,l)=>{var m;let u=Ba(t[c.id].title,c.matches.find(({field:h})=>h==="title")),p=Mu((m=t[c.id].path)!=null?m:[],c.matches.find(({field:h})=>h==="path")),d=t[c.id].location;if(qa()){let h=encodeURIComponent(wn()),[v,x]=d.split("#",2);d=`${v}?h=${h.replace(/%20/g,"+")}`,typeof x<"u"&&(d+=`#${x}`)}return z("li",{children:z("a",{ref:h=>{o.current[l]=h},href:d,onClick:()=>Dt(),class:zt(ut.item,{[ut.active]:l===dn()}),children:[z("div",{class:ut.wrapper,children:[z("h2",{class:ut.title,children:u}),z("menu",{class:ut.path,children:p.map(h=>z("li",{children:h}))})]}),z("nav",{class:ut.actions,children:z(co,{children:z(Pu,{})})})]})})})})]})}var Wu={container:"e"};function Vu(e){let t=Ga();return Aa(!0,r=>{var n,o,i,a,s;if(!t)if((r.metaKey||r.ctrlKey)&&r.key==="k")r.preventDefault(),Dt();else if((r.metaKey||r.ctrlKey)&&r.key==="j")document.body.classList.toggle("dark");else if(r.key==="Enter"&&!sr()){r.preventDefault();let c=dn(),l=(o=(n=it.value)==null?void 0:n.items[c])==null?void 0:o.id;if((a=(i=lr.value)==null?void 0:i.items[l])!=null&&a.location){Dt();let u=(s=lr.value)==null?void 0:s.items[l].location;if(qa()){let p=encodeURIComponent(wn()),[d,m]=u.split("#",2);u=`${d}?h=${p.replace(/%20/g,"+")}`,typeof m<"u"&&(u+=`#${m}`)}window.location.href=u}}else r.key==="Escape"&&!sr()&&(r.preventDefault(),Dt())},[t]),z("div",{class:Wu.container,children:[z(ku,{hidden:sr()}),z(Fu,{hidden:sr()})]})}function Ja(e,t){au(e),El(z(Vu,{}),t)}function go(){Dt()}function zu(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function qu(){return R(b(window,"compositionstart").pipe(f(()=>!0)),b(window,"compositionend").pipe(f(()=>!1))).pipe(J(!1))}function Xa(){let e=b(window,"keydown").pipe(f(t=>({mode:sr()?"global":"search",type:t.key,meta:t.ctrlKey||t.metaKey,claim(){t.preventDefault(),t.stopPropagation()}})),L(({mode:t,type:r})=>{if(t==="global"){let n=xt();if(typeof n!="undefined")return!zu(n,r)}return!0}),xe());return qu().pipe(g(t=>t?y:e))}function Ye(){return new URL(location.href)}function dt(e,t=!1){if(X("navigation.instant")&&!t){let r=A("a",{href:e.href});document.body.appendChild(r),r.click(),r.remove()}else location.href=e.href}function Za(){return new I}function Qa(){return location.hash.slice(1)}function es(e){let t=A("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function _o(e){return R(b(window,"hashchange"),e).pipe(f(Qa),J(Qa()),L(t=>t.length>0),se(1))}function ts(e){return _o(e).pipe(f(t=>Le(`[id="${t}"]`)),L(t=>typeof t!="undefined"))}function Ir(e){let t=matchMedia(e);return an(r=>t.addListener(()=>r(t.matches))).pipe(J(t.matches))}function rs(){let e=matchMedia("print");return R(b(window,"beforeprint").pipe(f(()=>!0)),b(window,"afterprint").pipe(f(()=>!1))).pipe(J(e.matches))}function yo(e,t){return e.pipe(g(r=>r?t():y))}function xo(e,t){return new U(r=>{let n=new XMLHttpRequest;return n.open("GET",`${e}`),n.responseType="blob",n.addEventListener("load",()=>{n.status>=200&&n.status<300?(r.next(n.response),r.complete()):r.error(new Error(n.statusText))}),n.addEventListener("error",()=>{r.error(new Error("Network error"))}),n.addEventListener("abort",()=>{r.complete()}),typeof(t==null?void 0:t.progress$)!="undefined"&&(n.addEventListener("progress",o=>{var i;if(o.lengthComputable)t.progress$.next(o.loaded/o.total*100);else{let a=(i=n.getResponseHeader("Content-Length"))!=null?i:0;t.progress$.next(o.loaded/+a*100)}}),t.progress$.next(5)),n.send(),()=>n.abort()})}function et(e,t){return xo(e,t).pipe(g(r=>r.text()),f(r=>JSON.parse(r)),se(1))}function En(e,t){let r=new DOMParser;return xo(e,t).pipe(g(n=>n.text()),f(n=>r.parseFromString(n,"text/html")),se(1))}function ns(e,t){let r=new DOMParser;return xo(e,t).pipe(g(n=>n.text()),f(n=>r.parseFromString(n,"text/xml")),se(1))}var wo={drawer:G("[data-md-toggle=drawer]"),search:G("[data-md-toggle=search]")};function Eo(e,t){wo[e].checked!==t&&wo[e].click()}function Tn(e){let t=wo[e];return b(t,"change").pipe(f(()=>t.checked),J(t.checked))}function os(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function is(){return R(b(window,"scroll",{passive:!0}),b(window,"resize",{passive:!0})).pipe(f(os),J(os()))}function as(){return{width:innerWidth,height:innerHeight}}function ss(){return b(window,"resize",{passive:!0}).pipe(f(as),J(as()))}function cs(){return re([is(),ss()]).pipe(f(([e,t])=>({offset:e,size:t})),se(1))}function Sn(e,{viewport$:t,header$:r}){let n=t.pipe(fe("size")),o=re([n,r]).pipe(f(()=>wt(e)));return re([r,t,o]).pipe(f(([{height:i},{offset:a,size:s},{x:c,y:l}])=>({offset:{x:a.x-c,y:a.y-l+i},size:s})))}var Ku=G("#__config"),mr=JSON.parse(Ku.textContent);mr.base=`${new URL(mr.base,Ye())}`;function Ue(){return mr}function X(e){return mr.features.includes(e)}function Bt(e,t){return typeof t!="undefined"?mr.translations[e].replace("#",t.toString()):mr.translations[e]}function ht(e,t=document){return G(`[data-md-component=${e}]`,t)}function Ee(e,t=document){return P(`[data-md-component=${e}]`,t)}function Bu(e){let t=G(".md-typeset > :first-child",e);return b(t,"click",{once:!0}).pipe(f(()=>G(".md-typeset",e)),f(r=>({hash:__md_hash(r.innerHTML)})))}function ls(e){if(!X("announce.dismiss")||!e.childElementCount)return y;if(!e.hidden){let t=G(".md-typeset",e);__md_hash(t.innerHTML)===__md_get("__announce")&&(e.hidden=!0)}return j(()=>{let t=new I;return t.subscribe(({hash:r})=>{e.hidden=!0,__md_set("__announce",r)}),Bu(e).pipe($(r=>t.next(r)),V(()=>t.complete()),f(r=>H({ref:e},r)))})}function Yu(e,{target$:t}){return t.pipe(f(r=>({hidden:r!==e})))}function us(e,t){let r=new I;return r.subscribe(({hidden:n})=>{e.hidden=n}),Yu(e,t).pipe($(n=>r.next(n)),V(()=>r.complete()),f(n=>H({ref:e},n)))}function To(e,t){return t==="inline"?A("div",{class:"md-tooltip md-tooltip--inline",id:e,role:"tooltip"},A("div",{class:"md-tooltip__inner md-typeset"})):A("div",{class:"md-tooltip",id:e,role:"tooltip"},A("div",{class:"md-tooltip__inner md-typeset"}))}function On(...e){return A("div",{class:"md-tooltip2",role:"dialog"},A("div",{class:"md-tooltip2__inner md-typeset"},e))}function ps(...e){return A("div",{class:"md-tooltip2",role:"tooltip"},A("div",{class:"md-tooltip2__inner md-typeset"},e))}function fs(e,t){if(t=t?`${t}_annotation_${e}`:void 0,t){let r=t?`#${t}`:void 0;return A("aside",{class:"md-annotation",tabIndex:0},To(t),A("a",{href:r,class:"md-annotation__index",tabIndex:-1},A("span",{"data-md-annotation-id":e})))}else return A("aside",{class:"md-annotation",tabIndex:0},To(t),A("span",{class:"md-annotation__index",tabIndex:-1},A("span",{"data-md-annotation-id":e})))}function ms(e){return A("button",{class:"md-code__button",title:Bt("clipboard.copy"),"data-clipboard-target":`#${e} > code`,"data-md-type":"copy"})}function ds(){return A("button",{class:"md-code__button",title:"Toggle line selection","data-md-type":"select"})}function hs(){return A("nav",{class:"md-code__nav"})}var Xu=_r(So());function bs(e){return A("ul",{class:"md-source__facts"},Object.entries(e).map(([t,r])=>A("li",{class:`md-source__fact md-source__fact--${t}`},typeof r=="number"?Li(r):r)))}function Oo(e){let t=`tabbed-control tabbed-control--${e}`;return A("div",{class:t,hidden:!0},A("button",{class:"tabbed-button",tabIndex:-1,"aria-hidden":"true"}))}function gs(e){return A("div",{class:"md-typeset__scrollwrap"},A("div",{class:"md-typeset__table"},e))}function Zu(e){var n;let t=Ue(),r=new URL(`../${e.version}/`,t.base);return A("li",{class:"md-version__item"},A("a",{href:`${r}`,class:"md-version__link"},e.title,((n=t.version)==null?void 0:n.alias)&&e.aliases.length>0&&A("span",{class:"md-version__alias"},e.aliases[0])))}function _s(e,t){var n;let r=Ue();return e=e.filter(o=>{var i;return!((i=o.properties)!=null&&i.hidden)}),A("div",{class:"md-version"},A("button",{class:"md-version__current","aria-label":Bt("select.version")},t.title,((n=r.version)==null?void 0:n.alias)&&t.aliases.length>0&&A("span",{class:"md-version__alias"},t.aliases[0])),A("ul",{class:"md-version__list"},e.map(Zu)))}var Qu=0;function ep(e,t=250){let r=re([ir(e),Ft(e,t)]).pipe(f(([o,i])=>o||i),ie()),n=j(()=>Ai(e)).pipe(oe(Ut),Lr(1),Ze(r),f(()=>Ci(e)));return r.pipe(Sr(o=>o),g(()=>re([r,n])),f(([o,i])=>({active:o,offset:i})),xe())}function Rr(e,t,r=250){let{content$:n,viewport$:o}=t,i=`__tooltip2_${Qu++}`;return j(()=>{let a=new I,s=new Un(!1);a.pipe(he(),ye(!1)).subscribe(s);let c=s.pipe(Tr(u=>Ve(+!u*250,Wn)),ie(),g(u=>u?n:y),$(u=>u.id=i),xe());re([a.pipe(f(({active:u})=>u)),c.pipe(g(u=>Ft(u,250)),J(!1))]).pipe(f(u=>u.some(p=>p))).subscribe(s);let l=s.pipe(L(u=>u),pe(c,o),f(([u,p,{size:d}])=>{let m=e.getBoundingClientRect(),h=m.width/2;if(p.role==="tooltip")return{x:h,y:8+m.height};if(m.y>=d.height/2){let{height:v}=Ae(p);return{x:h,y:-16-v}}else return{x:h,y:16+m.height}}));return re([c,a,l]).subscribe(([u,{offset:p},d])=>{u.style.setProperty("--md-tooltip-host-x",`${p.x}px`),u.style.setProperty("--md-tooltip-host-y",`${p.y}px`),u.style.setProperty("--md-tooltip-x",`${d.x}px`),u.style.setProperty("--md-tooltip-y",`${d.y}px`),u.classList.toggle("md-tooltip2--top",d.y<0),u.classList.toggle("md-tooltip2--bottom",d.y>=0)}),s.pipe(L(u=>u),pe(c,(u,p)=>p),L(u=>u.role==="tooltip")).subscribe(u=>{let p=Ae(G(":scope > *",u));u.style.setProperty("--md-tooltip-width",`${p.width}px`),u.style.setProperty("--md-tooltip-tail","0px")}),s.pipe(ie(),Ie(je),pe(c)).subscribe(([u,p])=>{p.classList.toggle("md-tooltip2--active",u)}),re([s.pipe(L(u=>u)),c]).subscribe(([u,p])=>{p.role==="dialog"?(e.setAttribute("aria-controls",i),e.setAttribute("aria-haspopup","dialog")):e.setAttribute("aria-describedby",i)}),s.pipe(L(u=>!u)).subscribe(()=>{e.removeAttribute("aria-controls"),e.removeAttribute("aria-describedby"),e.removeAttribute("aria-haspopup")}),ep(e,r).pipe($(u=>a.next(u)),V(()=>a.complete()),f(u=>H({ref:e},u)))})}function Ge(e,{viewport$:t},r=document.body){return Rr(e,{content$:new U(n=>{let o=e.title,i=ps(o);return n.next(i),e.removeAttribute("title"),r.append(i),()=>{i.remove(),e.setAttribute("title",o)}}),viewport$:t},0)}function tp(e,t){let r=j(()=>re([Hi(e),Ut(t)])).pipe(f(([{x:n,y:o},i])=>{let{width:a,height:s}=Ae(e);return{x:n-i.x+a/2,y:o-i.y+s/2}}));return ir(e).pipe(g(n=>r.pipe(f(o=>({active:n,offset:o})),Me(+!n||1/0))))}function ys(e,t,{target$:r}){let[n,o]=Array.from(e.children);return j(()=>{let i=new I,a=i.pipe(he(),ye(!0));return i.subscribe({next({offset:s}){e.style.setProperty("--md-tooltip-x",`${s.x}px`),e.style.setProperty("--md-tooltip-y",`${s.y}px`)},complete(){e.style.removeProperty("--md-tooltip-x"),e.style.removeProperty("--md-tooltip-y")}}),Et(e).pipe(Q(a)).subscribe(s=>{e.toggleAttribute("data-md-visible",s)}),R(i.pipe(L(({active:s})=>s)),i.pipe(Be(250),L(({active:s})=>!s))).subscribe({next({active:s}){s?e.prepend(n):n.remove()},complete(){e.prepend(n)}}),i.pipe(Xe(16,je)).subscribe(({active:s})=>{n.classList.toggle("md-tooltip--active",s)}),i.pipe(Lr(125,je),L(()=>!!e.offsetParent),f(()=>e.offsetParent.getBoundingClientRect()),f(({x:s})=>s)).subscribe({next(s){s?e.style.setProperty("--md-tooltip-0",`${-s}px`):e.style.removeProperty("--md-tooltip-0")},complete(){e.style.removeProperty("--md-tooltip-0")}}),b(o,"click").pipe(Q(a),L(s=>!(s.metaKey||s.ctrlKey))).subscribe(s=>{s.stopPropagation(),s.preventDefault()}),b(o,"mousedown").pipe(Q(a),pe(i)).subscribe(([s,{active:c}])=>{var l;if(s.button!==0||s.metaKey||s.ctrlKey)s.preventDefault();else if(c){s.preventDefault();let u=e.parentElement.closest(".md-annotation");u instanceof HTMLElement?u.focus():(l=xt())==null||l.blur()}}),r.pipe(Q(a),L(s=>s===n),It(125)).subscribe(()=>e.focus()),tp(e,t).pipe($(s=>i.next(s)),V(()=>i.complete()),f(s=>H({ref:e},s)))})}function rp(e){let t=Ue();if(e.tagName!=="CODE")return[e];let r=[".c",".c1",".cm"];if(t.annotate){let n=e.closest("[class|=language]");if(n)for(let o of Array.from(n.classList)){if(!o.startsWith("language-"))continue;let[,i]=o.split("-");i in t.annotate&&r.push(...t.annotate[i])}}return P(r.join(", "),e)}function np(e){let t=[];for(let r of rp(e)){let n=[],o=document.createNodeIterator(r,NodeFilter.SHOW_TEXT);for(let i=o.nextNode();i;i=o.nextNode())n.push(i);for(let i of n){let a;for(;a=/(\(\d+\))(!)?/.exec(i.textContent);){let[,s,c]=a;if(typeof c=="undefined"){let l=i.splitText(a.index);i=l.splitText(s.length),t.push(l)}else{i.textContent=s,t.push(i);break}}}}return t}function xs(e,t){t.append(...Array.from(e.childNodes))}function Ln(e,t,{target$:r,print$:n}){let o=t.closest("[id]"),i=o==null?void 0:o.id,a=new Map;for(let s of np(t)){let[,c]=s.textContent.match(/\((\d+)\)/);Le(`:scope > li:nth-child(${c})`,e)&&(a.set(c,fs(c,i)),s.replaceWith(a.get(c)))}return a.size===0?y:j(()=>{let s=new I,c=s.pipe(he(),ye(!0)),l=[];for(let[u,p]of a)l.push([G(".md-typeset",p),G(`:scope > li:nth-child(${u})`,e)]);return n.pipe(Q(c)).subscribe(u=>{e.hidden=!u,e.classList.toggle("md-annotation-list",u);for(let[p,d]of l)u?xs(p,d):xs(d,p)}),R(...[...a].map(([,u])=>ys(u,t,{target$:r}))).pipe(V(()=>s.complete()),xe())})}function ws(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return ws(t)}}function Es(e,t){return j(()=>{let r=ws(e);return typeof r!="undefined"?Ln(r,e,t):y})}var Ss=_r(Mo());var op=0,Ts=R(b(window,"keydown").pipe(f(()=>!0)),R(b(window,"keyup"),b(window,"contextmenu")).pipe(f(()=>!1))).pipe(J(!1),se(1));function Os(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return Os(t)}}function ip(e){return Re(e).pipe(f(({width:t})=>({scrollable:Mr(e).width>t})),fe("scrollable"))}function Ls(e,t){let{matches:r}=matchMedia("(hover)"),n=j(()=>{let o=new I,i=o.pipe(Gn(1));o.subscribe(({scrollable:m})=>{m&&r?e.setAttribute("tabindex","0"):e.removeAttribute("tabindex")});let a=[],s=e.closest("pre"),c=s.closest("[id]"),l=c?c.id:op++;s.id=`__code_${l}`;let u=[],p=e.closest(".highlight");if(p instanceof HTMLElement){let m=Os(p);if(typeof m!="undefined"&&(p.classList.contains("annotate")||X("content.code.annotate"))){let h=Ln(m,e,t);u.push(Re(p).pipe(Q(i),f(({width:v,height:x})=>v&&x),ie(),g(v=>v?h:y)))}}let d=P(":scope > span[id]",e);if(d.length&&(e.classList.add("md-code__content"),e.closest(".select")||X("content.code.select")&&!e.closest(".no-select"))){let m=+d[0].id.split("-").pop(),h=ds();a.push(h),X("content.tooltips")&&u.push(Ge(h,{viewport$}));let v=b(h,"click").pipe(Or(M=>!M,!1),$(()=>h.blur()),xe());v.subscribe(M=>{h.classList.toggle("md-code__button--active",M)});let x=me(d).pipe(oe(M=>Ft(M).pipe(f(O=>[M,O]))));v.pipe(g(M=>M?x:y)).subscribe(([M,O])=>{let N=Le(".hll.select",M);if(N&&!O)N.replaceWith(...Array.from(N.childNodes));else if(!N&&O){let ee=document.createElement("span");ee.className="hll select",ee.append(...Array.from(M.childNodes).slice(1)),M.append(ee)}});let w=me(d).pipe(oe(M=>b(M,"mousedown").pipe($(O=>O.preventDefault()),f(()=>M)))),E=v.pipe(g(M=>M?w:y),pe(Ts),f(([M,O])=>{var ee;let N=d.indexOf(M)+m;if(O===!1)return[N,N];{let le=P(".hll",e).map(ce=>d.indexOf(ce.parentElement)+m);return(ee=window.getSelection())==null||ee.removeAllRanges(),[Math.min(N,...le),Math.max(N,...le)]}})),_=_o(y).pipe(L(M=>M.startsWith(`__codelineno-${l}-`)));_.subscribe(M=>{let[,,O]=M.split("-"),N=O.split(":").map(le=>+le-m+1);N.length===1&&N.push(N[0]);for(let le of P(".hll:not(.select)",e))le.replaceWith(...Array.from(le.childNodes));let ee=d.slice(N[0]-1,N[1]);for(let le of ee){let ce=document.createElement("span");ce.className="hll",ce.append(...Array.from(le.childNodes).slice(1)),le.append(ce)}}),_.pipe(Me(1),Ie(ge)).subscribe(M=>{if(M.includes(":")){let O=document.getElementById(M.split(":")[0]);O&&setTimeout(()=>{let N=O,ee=-64;for(;N!==document.body;)ee+=N.offsetTop,N=N.offsetParent;window.scrollTo({top:ee})},1)}});let be=me(P('a[href^="#__codelineno"]',p)).pipe(oe(M=>b(M,"click").pipe($(O=>O.preventDefault()),f(()=>M)))).pipe(Q(i),pe(Ts),f(([M,O])=>{let ee=+G(`[id="${M.hash.slice(1)}"]`).parentElement.id.split("-").pop();if(O===!1)return[ee,ee];{let le=P(".hll",e).map(ce=>+ce.parentElement.id.split("-").pop());return[Math.min(ee,...le),Math.max(ee,...le)]}}));R(E,be).subscribe(M=>{let O=`#__codelineno-${l}-`;M[0]===M[1]?O+=M[0]:O+=`${M[0]}:${M[1]}`,history.replaceState({},"",O),window.dispatchEvent(new HashChangeEvent("hashchange",{newURL:window.location.origin+window.location.pathname+O,oldURL:window.location.href}))})}if(Ss.default.isSupported()&&(e.closest(".copy")||X("content.code.copy")&&!e.closest(".no-copy"))){let m=ms(s.id);a.push(m),X("content.tooltips")&&u.push(Ge(m,{viewport$}))}if(a.length){let m=hs();m.append(...a),s.insertBefore(m,e)}return ip(e).pipe($(m=>o.next(m)),V(()=>o.complete()),f(m=>H({ref:e},m)),Rt(R(...u).pipe(Q(i))))});return X("content.lazy")?Et(e).pipe(L(o=>o),Me(1),g(()=>n)):n}function ap(e,{target$:t,print$:r}){let n=!0;return R(t.pipe(f(o=>o.closest("details:not([open])")),L(o=>e===o),f(()=>({action:"open",reveal:!0}))),r.pipe(L(o=>o||!n),$(()=>n=e.open),f(o=>({action:o?"open":"close"}))))}function Ms(e,t){return j(()=>{let r=new I;return r.subscribe(({action:n,reveal:o})=>{e.toggleAttribute("open",n==="open"),o&&e.scrollIntoView()}),ap(e,t).pipe($(n=>r.next(n)),V(()=>r.complete()),f(n=>H({ref:e},n)))})}var ks=0,As=new Map;function sp(e){let t=document.createElement("h3");t.innerHTML=e.innerHTML;let r=[t],n=e.nextElementSibling;for(;n&&!(n instanceof HTMLHeadingElement);)r.push(n.cloneNode(!0)),n=n.nextElementSibling;return r}function cp(e,t){for(let r of P("[href], [src]",e))for(let n of["href","src"]){let o=r.getAttribute(n);if(o&&!/^(?:[a-z]+:)?\/\//i.test(o)){r[n]=new URL(r.getAttribute(n),t).toString();break}}for(let r of P("[name^=__], [for]",e))for(let n of["id","for","name"]){let o=r.getAttribute(n);o&&r.setAttribute(n,`${o}$preview_${ks}`)}return ks++,Y(e)}function lp(e){let t=As.get(e.toString());return t?Y(t):En(e).pipe(g(r=>cp(r,e)),f(r=>(As.set(e.toString(),r),r)))}function Cs(e,t){let{sitemap$:r}=t;if(!(e instanceof HTMLAnchorElement))return y;if(!(X("navigation.instant.preview")||e.hasAttribute("data-preview")))return y;e.removeAttribute("title");let n=re([ir(e),Ft(e).pipe(ke(1))]).pipe(f(([i,a])=>i||a),ie(),L(i=>i));return $t([r,n]).pipe(g(([i])=>{let a=new URL(e.href);return a.search=a.hash="",i.has(`${a}`)?Y(a):y}),g(i=>lp(i)),g(i=>{let a=e.hash?`article [id="${decodeURIComponent(e.hash.slice(1))}"]`:"article h1",s=Le(a,i);return typeof s=="undefined"?y:Y(sp(s))})).pipe(g(i=>{let a=new U(s=>{let c=On(...i);return s.next(c),document.body.append(c),()=>c.remove()});return Rr(e,H({content$:a},t))}))}var Hs=".node circle,.node ellipse,.node path,.node polygon,.node rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}marker{fill:var(--md-mermaid-edge-color)!important}.edgeLabel .label rect{fill:#0000}.flowchartTitleText{fill:var(--md-mermaid-label-fg-color)}.label{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.label foreignObject{line-height:normal;overflow:visible}.label div .edgeLabel{color:var(--md-mermaid-label-fg-color)}.edgeLabel,.edgeLabel p,.label div .edgeLabel{background-color:var(--md-mermaid-label-bg-color)}.edgeLabel,.edgeLabel p{fill:var(--md-mermaid-label-bg-color);color:var(--md-mermaid-edge-color)}.edgePath .path,.flowchart-link{stroke:var(--md-mermaid-edge-color)}.edgePath .arrowheadPath{fill:var(--md-mermaid-edge-color);stroke:none}.cluster rect{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}.cluster span{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}g #flowchart-circleEnd,g #flowchart-circleStart,g #flowchart-crossEnd,g #flowchart-crossStart,g #flowchart-pointEnd,g #flowchart-pointStart{stroke:none}.classDiagramTitleText{fill:var(--md-mermaid-label-fg-color)}g.classGroup line,g.classGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.classGroup text{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.classLabel .box{fill:var(--md-mermaid-label-bg-color);background-color:var(--md-mermaid-label-bg-color);opacity:1}.classLabel .label{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.node .divider{stroke:var(--md-mermaid-node-fg-color)}.relation{stroke:var(--md-mermaid-edge-color)}.cardinality{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.cardinality text{fill:inherit!important}defs marker.marker.composition.class path,defs marker.marker.dependency.class path,defs marker.marker.extension.class path{fill:var(--md-mermaid-edge-color)!important;stroke:var(--md-mermaid-edge-color)!important}defs marker.marker.aggregation.class path{fill:var(--md-mermaid-label-bg-color)!important;stroke:var(--md-mermaid-edge-color)!important}.statediagramTitleText{fill:var(--md-mermaid-label-fg-color)}g.stateGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.stateGroup .state-title{fill:var(--md-mermaid-label-fg-color)!important;font-family:var(--md-mermaid-font-family)}g.stateGroup .composit{fill:var(--md-mermaid-label-bg-color)}.nodeLabel,.nodeLabel p{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}a .nodeLabel{text-decoration:underline}.node circle.state-end,.node circle.state-start,.start-state{fill:var(--md-mermaid-edge-color);stroke:none}.end-state-inner,.end-state-outer{fill:var(--md-mermaid-edge-color)}.end-state-inner,.node circle.state-end{stroke:var(--md-mermaid-label-bg-color)}.transition{stroke:var(--md-mermaid-edge-color)}[id^=state-fork] rect,[id^=state-join] rect{fill:var(--md-mermaid-edge-color)!important;stroke:none!important}.statediagram-cluster.statediagram-cluster .inner{fill:var(--md-default-bg-color)}.statediagram-cluster rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.statediagram-state rect.divider{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}defs #statediagram-barbEnd{stroke:var(--md-mermaid-edge-color)}[id^=entity] path,[id^=entity] rect{fill:var(--md-default-bg-color)}.relationshipLine{stroke:var(--md-mermaid-edge-color)}defs .marker.oneOrMore.er *,defs .marker.onlyOne.er *,defs .marker.zeroOrMore.er *,defs .marker.zeroOrOne.er *{stroke:var(--md-mermaid-edge-color)!important}text:not([class]):last-child{fill:var(--md-mermaid-label-fg-color)}.actor{fill:var(--md-mermaid-sequence-actor-bg-color);stroke:var(--md-mermaid-sequence-actor-border-color)}text.actor>tspan{fill:var(--md-mermaid-sequence-actor-fg-color);font-family:var(--md-mermaid-font-family)}line{stroke:var(--md-mermaid-sequence-actor-line-color)}.actor-man circle,.actor-man line{fill:var(--md-mermaid-sequence-actorman-bg-color);stroke:var(--md-mermaid-sequence-actorman-line-color)}.messageLine0,.messageLine1{stroke:var(--md-mermaid-sequence-message-line-color)}.note{fill:var(--md-mermaid-sequence-note-bg-color);stroke:var(--md-mermaid-sequence-note-border-color)}.loopText,.loopText>tspan,.messageText,.noteText>tspan{stroke:none;font-family:var(--md-mermaid-font-family)!important}.messageText{fill:var(--md-mermaid-sequence-message-fg-color)}.loopText,.loopText>tspan{fill:var(--md-mermaid-sequence-loop-fg-color)}.noteText>tspan{fill:var(--md-mermaid-sequence-note-fg-color)}#arrowhead path{fill:var(--md-mermaid-sequence-message-line-color);stroke:none}.loopLine{fill:var(--md-mermaid-sequence-loop-bg-color);stroke:var(--md-mermaid-sequence-loop-border-color)}.labelBox{fill:var(--md-mermaid-sequence-label-bg-color);stroke:none}.labelText,.labelText>span{fill:var(--md-mermaid-sequence-label-fg-color);font-family:var(--md-mermaid-font-family)}.sequenceNumber{fill:var(--md-mermaid-sequence-number-fg-color)}rect.rect{fill:var(--md-mermaid-sequence-box-bg-color);stroke:none}rect.rect+text.text{fill:var(--md-mermaid-sequence-box-fg-color)}defs #sequencenumber{fill:var(--md-mermaid-sequence-number-bg-color)!important}";var ko,pp=0;function fp(){return typeof mermaid=="undefined"||mermaid instanceof Element?ar("https://unpkg.com/mermaid@11/dist/mermaid.min.js"):Y(void 0)}function $s(e){return e.classList.remove("mermaid"),ko||(ko=fp().pipe($(()=>mermaid.initialize({startOnLoad:!1,themeCSS:Hs,sequence:{actorFontSize:"16px",messageFontSize:"16px",noteFontSize:"16px"}})),f(()=>{}),se(1))),ko.subscribe(()=>Uo(null,null,function*(){e.classList.add("mermaid");let t=`__mermaid_${pp++}`,r=A("div",{class:"mermaid"}),n=e.textContent,{svg:o,fn:i}=yield mermaid.render(t,n),a=r.attachShadow({mode:"closed"});a.innerHTML=o,e.replaceWith(r),i==null||i(a)})),ko.pipe(f(()=>({ref:e})))}var Ps=A("table");function Is(e){return e.replaceWith(Ps),Ps.replaceWith(gs(e)),Y({ref:e})}function mp(e){let t=e.find(r=>r.checked)||e[0];return R(...e.map(r=>b(r,"change").pipe(f(()=>G(`label[for="${r.id}"]`))))).pipe(J(G(`label[for="${t.id}"]`)),f(r=>({active:r})))}function Rs(e,{viewport$:t,target$:r}){let n=G(".tabbed-labels",e),o=P(":scope > input",e),i=Oo("prev");e.append(i);let a=Oo("next");return e.append(a),j(()=>{let s=new I,c=s.pipe(he(),ye(!0));re([s,Re(e),Et(e)]).pipe(Q(c),Xe(1,je)).subscribe({next([{active:l},u]){let p=wt(l),{width:d}=Ae(l);e.style.setProperty("--md-indicator-x",`${p.x}px`),e.style.setProperty("--md-indicator-width",`${d}px`);let m=ln(n);(p.xm.x+u.width)&&n.scrollTo({left:Math.max(0,p.x-16),behavior:"smooth"})},complete(){e.style.removeProperty("--md-indicator-x"),e.style.removeProperty("--md-indicator-width")}}),re([Ut(n),Re(n)]).pipe(Q(c)).subscribe(([l,u])=>{let p=Mr(n);i.hidden=l.x<16,a.hidden=l.x>p.width-u.width-16}),R(b(i,"click").pipe(f(()=>-1)),b(a,"click").pipe(f(()=>1))).pipe(Q(c)).subscribe(l=>{let{width:u}=Ae(n);n.scrollBy({left:u*l,behavior:"smooth"})}),r.pipe(Q(c),L(l=>o.includes(l))).subscribe(l=>l.click()),n.classList.add("tabbed-labels--linked");for(let l of o){let u=G(`label[for="${l.id}"]`);u.replaceChildren(A("a",{href:`#${u.htmlFor}`,tabIndex:-1},...Array.from(u.childNodes))),b(u.firstElementChild,"click").pipe(Q(c),L(p=>!(p.metaKey||p.ctrlKey)),$(p=>{p.preventDefault(),p.stopPropagation()})).subscribe(()=>{history.replaceState({},"",`#${u.htmlFor}`),u.click()})}return X("content.tabs.link")&&s.pipe(ke(1),pe(t)).subscribe(([{active:l},{offset:u}])=>{let p=l.innerText.trim();if(l.hasAttribute("data-md-switching"))l.removeAttribute("data-md-switching");else{let d=e.offsetTop-u.y;for(let h of P("[data-tabs]"))for(let v of P(":scope > input",h)){let x=G(`label[for="${v.id}"]`);if(x!==l&&x.innerText.trim()===p){x.setAttribute("data-md-switching",""),v.click();break}}window.scrollTo({top:e.offsetTop-d});let m=__md_get("__tabs")||[];__md_set("__tabs",[...new Set([p,...m])])}}),s.pipe(Q(c)).subscribe(()=>{for(let l of P("audio, video",e))l.offsetWidth&&l.autoplay?l.play().catch(()=>{}):l.pause()}),mp(o).pipe($(l=>s.next(l)),V(()=>s.complete()),f(l=>H({ref:e},l)))}).pipe(Ht(ge))}function js(e,t){let{viewport$:r,target$:n,print$:o}=t;return R(...P(".annotate:not(.highlight)",e).map(i=>Es(i,{target$:n,print$:o})),...P("pre:not(.mermaid) > code",e).map(i=>Ls(i,{target$:n,print$:o})),...P("a",e).map(i=>Cs(i,t)),...P("pre.mermaid",e).map(i=>$s(i)),...P("table:not([class])",e).map(i=>Is(i)),...P("details",e).map(i=>Ms(i,{target$:n,print$:o})),...P("[data-tabs]",e).map(i=>Rs(i,{viewport$:r,target$:n})),...P("[title]:not([data-preview])",e).filter(()=>X("content.tooltips")).map(i=>Ge(i,{viewport$:r})),...P(".footnote-ref",e).filter(()=>X("content.footnote.tooltips")).map(i=>Rr(i,{content$:new U(a=>{let s=new URL(i.href).hash.slice(1),c=Array.from(document.getElementById(s).cloneNode(!0).children),l=On(...c);return a.next(l),document.body.append(l),()=>l.remove()}),viewport$:r})))}function dp(e,{alert$:t}){return t.pipe(g(r=>R(Y(!0),Y(!1).pipe(It(2e3))).pipe(f(n=>({message:r,active:n})))))}function Fs(e,t){let r=G(".md-typeset",e);return j(()=>{let n=new I;return n.subscribe(({message:o,active:i})=>{e.classList.toggle("md-dialog--active",i),r.textContent=o}),dp(e,t).pipe($(o=>n.next(o)),V(()=>n.complete()),f(o=>H({ref:e},o)))})}function hp({viewport$:e}){if(!X("header.autohide"))return Y(!1);let t=e.pipe(f(({offset:{y:o}})=>o),Pt(2,1),f(([o,i])=>[oMath.abs(i-o.y)>100),f(([,[o]])=>o),ie()),n=Tn("search");return re([e,n]).pipe(f(([{offset:o},i])=>o.y>400&&!i),ie(),g(o=>o?r:Y(!1)),J(!1))}function Us(e,t){return j(()=>re([Re(e),hp(t)])).pipe(f(([{height:r},n])=>({height:r,hidden:n})),ie((r,n)=>r.height===n.height&&r.hidden===n.hidden),se(1))}function Ns(e,{viewport$:t,header$:r,main$:n}){return j(()=>{let o=new I,i=o.pipe(he(),ye(!0));o.pipe(fe("active"),Ze(r)).subscribe(([{active:s},{hidden:c}])=>{e.classList.toggle("md-header--shadow",s&&!c),e.hidden=c});let a=me(P("[title]",e)).pipe(L(()=>X("content.tooltips")),oe(s=>Ge(s,{viewport$:t})));return n.subscribe(o),r.pipe(Q(i),f(s=>H({ref:e},s)),Rt(a.pipe(Q(i))))})}function vp(e,{viewport$:t,header$:r}){return Sn(e,{viewport$:t,header$:r}).pipe(f(({offset:{y:n}})=>{let{height:o}=Ae(e);return{active:o>0&&n>=o}}),fe("active"))}function Ds(e,t){return j(()=>{let r=new I;r.subscribe({next({active:o}){e.classList.toggle("md-header__title--active",o)},complete(){e.classList.remove("md-header__title--active")}});let n=Le(".md-content h1");return typeof n=="undefined"?y:vp(n,t).pipe($(o=>r.next(o)),V(()=>r.complete()),f(o=>H({ref:e},o)))})}function Ws(e,{viewport$:t,header$:r}){let n=r.pipe(f(({height:i})=>i),ie()),o=n.pipe(g(()=>Re(e).pipe(f(({height:i})=>({top:e.offsetTop,bottom:e.offsetTop+i})),fe("bottom"))));return re([n,o,t]).pipe(f(([i,{top:a,bottom:s},{offset:{y:c},size:{height:l}}])=>(l=Math.max(0,l-Math.max(0,a-c,i)-Math.max(0,l+c-s)),{offset:a-i,height:l,active:a-i<=c})),ie((i,a)=>i.offset===a.offset&&i.height===a.height&&i.active===a.active))}function bp(e){let t=__md_get("__palette")||{index:e.findIndex(n=>matchMedia(n.getAttribute("data-md-color-media")).matches)},r=Math.max(0,Math.min(t.index,e.length-1));return Y(...e).pipe(oe(n=>b(n,"change").pipe(f(()=>n))),J(e[r]),f(n=>({index:e.indexOf(n),color:{media:n.getAttribute("data-md-color-media"),scheme:n.getAttribute("data-md-color-scheme"),primary:n.getAttribute("data-md-color-primary"),accent:n.getAttribute("data-md-color-accent")}})),se(1))}function Vs(e){let t=P("input",e),r=A("meta",{name:"theme-color"});document.head.appendChild(r);let n=A("meta",{name:"color-scheme"});document.head.appendChild(n);let o=Ir("(prefers-color-scheme: light)");return j(()=>{let i=new I;return i.subscribe(a=>{if(document.body.setAttribute("data-md-color-switching",""),a.color.media==="(prefers-color-scheme)"){let s=matchMedia("(prefers-color-scheme: light)"),c=document.querySelector(s.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");a.color.scheme=c.getAttribute("data-md-color-scheme"),a.color.primary=c.getAttribute("data-md-color-primary"),a.color.accent=c.getAttribute("data-md-color-accent")}for(let[s,c]of Object.entries(a.color))document.body.setAttribute(`data-md-color-${s}`,c);for(let s=0;sa.key==="Enter"),pe(i,(a,s)=>s)).subscribe(({index:a})=>{a=(a+1)%t.length,t[a].click(),t[a].focus()}),i.pipe(f(()=>{let a=ht("header"),s=window.getComputedStyle(a);return n.content=s.colorScheme,s.backgroundColor.match(/\d+/g).map(c=>(+c).toString(16).padStart(2,"0")).join("")})).subscribe(a=>r.content=`#${a}`),i.pipe(Ie(ge)).subscribe(()=>{document.body.removeAttribute("data-md-color-switching")}),bp(t).pipe(Q(o.pipe(ke(1))),jt(),$(a=>i.next(a)),V(()=>i.complete()),f(a=>H({ref:e},a)))})}function zs(e,{progress$:t}){return j(()=>{let r=new I;return r.subscribe(({value:n})=>{e.style.setProperty("--md-progress-value",`${n}`)}),t.pipe($(n=>r.next({value:n})),V(()=>r.complete()),f(n=>({ref:e,value:n})))})}var qs='.v u{text-decoration:underline!important;text-decoration-style:wavy!important;text-decoration-thickness:1px!important}.p{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);background-color:rgba(var(--color-backdrop)/var(--alpha-lighter));cursor:pointer;height:100%;pointer-events:auto;position:absolute;transition:opacity .25s;width:100%}.p.m{opacity:0;pointer-events:none;transition:opacity .35s}.r{align-items:center;background-color:initial;border:none;border-radius:var(--space-2);cursor:pointer;display:flex;flex-shrink:0;font-family:var(--font-family);height:36px;justify-content:center;outline:none;padding:0;position:relative;transition:background-color .25s,color .25s;width:36px;z-index:1}.r svg{stroke:rgb(var(--color-foreground));height:18px;opacity:.5;width:18px}.r:before{background-color:rgb(var(--color-background-subtle));border-radius:var(--border-radius-2);content:"";inset:0;opacity:0;position:absolute;transform:scale(.75);transition:transform 125ms,opacity 125ms;z-index:0}.r:hover:before{opacity:1;transform:scale(1)}.r.c{cursor:auto}.r.c:before{display:none}.n{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);background-color:rgba(var(--color-background)/var(--alpha-light));border-radius:var(--space-3);box-shadow:0 0 60px #0000000d;display:flex;height:480px;overflow:hidden;pointer-events:auto;position:absolute;transition:transform .25s cubic-bezier(.16,1,.3,1),opacity .25s;width:640px}.n.l{opacity:0;pointer-events:none;transform:scale(1.1);transition:transform .25s .15s,opacity .15s}@media (max-width:680px){.n{border-radius:0;height:100%;width:100%}}.u{display:flex;flex-basis:min-content;flex-direction:column;flex-grow:1;flex-shrink:0}@keyframes d{0%{transform:scale(0)}50%{transform:scale(1.2)}to{transform:scale(1)}}.y{animation:d .25s ease-in-out;background:var(--color-highlight);border-radius:100%;color:#fff;font-size:8px;font-weight:700;height:12px;padding-top:1px;position:absolute;right:4px;top:4px;width:12px}.i{background-color:rgb(var(--color-background-subtle)/var(--alpha-lighter));flex-shrink:0;overflow:scroll;position:relative;transition:width .35s cubic-bezier(.16,1,.3,1),opacity .25s;width:200px}.i>*{transform:translate(0);transition:transform .25s cubic-bezier(.16,1,.3,1)}.i.l{opacity:0;width:0}.i.l>*{transform:translate(-48px)}@media (max-width:680px){.i{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);background-color:rgba(var(--color-background-subtle)/var(--alpha-light));box-shadow:0 0 60px #00000026;height:100%;position:absolute;right:0;top:0}}.w{border-bottom:1px solid rgb(var(--color-foreground)/var(--alpha-lightest));display:flex;gap:var(--space-1);padding:var(--space-2)}.k{-webkit-overflow-scrolling:touch;overflow:auto;overscroll-behavior:contain}.z{padding:8px 10px}.X{color:rgb(var(--color-foreground)/var(--alpha-light));padding:var(--space-2);position:absolute;width:200px}.X,.j{display:flex;flex-direction:column}.j{gap:2px;list-style:none;padding:0}.F,.j{margin:0}.F{font-size:16px;font-weight:400}.F,.I{padding:8px}.I{font-size:14px;margin:4px 0 0;opacity:.5}.I,.o{font-size:12px}.o{cursor:pointer;display:flex;padding:4px 8px;position:relative}.o:before{background-color:var(--color-highlight-transparent);border-radius:var(--space-1);content:"";inset:0;opacity:0;position:absolute;transform:scale(.75);transition:transform 125ms,opacity 125ms;z-index:0}.o.g:before,.o:hover:before{opacity:1;transform:scale(1)}.o.g,.o:hover{color:var(--color-highlight)}.R{flex-grow:1}.R,.q{position:relative}.q{font-weight:700}.f{flex-grow:1}.f input{background:#0000;border:none;color:rgb(var(--color-foreground));font-family:var(--font-family);font-size:16px;height:100%;letter-spacing:-.25px;outline:none;width:100%}.b{color:rgb(var(--color-foreground)/var(--alpha-light));display:flex;flex-direction:column;gap:2px;line-height:1.3;list-style:none;margin:var(--space-2);margin-top:0;padding:0}.A,.b li{margin:0}.A{color:rgb(var(--color-foreground)/var(--alpha-lighter));font-size:12px;margin-top:var(--space-2);padding:0 18px}.a{border-radius:var(--space-2);color:inherit;cursor:pointer;display:flex;flex-direction:row;flex-grow:1;padding:8px 10px;position:relative;text-decoration:none}.a:before{background-color:rgb(var(--color-background-subtle));border-radius:var(--border-radius-2);content:"";display:block;inset:0;opacity:0;position:absolute;transform:scale(.9);transition:transform 125ms,opacity 125ms;z-index:0}@media (pointer:fine){.a.h:before,.a:hover:before{opacity:1;transform:scale(1)}}.a mark{background:#0000;color:var(--color-highlight)}.a u{background-color:var(--color-highlight-transparent);border-radius:2px;box-shadow:0 0 0 1px var(--color-highlight-transparent);text-decoration:none}.B{flex-grow:1}.s{margin-right:-8px;opacity:0;position:relative;transform:translate(-2px);transition:transform 125ms,opacity 125ms;z-index:0}@media (pointer:fine){.h>.s,:hover>.s{opacity:1;transform:none}}.x{font-size:14px;margin:0;position:relative}.x code{background:rgb(var(--color-background-subtle));border-radius:var(--space-1);font-size:13px;padding:2px 4px}.t{color:rgb(var(--color-foreground)/var(--alpha-lighter));display:inline-flex;flex-wrap:wrap;font-size:12px;gap:var(--space-1);list-style:none;margin:0;padding:0;position:relative}.t li{white-space:nowrap}.t li:after{content:"/";display:inline;margin-left:var(--space-1)}.t li:last-child:after{content:"";display:none}.e{--space-1:4px;--space-2:calc(var(--space-1)*2);--space-3:calc(var(--space-2)*2);--space-4:calc(var(--space-3)*2);--space-5:calc(var(--space-4)*2);--alpha-light:.7;--alpha-lighter:.54;--alpha-lightest:.1;--color-highlight:var(--md-accent-fg-color,#526cfe);--color-highlight-transparent:var(--md-accent-fg-color--transparent,#526cfe1a);--border-radius-1:var(--space-1);--border-radius-2:var(--space-2);--border-radius-3:calc(var(--space-1) + var(--space-2));--font-family:var(--md-text-font-family,Inter,Roboto Flex,system-ui,sans-serif);--font-size:16px;--line-height:1.5;--letter-spacing:-.5px;-webkit-font-smoothing:antialiased;align-items:center;display:flex;font-family:var(--font-family);font-size:var(--font-size);height:100vh;justify-content:center;letter-spacing:var(--letter-spacing);line-height:var(--line-height);pointer-events:none;position:absolute;width:100vw}@media (pointer:coarse){.e{height:-webkit-fill-available}}.e *,.e :after,.e :before{box-sizing:border-box}';function Ks(e,{index$:t}){let r=Ue(),n=document.createElement("div");document.body.appendChild(n),n.style.position="fixed",n.style.height="100%",n.style.top="0",n.style.zIndex="4";let o=n.attachShadow({mode:"open"});o.appendChild(A("style",{},qs.toString()));try{Ya(r.search,{highlight:r.features.includes("search.highlight")}),me(t).subscribe(i=>{for(let a of i.items)a.location=new URL(a.location,r.base).toString();Ja(i,o)}),b(e,"click").subscribe(()=>{go()}),Tn("search").pipe(ke(1)).subscribe(()=>go())}catch(i){e.hidden=!0;let a=G("label[for=__search]");a.hidden=!0}return Ke}var Bs=_r(So());function Ys(e,{index$:t,location$:r}){return re([t,r.pipe(J(Ye()),L(n=>!!n.searchParams.get("h")))]).pipe(f(([n,o])=>_p(n.config)(o.searchParams.get("h"))),f(n=>{var a;let o=new Map,i=document.createNodeIterator(e,NodeFilter.SHOW_TEXT);for(let s=i.nextNode();s;s=i.nextNode())if((a=s.parentElement)!=null&&a.offsetHeight){let c=s.textContent,l=n(c);l.length>c.length&&o.set(s,l)}for(let[s,c]of o){let{childNodes:l}=A("span",null,c);s.replaceWith(...Array.from(l))}return{ref:e,nodes:o}}))}function _p(e){let t=e.separator.split("|").map(o=>o.replace(/(\(\?[!=<][^)]+\))/g,"").length===0?"\uFFFD":o).join("|"),r=new RegExp(t,"img"),n=(o,i,a)=>`${i}${a}`;return o=>{o=o.replace(/\s+/g," ").replace(/&/g,"&").trim();let i=new RegExp(`(^|${e.separator}|)(${o.split(r).map(a=>a.replace(/[|\\{}()[\]^$+*?.-]/g,"\\$&")).filter(a=>a.length>0).join("|")})`,"img");return a=>(0,Bs.default)(a).replace(i,n).replace(/<\/mark>(\s+)]*>/img,"$1")}}function yp(e,{viewport$:t,main$:r}){let n=e.closest(".md-grid"),o=n.offsetTop-n.parentElement.offsetTop;return re([r,t]).pipe(f(([{offset:i,height:a},{offset:{y:s}}])=>(a=a+Math.min(o,Math.max(0,s-i))-o,{height:a,locked:s>=i+o})),ie((i,a)=>i.height===a.height&&i.locked===a.locked))}function Ao(e,n){var o=n,{header$:t}=o,r=gr(o,["header$"]);let i=G(".md-sidebar__scrollwrap",e),{y:a}=wt(i);return j(()=>{let s=new I,c=s.pipe(he(),ye(!0)),l=s.pipe(Xe(0,je));return l.pipe(pe(t)).subscribe({next([{height:u},{height:p}]){i.style.height=`${u-2*a}px`,e.style.top=`${p}px`},complete(){i.style.height="",e.style.top=""}}),l.pipe(Sr()).subscribe(()=>{for(let u of P(".md-nav__link--active[href]",e)){if(!u.clientHeight)continue;let p=u.closest(".md-sidebar__scrollwrap");if(typeof p!="undefined"){let d=u.offsetTop-p.offsetTop,{height:m}=Ae(p);p.scrollTo({top:d-m/2})}}}),me(P("label[tabindex]",e)).pipe(oe(u=>b(u,"click").pipe(Ie(ge),f(()=>u),Q(c)))).subscribe(u=>{let p=G(`[id="${u.htmlFor}"]`);G(`[aria-labelledby="${u.id}"]`).setAttribute("aria-expanded",`${p.checked}`)}),X("content.tooltips")&&me(P("abbr[title]",e)).pipe(oe(u=>Ge(u,{viewport$})),Q(c)).subscribe(),yp(e,r).pipe($(u=>s.next(u)),V(()=>s.complete()),f(u=>H({ref:e},u)))})}function Gs(e,t){if(typeof t!="undefined"){let r=`https://api.github.com/repos/${e}/${t}`;return $t(et(`${r}/releases/latest`).pipe(_e(()=>y),f(n=>({version:n.tag_name})),ot({})),et(r).pipe(_e(()=>y),f(n=>({stars:n.stargazers_count,forks:n.forks_count})),ot({}))).pipe(f(([n,o])=>H(H({},n),o)))}else{let r=`https://api.github.com/users/${e}`;return et(r).pipe(f(n=>({repositories:n.public_repos})),ot({}))}}function Js(e,t){let r=`https://${e}/api/v4/projects/${encodeURIComponent(t)}`;return $t(et(`${r}/releases/permalink/latest`).pipe(_e(()=>y),f(({tag_name:n})=>({version:n})),ot({})),et(r).pipe(_e(()=>y),f(({star_count:n,forks_count:o})=>({stars:n,forks:o})),ot({}))).pipe(f(([n,o])=>H(H({},n),o)))}function Xs(e){let t=e.match(/^.+github\.com\/([^/]+)\/?([^/]+)?/i);if(t){let[,r,n]=t;return Gs(r,n)}if(t=e.match(/^.+?([^/]*gitlab[^/]+)\/(.+?)\/?$/i),t){let[,r,n]=t;return Js(r,n)}return y}var xp;function wp(e){return xp||(xp=j(()=>{let t=__md_get("__source",sessionStorage);if(t)return Y(t);if(Ee("consent").length){let n=__md_get("__consent");if(!(n&&n.github))return y}return Xs(e.href).pipe($(n=>__md_set("__source",n,sessionStorage)))}).pipe(_e(()=>y),L(t=>Object.keys(t).length>0),f(t=>({facts:t})),se(1)))}function Zs(e){let t=G(":scope > :last-child",e);return j(()=>{let r=new I;return r.subscribe(({facts:n})=>{t.appendChild(bs(n)),t.classList.add("md-source__repository--active")}),wp(e).pipe($(n=>r.next(n)),V(()=>r.complete()),f(n=>H({ref:e},n)))})}function Ep(e,{viewport$:t,header$:r}){return Re(document.body).pipe(g(()=>Sn(e,{header$:r,viewport$:t})),f(({offset:{y:n}})=>({hidden:n>=10})),fe("hidden"))}function Qs(e,t){return j(()=>{let r=new I;return r.subscribe({next({hidden:n}){e.hidden=n},complete(){e.hidden=!1}}),(X("navigation.tabs.sticky")?Y({hidden:!1}):Ep(e,t)).pipe($(n=>r.next(n)),V(()=>r.complete()),f(n=>H({ref:e},n)))})}function Tp(e,{viewport$:t,header$:r}){let n=new Map,o=P(".md-nav__link",e);for(let s of o){let c=decodeURIComponent(s.hash.substring(1)),l=Le(`[id="${c}"]`);typeof l!="undefined"&&n.set(s,l)}let i=r.pipe(fe("height"),f(({height:s})=>{let c=ht("main"),l=G(":scope > :first-child",c);return s+.9*(l.offsetTop-c.offsetTop)}),xe());return Re(document.body).pipe(fe("height"),g(s=>j(()=>{let c=[];return Y([...n].reduce((l,[u,p])=>{for(;c.length&&n.get(c[c.length-1]).tagName>=p.tagName;)c.pop();let d=p.offsetTop;for(;!d&&p.parentElement;)p=p.parentElement,d=p.offsetTop;let m=p.offsetParent;for(;m;m=m.offsetParent)d+=m.offsetTop;return l.set([...c=[...c,u]].reverse(),d)},new Map))}).pipe(f(c=>new Map([...c].sort(([,l],[,u])=>l-u))),Ze(i),g(([c,l])=>t.pipe(Or(([u,p],{offset:{y:d},size:m})=>{let h=d+m.height>=Math.floor(s.height);for(;p.length;){let[,v]=p[0];if(v-l=d&&!h)p=[u.pop(),...p];else break}return[u,p]},[[],[...c]]),ie((u,p)=>u[0]===p[0]&&u[1]===p[1])))))).pipe(f(([s,c])=>({prev:s.map(([l])=>l),next:c.map(([l])=>l)})),J({prev:[],next:[]}),Pt(2,1),f(([s,c])=>s.prev.length{let i=new I,a=i.pipe(he(),ye(!0));if(i.subscribe(({prev:s,next:c})=>{for(let[l]of c)l.classList.remove("md-nav__link--passed"),l.classList.remove("md-nav__link--active");for(let[l,[u]]of s.entries())u.classList.add("md-nav__link--passed"),u.classList.toggle("md-nav__link--active",l===s.length-1)}),X("toc.follow")){let s=R(t.pipe(Be(1),f(()=>{})),t.pipe(Be(250),f(()=>"smooth")));i.pipe(L(({prev:c})=>c.length>0),Ze(n.pipe(Ie(ge))),pe(s)).subscribe(([[{prev:c}],l])=>{let[u]=c[c.length-1];if(u.offsetHeight){let p=ki(u);if(typeof p!="undefined"){let d=u.offsetTop-p.offsetTop,{height:m}=Ae(p);p.scrollTo({top:d-m/2,behavior:l})}}})}return X("navigation.tracking")&&t.pipe(Q(a),fe("offset"),Be(250),ke(1),Q(o.pipe(ke(1))),jt({delay:250}),pe(i)).subscribe(([,{prev:s}])=>{let c=Ye(),l=s[s.length-1];if(l&&l.length){let[u]=l,{hash:p}=new URL(u.href);c.hash!==p&&(c.hash=p,history.replaceState({},"",`${c}`))}else c.hash="",history.replaceState({},"",`${c}`)}),Tp(e,{viewport$:t,header$:r}).pipe($(s=>i.next(s)),V(()=>i.complete()),f(s=>H({ref:e},s)))})}function Sp(e,{viewport$:t,main$:r,target$:n}){let o=t.pipe(f(({offset:{y:a}})=>a),Pt(2,1),f(([a,s])=>a>s&&s>0),ie()),i=r.pipe(f(({active:a})=>a));return re([i,o]).pipe(f(([a,s])=>!(a&&s)),ie(),Q(n.pipe(ke(1))),ye(!0),jt({delay:250}),f(a=>({hidden:a})))}function tc(e,{viewport$:t,header$:r,main$:n,target$:o}){let i=new I,a=i.pipe(he(),ye(!0));return i.subscribe({next({hidden:s}){e.hidden=s,s?(e.setAttribute("tabindex","-1"),e.blur()):e.removeAttribute("tabindex")},complete(){e.style.top="",e.hidden=!0,e.removeAttribute("tabindex")}}),r.pipe(Q(a),fe("height")).subscribe(({height:s})=>{e.style.top=`${s+16}px`}),b(e,"click").subscribe(s=>{s.preventDefault(),window.scrollTo({top:0})}),Sp(e,{viewport$:t,main$:n,target$:o}).pipe($(s=>i.next(s)),V(()=>i.complete()),f(s=>H({ref:e},s)))}function rc(e,t){return e.protocol=t.protocol,e.hostname=t.hostname,t.port&&(e.port=t.port),e}function Op(e,t){let r=new Map;for(let n of P("url",e)){let o=G("loc",n),i=[rc(new URL(o.textContent),t)];r.set(`${i[0]}`,i);for(let a of P("[rel=alternate]",n)){let s=a.getAttribute("href");s!=null&&i.push(rc(new URL(s),t))}}return r}function dr(e){return ns(new URL("sitemap.xml",e)).pipe(f(t=>Op(t,new URL(e))),_e(()=>Y(new Map)),xe())}function __ha_langroot(e){let t=new URL(e),r=t.pathname.match(/^\/(zh-hant|en|ja|ru)(?:\/|$)/);return t.pathname=r?`/${r[1]}/`:"/",t.search="",t.hash="",t}function nc({document$:e}){let t=new Map;e.pipe(g(()=>P("link[rel=alternate]")),f(r=>__ha_langroot(r.href)),L(r=>!t.has(r.toString())),oe(r=>dr(r).pipe(f(n=>[r,n]),_e(()=>y)))).subscribe(([r,n])=>{t.set(r.toString().replace(/\/$/,""),n)}),b(document.body,"click").pipe(L(r=>!r.metaKey&&!r.ctrlKey),g(r=>{if(r.target instanceof Element){let n=r.target.closest("a");if(n&&!n.target){let o=[...t].find(([p])=>n.href.startsWith(`${p}/`));if(typeof o=="undefined")return y;let[i,a]=o,s=Ye();if(s.href.startsWith(i))return y;let c=Ue(),l=s.href.replace(c.base,"");l=`${i}/${l}`;let u=a.has(l.split("#")[0])?new URL(l,c.base):new URL(i);return r.preventDefault(),Y(u)}}return y})).subscribe(r=>dt(r,!0))}var Co=_r(Mo());function Lp(e){e.setAttribute("data-md-copying","");let t=e.closest("[data-copy]"),r=t?t.getAttribute("data-copy"):e.innerText;return e.removeAttribute("data-md-copying"),r.trimEnd()}function oc({alert$:e}){Co.default.isSupported()&&new U(t=>{new Co.default("[data-clipboard-target], [data-clipboard-text]",{text:r=>r.getAttribute("data-clipboard-text")||Lp(G(r.getAttribute("data-clipboard-target")))}).on("success",r=>t.next(r))}).pipe($(t=>{t.trigger.focus()}),f(()=>Bt("clipboard.copied"))).subscribe(e)}function ic(e,t){if(!(e.target instanceof Element))return y;let r=e.target.closest("a");if(r===null)return y;if(r.target||e.metaKey||e.ctrlKey)return y;let n=new URL(r.href);return n.search=n.hash="",t.has(`${n}`)?(e.preventDefault(),Y(r)):y}function ac(e){let t=new Map;for(let r of P(":scope > *",e.head))t.set(r.outerHTML,r);return t}function sc(e){for(let t of P("[href], [src]",e))for(let r of["href","src"]){let n=t.getAttribute(r);if(n&&!/^(?:[a-z]+:)?\/\//i.test(n)){t[r]=t[r];break}}return Y(e)}function Mp(e){for(let n of["[data-md-component=announce]","[data-md-component=container]","[data-md-component=header-topic]","[data-md-component=outdated]","[data-md-component=logo]","[data-md-component=skip]",...X("navigation.tabs.sticky")?["[data-md-component=tabs]"]:[]]){let o=Le(n),i=Le(n,e);typeof o!="undefined"&&typeof i!="undefined"&&o.replaceWith(i)}let t=ac(document);for(let[n,o]of ac(e))t.has(n)?t.delete(n):document.head.appendChild(o);for(let n of t.values()){let o=n.getAttribute("name");o!=="theme-color"&&o!=="color-scheme"&&n.remove()}let r=ht("container");return nt(P("script",r)).pipe(g(n=>{let o=e.createElement("script");if(n.src){for(let i of n.getAttributeNames())o.setAttribute(i,n.getAttribute(i));return n.replaceWith(o),new U(i=>{o.onload=()=>i.complete()})}else return o.textContent=n.textContent,n.replaceWith(o),y}),he(),ye(document))}function cc({sitemap$:e,location$:t,viewport$:r,progress$:n}){if(location.protocol==="file:")return Ke;Y(document).subscribe(sc);let o=b(document.body,"click").pipe(Ze(e),g(([s,c])=>ic(s,c)),f(({href:s})=>new URL(s)),xe()),i=b(window,"popstate").pipe(f(Ye),xe());o.pipe(pe(r)).subscribe(([s,{offset:c}])=>{history.replaceState(c,""),history.pushState(null,"",s)}),R(o,i).subscribe(t);let a=t.pipe(fe("pathname"),g(s=>En(s,{progress$:n}).pipe(_e(()=>(dt(s,!0),y)))),g(sc),g(Mp),xe());return R(a.pipe(pe(t,(s,c)=>c)),a.pipe(g(()=>t),fe("hash")),t.pipe(ie((s,c)=>s.pathname===c.pathname&&s.hash===c.hash),g(()=>o),$(()=>history.back()))).subscribe(s=>{var c,l;history.state!==null||!s.hash?window.scrollTo(0,(l=(c=history.state)==null?void 0:c.y)!=null?l:0):(history.scrollRestoration="auto",es(s.hash),history.scrollRestoration="manual")}),t.subscribe(()=>{history.scrollRestoration="manual"}),b(window,"beforeunload").subscribe(()=>{history.scrollRestoration="auto"}),r.pipe(fe("offset"),Be(100)).subscribe(({offset:s})=>{history.replaceState(s,"")}),X("navigation.instant.prefetch")&&R(b(document.body,"mousemove"),b(document.body,"focusin")).pipe(Ze(e),g(([s,c])=>ic(s,c)),Be(25),Yn(({href:s})=>s),cn(s=>{let c=document.createElement("link");return c.rel="prefetch",c.href=s.toString(),document.head.appendChild(c),b(c,"load").pipe(f(()=>c),Me(1))})).subscribe(s=>s.remove()),a}function lc(e){var u;let{selectedVersionSitemap:t,selectedVersionBaseURL:r,currentLocation:n,currentBaseURL:o}=e,i=(u=Ho(o))==null?void 0:u.pathname;if(i===void 0)return;let a=kp(n.pathname,i);if(a===void 0)return;let s=Cp(t.keys());if(!t.has(s))return;let c=Ho(a,s);if(!c||!t.has(c.href))return;let l=Ho(a,r);if(l)return l.hash=n.hash,l.search=n.search,l}function Ho(e,t){try{return new URL(e,t)}catch(r){return}}function kp(e,t){if(e.startsWith(t))return e.slice(t.length)}function Ap(e,t){let r=Math.min(e.length,t.length),n;for(n=0;ny)),n=r.pipe(f(o=>{let[,i]=t.base.match(/([^/]+)\/?$/);return o.find(({version:a,aliases:s})=>a===i||s.includes(i))||o[0]}));r.pipe(f(o=>new Map(o.map(i=>[`${new URL(`../${i.version}/`,t.base)}`,i]))),g(o=>b(document.body,"click").pipe(L(i=>!i.metaKey&&!i.ctrlKey),pe(n),g(([i,a])=>{if(i.target instanceof Element){let s=i.target.closest("a");if(s&&!s.target&&o.has(s.href)){let c=s.href;return!i.target.closest(".md-version")&&o.get(c)===a?y:(i.preventDefault(),Y(new URL(c)))}}return y}),g(i=>dr(i).pipe(f(a=>{var s;return(s=lc({selectedVersionSitemap:a,selectedVersionBaseURL:i,currentLocation:Ye(),currentBaseURL:t.base}))!=null?s:i})))))).subscribe(o=>dt(o,!0)),re([r,n]).subscribe(([o,i])=>{G(".md-header__topic").appendChild(_s(o,i))}),e.pipe(g(()=>n)).subscribe(o=>{var s;let i=new URL(t.base),a=__md_get("__outdated",sessionStorage,i);if(a===null){a=!0;let c=((s=t.version)==null?void 0:s.default)||"latest";Array.isArray(c)||(c=[c]);e:for(let l of c)for(let u of o.aliases.concat(o.version))if(new RegExp(l,"i").test(u)){a=!1;break e}__md_set("__outdated",a,sessionStorage,i)}if(a)for(let c of Ee("outdated"))c.hidden=!1})}function pc({document$:e,viewport$:t}){e.pipe(g(()=>P(".md-ellipsis")),oe(r=>Et(r).pipe(Q(e.pipe(ke(1))),L(n=>n),f(()=>r),Me(1))),L(r=>r.offsetWidth{let n=r.innerText,o=r.closest("a")||r;return o.title=n,X("content.tooltips")?Ge(o,{viewport$:t}).pipe(Q(e.pipe(ke(1))),V(()=>o.removeAttribute("title"))):y})).subscribe(),X("content.tooltips")&&e.pipe(g(()=>P(".md-status")),oe(r=>Ge(r,{viewport$:t}))).subscribe()}function fc({document$:e,tablet$:t}){e.pipe(g(()=>P(".md-toggle--indeterminate")),$(r=>{r.indeterminate=!0,r.checked=!1}),oe(r=>b(r,"change").pipe(Xn(()=>r.classList.contains("md-toggle--indeterminate")),f(()=>r))),pe(t)).subscribe(([r,n])=>{r.classList.remove("md-toggle--indeterminate"),n&&(r.checked=!1)})}function Hp(){return/(iPad|iPhone|iPod)/.test(navigator.userAgent)}function mc({document$:e}){e.pipe(g(()=>P("[data-md-scrollfix]")),$(t=>t.removeAttribute("data-md-scrollfix")),L(Hp),oe(t=>b(t,"touchstart").pipe(f(()=>t)))).subscribe(t=>{let r=t.scrollTop;r===0?t.scrollTop=1:r+t.offsetHeight===t.scrollHeight&&(t.scrollTop=r-1)})}Object.entries||(Object.entries=function(e){let t=[];for(let r of Object.keys(e))t.push([r,e[r]]);return t});Object.values||(Object.values=function(e){let t=[];for(let r of Object.keys(e))t.push(e[r]);return t});typeof Element!="undefined"&&(Element.prototype.scrollTo||(Element.prototype.scrollTo=function(e,t){typeof e=="object"?(this.scrollLeft=e.left,this.scrollTop=e.top):(this.scrollLeft=e,this.scrollTop=t)}),Element.prototype.replaceWith||(Element.prototype.replaceWith=function(...e){let t=this.parentNode;if(t){e.length===0&&t.removeChild(this);for(let r=e.length-1;r>=0;r--){let n=e[r];typeof n=="string"?n=document.createTextNode(n):n.parentNode&&n.parentNode.removeChild(n),r?t.insertBefore(this.previousSibling,n):t.replaceChild(n,this)}}}));function $p(){return location.protocol==="file:"?ar(`${new URL("search.js",Mn.base)}`).pipe(f(()=>__index),_e(()=>Ke),se(1)):et(new URL("search.json",Mn.base))}document.documentElement.classList.remove("no-js");document.documentElement.classList.add("js");var vt=Si(),Ur=Za(),hr=ts(Ur),hc=Xa(),ze=cs(),$o=Ir("(min-width: 60em)"),vc=Ir("(min-width: 76.25em)"),bc=rs(),Mn=Ue(),gc=Le(".md-search")?$p():Ke,Po=new I;oc({alert$:Po});nc({document$:vt});var Io=new I,_c=dr(Mn.base);X("navigation.instant")&&cc({sitemap$:_c,location$:Ur,viewport$:ze,progress$:Io}).subscribe(vt);var dc;((dc=Mn.version)==null?void 0:dc.provider)==="mike"&&uc({document$:vt});R(Ur,hr).pipe(It(125)).subscribe(()=>{Eo("drawer",!1),Eo("search",!1)});hc.pipe(L(({mode:e,meta:t})=>e==="global"&&!t)).subscribe(e=>{switch(e.type){case",":case"p":let t=document.querySelector("link[rel=prev]");t instanceof HTMLLinkElement&&dt(t);break;case".":case"n":let r=document.querySelector("link[rel=next]");r instanceof HTMLLinkElement&&dt(r);break;case"/":let n=document.querySelector("[data-md-component=search] button");n instanceof HTMLButtonElement&&n.click();break;case"Enter":let o=xt();o instanceof HTMLLabelElement&&o.click()}});pc({viewport$:ze,document$:vt});fc({document$:vt,tablet$:$o});mc({document$:vt});var Lt=Us(ht("header"),{viewport$:ze}),Fr=vt.pipe(f(()=>ht("main")),g(e=>Ws(e,{viewport$:ze,header$:Lt})),se(1)),Pp=R(...Ee("consent").map(e=>us(e,{target$:hr})),...Ee("dialog").map(e=>Fs(e,{alert$:Po})),...Ee("palette").map(e=>Vs(e)),...Ee("progress").map(e=>zs(e,{progress$:Io})),...Ee("search").map(e=>Ks(e,{index$:gc})),...Ee("source").map(e=>Zs(e))),Ip=j(()=>R(...Ee("announce").map(e=>ls(e)),...Ee("content").map(e=>js(e,{sitemap$:_c,viewport$:ze,target$:hr,print$:bc})),...Ee("content").map(e=>X("search.highlight")?Ys(e,{index$:gc,location$:Ur}):y),...Ee("header").map(e=>Ns(e,{viewport$:ze,header$:Lt,main$:Fr})),...Ee("header-title").map(e=>Ds(e,{viewport$:ze,header$:Lt})),...Ee("sidebar").map(e=>e.getAttribute("data-md-type")==="navigation"?yo(vc,()=>Ao(e,{viewport$:ze,header$:Lt,main$:Fr})):yo($o,()=>Ao(e,{viewport$:ze,header$:Lt,main$:Fr}))),...Ee("tabs").map(e=>Qs(e,{viewport$:ze,header$:Lt})),...Ee("toc").map(e=>ec(e,{viewport$:ze,header$:Lt,main$:Fr,target$:hr})),...Ee("top").map(e=>tc(e,{viewport$:ze,header$:Lt,main$:Fr,target$:hr})))),yc=vt.pipe(g(()=>Ip),Rt(Pp),se(1));yc.subscribe();window.document$=vt;window.location$=Ur;window.target$=hr;window.keyboard$=hc;window.viewport$=ze;window.tablet$=$o;window.screen$=vc;window.print$=bc;window.alert$=Po;window.progress$=Io;window.component$=yc;})(); -/*! update cache: 20260410225905 */ +/*! update cache: 20260414173552 */ diff --git a/en/assets/javascripts/bundle.c2b142ea.min.js b/en/assets/javascripts/bundle.c2b142ea.min.js index c5915d05c..e77b30d40 100644 --- a/en/assets/javascripts/bundle.c2b142ea.min.js +++ b/en/assets/javascripts/bundle.c2b142ea.min.js @@ -1,4 +1,4 @@ "use strict";(()=>{var xc=Object.create;var kn=Object.defineProperty,wc=Object.defineProperties,Ec=Object.getOwnPropertyDescriptor,Tc=Object.getOwnPropertyDescriptors,Sc=Object.getOwnPropertyNames,Dr=Object.getOwnPropertySymbols,Oc=Object.getPrototypeOf,An=Object.prototype.hasOwnProperty,Fo=Object.prototype.propertyIsEnumerable;var jo=(e,t,r)=>t in e?kn(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,H=(e,t)=>{for(var r in t||(t={}))An.call(t,r)&&jo(e,r,t[r]);if(Dr)for(var r of Dr(t))Fo.call(t,r)&&jo(e,r,t[r]);return e},He=(e,t)=>wc(e,Tc(t));var gr=(e,t)=>{var r={};for(var n in e)An.call(e,n)&&t.indexOf(n)<0&&(r[n]=e[n]);if(e!=null&&Dr)for(var n of Dr(e))t.indexOf(n)<0&&Fo.call(e,n)&&(r[n]=e[n]);return r};var Cn=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var Lc=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of Sc(t))!An.call(e,o)&&o!==r&&kn(e,o,{get:()=>t[o],enumerable:!(n=Ec(t,o))||n.enumerable});return e};var _r=(e,t,r)=>(r=e!=null?xc(Oc(e)):{},Lc(t||!e||!e.__esModule?kn(r,"default",{value:e,enumerable:!0}):r,e));var Uo=(e,t,r)=>new Promise((n,o)=>{var i=c=>{try{s(r.next(c))}catch(l){o(l)}},a=c=>{try{s(r.throw(c))}catch(l){o(l)}},s=c=>c.done?n(c.value):Promise.resolve(c.value).then(i,a);s((r=r.apply(e,t)).next())});var Do=Cn((Hn,No)=>{(function(e,t){typeof Hn=="object"&&typeof No!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(Hn,(function(){"use strict";function e(r){var n=!0,o=!1,i=null,a={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function s(_){return!!(_&&_!==document&&_.nodeName!=="HTML"&&_.nodeName!=="BODY"&&"classList"in _&&"contains"in _.classList)}function c(_){var de=_.type,be=_.tagName;return!!(be==="INPUT"&&a[de]&&!_.readOnly||be==="TEXTAREA"&&!_.readOnly||_.isContentEditable)}function l(_){_.classList.contains("focus-visible")||(_.classList.add("focus-visible"),_.setAttribute("data-focus-visible-added",""))}function u(_){_.hasAttribute("data-focus-visible-added")&&(_.classList.remove("focus-visible"),_.removeAttribute("data-focus-visible-added"))}function p(_){_.metaKey||_.altKey||_.ctrlKey||(s(r.activeElement)&&l(r.activeElement),n=!0)}function d(_){n=!1}function m(_){s(_.target)&&(n||c(_.target))&&l(_.target)}function h(_){s(_.target)&&(_.target.classList.contains("focus-visible")||_.target.hasAttribute("data-focus-visible-added"))&&(o=!0,window.clearTimeout(i),i=window.setTimeout(function(){o=!1},100),u(_.target))}function v(_){document.visibilityState==="hidden"&&(o&&(n=!0),x())}function x(){document.addEventListener("mousemove",E),document.addEventListener("mousedown",E),document.addEventListener("mouseup",E),document.addEventListener("pointermove",E),document.addEventListener("pointerdown",E),document.addEventListener("pointerup",E),document.addEventListener("touchmove",E),document.addEventListener("touchstart",E),document.addEventListener("touchend",E)}function w(){document.removeEventListener("mousemove",E),document.removeEventListener("mousedown",E),document.removeEventListener("mouseup",E),document.removeEventListener("pointermove",E),document.removeEventListener("pointerdown",E),document.removeEventListener("pointerup",E),document.removeEventListener("touchmove",E),document.removeEventListener("touchstart",E),document.removeEventListener("touchend",E)}function E(_){_.target.nodeName&&_.target.nodeName.toLowerCase()==="html"||(n=!1,w())}document.addEventListener("keydown",p,!0),document.addEventListener("mousedown",d,!0),document.addEventListener("pointerdown",d,!0),document.addEventListener("touchstart",d,!0),document.addEventListener("visibilitychange",v,!0),x(),r.addEventListener("focus",m,!0),r.addEventListener("blur",h,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)}))});var So=Cn((M0,vs)=>{"use strict";var Gu=/["'&<>]/;vs.exports=Ju;function Ju(e){var t=""+e,r=Gu.exec(t);if(!r)return t;var n,o="",i=0,a=0;for(i=r.index;i{(function(t,r){typeof jr=="object"&&typeof Lo=="object"?Lo.exports=r():typeof define=="function"&&define.amd?define([],r):typeof jr=="object"?jr.ClipboardJS=r():t.ClipboardJS=r()})(jr,function(){return(function(){var e={686:(function(n,o,i){"use strict";i.d(o,{default:function(){return vr}});var a=i(279),s=i.n(a),c=i(370),l=i.n(c),u=i(817),p=i.n(u);function d(B){try{return document.execCommand(B)}catch(C){return!1}}var m=function(C){var k=p()(C);return d("cut"),k},h=m;function v(B){var C=document.documentElement.getAttribute("dir")==="rtl",k=document.createElement("textarea");k.style.fontSize="12pt",k.style.border="0",k.style.padding="0",k.style.margin="0",k.style.position="absolute",k.style[C?"right":"left"]="-9999px";var D=window.pageYOffset||document.documentElement.scrollTop;return k.style.top="".concat(D,"px"),k.setAttribute("readonly",""),k.value=B,k}var x=function(C,k){var D=v(C);k.container.appendChild(D);var W=p()(D);return d("copy"),D.remove(),W},w=function(C){var k=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},D="";return typeof C=="string"?D=x(C,k):C instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(C==null?void 0:C.type)?D=x(C.value,k):(D=p()(C),d("copy")),D},E=w;function _(B){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?_=function(k){return typeof k}:_=function(k){return k&&typeof Symbol=="function"&&k.constructor===Symbol&&k!==Symbol.prototype?"symbol":typeof k},_(B)}var de=function(){var C=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},k=C.action,D=k===void 0?"copy":k,W=C.container,Z=C.target,We=C.text;if(D!=="copy"&&D!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(Z!==void 0)if(Z&&_(Z)==="object"&&Z.nodeType===1){if(D==="copy"&&Z.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(D==="cut"&&(Z.hasAttribute("readonly")||Z.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if(We)return E(We,{container:W});if(Z)return D==="cut"?h(Z):E(Z,{container:W})},be=de;function M(B){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?M=function(k){return typeof k}:M=function(k){return k&&typeof Symbol=="function"&&k.constructor===Symbol&&k!==Symbol.prototype?"symbol":typeof k},M(B)}function O(B,C){if(!(B instanceof C))throw new TypeError("Cannot call a class as a function")}function N(B,C){for(var k=0;k0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof W.action=="function"?W.action:this.defaultAction,this.target=typeof W.target=="function"?W.target:this.defaultTarget,this.text=typeof W.text=="function"?W.text:this.defaultText,this.container=M(W.container)==="object"?W.container:document.body}},{key:"listenClick",value:function(W){var Z=this;this.listener=l()(W,"click",function(We){return Z.onClick(We)})}},{key:"onClick",value:function(W){var Z=W.delegateTarget||W.currentTarget,We=this.action(Z)||"copy",Gt=be({action:We,container:this.container,target:this.target(Z),text:this.text(Z)});this.emit(Gt?"success":"error",{action:We,text:Gt,trigger:Z,clearSelection:function(){Z&&Z.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(W){return Yt("action",W)}},{key:"defaultTarget",value:function(W){var Z=Yt("target",W);if(Z)return document.querySelector(Z)}},{key:"defaultText",value:function(W){return Yt("text",W)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(W){var Z=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return E(W,Z)}},{key:"cut",value:function(W){return h(W)}},{key:"isSupported",value:function(){var W=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],Z=typeof W=="string"?[W]:W,We=!!document.queryCommandSupported;return Z.forEach(function(Gt){We=We&&!!document.queryCommandSupported(Gt)}),We}}]),k})(s()),vr=Mt}),828:(function(n){var o=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function a(s,c){for(;s&&s.nodeType!==o;){if(typeof s.matches=="function"&&s.matches(c))return s;s=s.parentNode}}n.exports=a}),438:(function(n,o,i){var a=i(828);function s(u,p,d,m,h){var v=l.apply(this,arguments);return u.addEventListener(d,v,h),{destroy:function(){u.removeEventListener(d,v,h)}}}function c(u,p,d,m,h){return typeof u.addEventListener=="function"?s.apply(null,arguments):typeof d=="function"?s.bind(null,document).apply(null,arguments):(typeof u=="string"&&(u=document.querySelectorAll(u)),Array.prototype.map.call(u,function(v){return s(v,p,d,m,h)}))}function l(u,p,d,m){return function(h){h.delegateTarget=a(h.target,p),h.delegateTarget&&m.call(u,h)}}n.exports=c}),879:(function(n,o){o.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},o.nodeList=function(i){var a=Object.prototype.toString.call(i);return i!==void 0&&(a==="[object NodeList]"||a==="[object HTMLCollection]")&&"length"in i&&(i.length===0||o.node(i[0]))},o.string=function(i){return typeof i=="string"||i instanceof String},o.fn=function(i){var a=Object.prototype.toString.call(i);return a==="[object Function]"}}),370:(function(n,o,i){var a=i(879),s=i(438);function c(d,m,h){if(!d&&!m&&!h)throw new Error("Missing required arguments");if(!a.string(m))throw new TypeError("Second argument must be a String");if(!a.fn(h))throw new TypeError("Third argument must be a Function");if(a.node(d))return l(d,m,h);if(a.nodeList(d))return u(d,m,h);if(a.string(d))return p(d,m,h);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function l(d,m,h){return d.addEventListener(m,h),{destroy:function(){d.removeEventListener(m,h)}}}function u(d,m,h){return Array.prototype.forEach.call(d,function(v){v.addEventListener(m,h)}),{destroy:function(){Array.prototype.forEach.call(d,function(v){v.removeEventListener(m,h)})}}}function p(d,m,h){return s(document.body,d,m,h)}n.exports=c}),817:(function(n){function o(i){var a;if(i.nodeName==="SELECT")i.focus(),a=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var s=i.hasAttribute("readonly");s||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),s||i.removeAttribute("readonly"),a=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var c=window.getSelection(),l=document.createRange();l.selectNodeContents(i),c.removeAllRanges(),c.addRange(l),a=c.toString()}return a}n.exports=o}),279:(function(n){function o(){}o.prototype={on:function(i,a,s){var c=this.e||(this.e={});return(c[i]||(c[i]=[])).push({fn:a,ctx:s}),this},once:function(i,a,s){var c=this;function l(){c.off(i,l),a.apply(s,arguments)}return l._=a,this.on(i,l,s)},emit:function(i){var a=[].slice.call(arguments,1),s=((this.e||(this.e={}))[i]||[]).slice(),c=0,l=s.length;for(c;c0&&i[i.length-1])&&(l[0]===6||l[0]===2)){r=0;continue}if(l[0]===3&&(!i||l[1]>i[0]&&l[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function te(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var n=r.call(e),o,i=[],a;try{for(;(t===void 0||t-- >0)&&!(o=n.next()).done;)i.push(o.value)}catch(s){a={error:s}}finally{try{o&&!o.done&&(r=n.return)&&r.call(n)}finally{if(a)throw a.error}}return i}function ne(e,t,r){if(r||arguments.length===2)for(var n=0,o=t.length,i;n1||c(m,v)})},h&&(o[m]=h(o[m])))}function c(m,h){try{l(n[m](h))}catch(v){d(i[0][3],v)}}function l(m){m.value instanceof kt?Promise.resolve(m.value.v).then(u,p):d(i[0][2],m)}function u(m){c("next",m)}function p(m){c("throw",m)}function d(m,h){m(h),i.shift(),i.length&&c(i[0][0],i[0][1])}}function zo(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],r;return t?t.call(e):(e=typeof $e=="function"?$e(e):e[Symbol.iterator](),r={},n("next"),n("throw"),n("return"),r[Symbol.asyncIterator]=function(){return this},r);function n(i){r[i]=e[i]&&function(a){return new Promise(function(s,c){a=e[i](a),o(s,c,a.done,a.value)})}}function o(i,a,s,c){Promise.resolve(c).then(function(l){i({value:l,done:s})},a)}}function F(e){return typeof e=="function"}function Jt(e){var t=function(n){Error.call(n),n.stack=new Error().stack},r=e(t);return r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r}var Vr=Jt(function(e){return function(r){e(this),this.message=r?r.length+` errors occurred during unsubscription: `+r.map(function(n,o){return o+1+") "+n.toString()}).join(` `):"",this.name="UnsubscriptionError",this.errors=r}});function ct(e,t){if(e){var r=e.indexOf(t);0<=r&&e.splice(r,1)}}var rt=(function(){function e(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var t,r,n,o,i;if(!this.closed){this.closed=!0;var a=this._parentage;if(a)if(this._parentage=null,Array.isArray(a))try{for(var s=$e(a),c=s.next();!c.done;c=s.next()){var l=c.value;l.remove(this)}}catch(v){t={error:v}}finally{try{c&&!c.done&&(r=s.return)&&r.call(s)}finally{if(t)throw t.error}}else a.remove(this);var u=this.initialTeardown;if(F(u))try{u()}catch(v){i=v instanceof Vr?v.errors:[v]}var p=this._finalizers;if(p){this._finalizers=null;try{for(var d=$e(p),m=d.next();!m.done;m=d.next()){var h=m.value;try{qo(h)}catch(v){i=i!=null?i:[],v instanceof Vr?i=ne(ne([],te(i)),te(v.errors)):i.push(v)}}}catch(v){n={error:v}}finally{try{m&&!m.done&&(o=d.return)&&o.call(d)}finally{if(n)throw n.error}}}if(i)throw new Vr(i)}},e.prototype.add=function(t){var r;if(t&&t!==this)if(this.closed)qo(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(r=this._finalizers)!==null&&r!==void 0?r:[]).push(t)}},e.prototype._hasParent=function(t){var r=this._parentage;return r===t||Array.isArray(r)&&r.includes(t)},e.prototype._addParent=function(t){var r=this._parentage;this._parentage=Array.isArray(r)?(r.push(t),r):r?[r,t]:t},e.prototype._removeParent=function(t){var r=this._parentage;r===t?this._parentage=null:Array.isArray(r)&&ct(r,t)},e.prototype.remove=function(t){var r=this._finalizers;r&&ct(r,t),t instanceof e&&t._removeParent(this)},e.EMPTY=(function(){var t=new e;return t.closed=!0,t})(),e})();var Pn=rt.EMPTY;function zr(e){return e instanceof rt||e&&"closed"in e&&F(e.remove)&&F(e.add)&&F(e.unsubscribe)}function qo(e){F(e)?e():e.unsubscribe()}var Je={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var Xt={setTimeout:function(e,t){for(var r=[],n=2;n0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var n=this,o=this,i=o.hasError,a=o.isStopped,s=o.observers;return i||a?Pn:(this.currentObservers=null,s.push(r),new rt(function(){n.currentObservers=null,ct(s,r)}))},t.prototype._checkFinalizedStatuses=function(r){var n=this,o=n.hasError,i=n.thrownError,a=n.isStopped;o?r.error(i):a&&r.complete()},t.prototype.asObservable=function(){var r=new U;return r.source=this,r},t.create=function(r,n){return new Qo(r,n)},t})(U);var Qo=(function(e){ue(t,e);function t(r,n){var o=e.call(this)||this;return o.destination=r,o.source=n,o}return t.prototype.next=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.next)===null||o===void 0||o.call(n,r)},t.prototype.error=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.error)===null||o===void 0||o.call(n,r)},t.prototype.complete=function(){var r,n;(n=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||n===void 0||n.call(r)},t.prototype._subscribe=function(r){var n,o;return(o=(n=this.source)===null||n===void 0?void 0:n.subscribe(r))!==null&&o!==void 0?o:Pn},t})(I);var Un=(function(e){ue(t,e);function t(r){var n=e.call(this)||this;return n._value=r,n}return Object.defineProperty(t.prototype,"value",{get:function(){return this.getValue()},enumerable:!1,configurable:!0}),t.prototype._subscribe=function(r){var n=e.prototype._subscribe.call(this,r);return!n.closed&&r.next(this._value),n},t.prototype.getValue=function(){var r=this,n=r.hasError,o=r.thrownError,i=r._value;if(n)throw o;return this._throwIfClosed(),i},t.prototype.next=function(r){e.prototype.next.call(this,this._value=r)},t})(I);var xr={now:function(){return(xr.delegate||Date).now()},delegate:void 0};var wr=(function(e){ue(t,e);function t(r,n,o){r===void 0&&(r=1/0),n===void 0&&(n=1/0),o===void 0&&(o=xr);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=n,i._timestampProvider=o,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=n===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,n),i}return t.prototype.next=function(r){var n=this,o=n.isStopped,i=n._buffer,a=n._infiniteTimeWindow,s=n._timestampProvider,c=n._windowTime;o||(i.push(r),!a&&i.push(s.now()+c)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var n=this._innerSubscribe(r),o=this,i=o._infiniteTimeWindow,a=o._buffer,s=a.slice(),c=0;c0?e.prototype.schedule.call(this,r,n):(this.delay=n,this.state=r,this.scheduler.flush(this),this)},t.prototype.execute=function(r,n){return n>0||this.closed?e.prototype.execute.call(this,r,n):this._execute(r,n)},t.prototype.requestAsyncId=function(r,n,o){return o===void 0&&(o=0),o!=null&&o>0||o==null&&this.delay>0?e.prototype.requestAsyncId.call(this,r,n,o):(r.flush(this),0)},t})(tr);var ri=(function(e){ue(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t})(rr);var Wn=new ri(ti);var ni=(function(e){ue(t,e);function t(r,n){var o=e.call(this,r,n)||this;return o.scheduler=r,o.work=n,o}return t.prototype.requestAsyncId=function(r,n,o){return o===void 0&&(o=0),o!==null&&o>0?e.prototype.requestAsyncId.call(this,r,n,o):(r.actions.push(this),r._scheduled||(r._scheduled=er.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,n,o){var i;if(o===void 0&&(o=0),o!=null?o>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,n,o);var a=r.actions;n!=null&&n===r._scheduled&&((i=a[a.length-1])===null||i===void 0?void 0:i.id)!==n&&(er.cancelAnimationFrame(n),r._scheduled=void 0)},t})(tr);var oi=(function(e){ue(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var n;r?n=r.id:(n=this._scheduled,this._scheduled=void 0);var o=this.actions,i;r=r||o.shift();do if(i=r.execute(r.state,r.delay))break;while((r=o[0])&&r.id===n&&o.shift());if(this._active=!1,i){for(;(r=o[0])&&r.id===n&&o.shift();)r.unsubscribe();throw i}},t})(rr);var je=new oi(ni);var y=new U(function(e){return e.complete()});function Br(e){return e&&F(e.schedule)}function Vn(e){return e[e.length-1]}function _t(e){return F(Vn(e))?e.pop():void 0}function qe(e){return Br(Vn(e))?e.pop():void 0}function Yr(e,t){return typeof Vn(e)=="number"?e.pop():t}var nr=(function(e){return e&&typeof e.length=="number"&&typeof e!="function"});function Gr(e){return F(e==null?void 0:e.then)}function Jr(e){return F(e[Qt])}function Xr(e){return Symbol.asyncIterator&&F(e==null?void 0:e[Symbol.asyncIterator])}function Zr(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function Rc(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var Qr=Rc();function en(e){return F(e==null?void 0:e[Qr])}function tn(e){return Vo(this,arguments,function(){var r,n,o,i;return Wr(this,function(a){switch(a.label){case 0:r=e.getReader(),a.label=1;case 1:a.trys.push([1,,9,10]),a.label=2;case 2:return[4,kt(r.read())];case 3:return n=a.sent(),o=n.value,i=n.done,i?[4,kt(void 0)]:[3,5];case 4:return[2,a.sent()];case 5:return[4,kt(o)];case 6:return[4,a.sent()];case 7:return a.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function rn(e){return F(e==null?void 0:e.getReader)}function q(e){if(e instanceof U)return e;if(e!=null){if(Jr(e))return jc(e);if(nr(e))return Fc(e);if(Gr(e))return Uc(e);if(Xr(e))return ii(e);if(en(e))return Nc(e);if(rn(e))return Dc(e)}throw Zr(e)}function jc(e){return new U(function(t){var r=e[Qt]();if(F(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function Fc(e){return new U(function(t){for(var r=0;r=2;return function(n){return n.pipe(e?L(function(o,i){return e(o,i,n)}):Oe,Me(1),r?ot(t):wi(function(){return new on}))}}function Gn(e){return e<=0?function(){return y}:S(function(t,r){var n=[];t.subscribe(T(r,function(o){n.push(o),e=2,!0))}function xe(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new I}:t,n=e.resetOnError,o=n===void 0?!0:n,i=e.resetOnComplete,a=i===void 0?!0:i,s=e.resetOnRefCountZero,c=s===void 0?!0:s;return function(l){var u,p,d,m=0,h=!1,v=!1,x=function(){p==null||p.unsubscribe(),p=void 0},w=function(){x(),u=d=void 0,h=v=!1},E=function(){var _=u;w(),_==null||_.unsubscribe()};return S(function(_,de){m++,!v&&!h&&x();var be=d=d!=null?d:r();de.add(function(){m--,m===0&&!v&&!h&&(p=Jn(E,c))}),be.subscribe(de),!u&&m>0&&(u=new Ct({next:function(M){return be.next(M)},error:function(M){v=!0,x(),p=Jn(w,o,M),be.error(M)},complete:function(){h=!0,x(),p=Jn(w,a),be.complete()}}),q(_).subscribe(u))})(l)}}function Jn(e,t){for(var r=[],n=2;ne.next(document)),e}function P(e,t=document){return Array.from(t.querySelectorAll(e))}function G(e,t=document){let r=Le(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function Le(e,t=document){return t.querySelector(e)||void 0}function xt(){var e,t,r,n;return(n=(r=(t=(e=document.activeElement)==null?void 0:e.shadowRoot)==null?void 0:t.activeElement)!=null?r:document.activeElement)!=null?n:void 0}var il=R(b(document.body,"focusin"),b(document.body,"focusout")).pipe(Be(1),J(void 0),f(()=>xt()||document.body),se(1));function ir(e){return il.pipe(f(t=>e.contains(t)),ie())}function Ft(e,t){let{matches:r}=matchMedia("(hover)");return j(()=>(r?R(b(e,"mouseenter").pipe(f(()=>!0)),b(e,"mouseleave").pipe(f(()=>!1))):R(b(e,"touchstart").pipe(f(()=>!0)),b(e,"touchend").pipe(f(()=>!1)),b(e,"touchcancel").pipe(f(()=>!1)))).pipe(t?Tr(o=>Ve(+!o*t)):Oe,J(!0,e.matches(":hover"))))}function Oi(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)Oi(e,r)}function A(e,t,...r){let n=document.createElement(e);if(t)for(let o of Object.keys(t))typeof t[o]!="undefined"&&(typeof t[o]!="boolean"?n.setAttribute(o,t[o]):n.setAttribute(o,""));for(let o of r)Oi(n,o);return n}function Li(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function ar(e){let t=A("script",{src:e});return j(()=>(document.head.appendChild(t),R(b(t,"load"),b(t,"error").pipe(g(()=>zn(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(f(()=>{}),V(()=>document.head.removeChild(t)),Me(1))))}var Mi=new I,al=j(()=>typeof ResizeObserver=="undefined"?ar("https://unpkg.com/resize-observer-polyfill"):Y(void 0)).pipe(f(()=>new ResizeObserver(e=>e.forEach(t=>Mi.next(t)))),g(e=>R(Ke,Y(e)).pipe(V(()=>e.disconnect()))),se(1));function Ae(e){return{width:e.offsetWidth,height:e.offsetHeight}}function Re(e){let t=e;for(;t.clientWidth===0&&t.parentElement;)t=t.parentElement;return al.pipe($(r=>r.observe(t)),g(r=>Mi.pipe(L(n=>n.target===t),V(()=>r.unobserve(t)))),f(()=>Ae(e)),J(Ae(e)))}function Mr(e){return{width:e.scrollWidth,height:e.scrollHeight}}function ki(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}function Ai(e){let t=[],r=e.parentElement;for(;r;)(e.clientWidth>r.clientWidth||e.clientHeight>r.clientHeight)&&t.push(r),r=(e=r).parentElement;return t.length===0&&t.push(document.documentElement),t}function wt(e){return{x:e.offsetLeft,y:e.offsetTop}}function Ci(e){let t=e.getBoundingClientRect();return{x:t.x+window.scrollX,y:t.y+window.scrollY}}function Hi(e){return R(b(window,"load"),b(window,"resize")).pipe(Xe(0,je),f(()=>wt(e)),J(wt(e)))}function ln(e){return{x:e.scrollLeft,y:e.scrollTop}}function Ut(e){return R(b(e,"scroll"),b(window,"scroll"),b(window,"resize")).pipe(Xe(0,je),f(()=>ln(e)),J(ln(e)))}var $i=new I,sl=j(()=>Y(new IntersectionObserver(e=>{for(let t of e)$i.next(t)},{threshold:0}))).pipe(g(e=>R(Ke,Y(e)).pipe(V(()=>e.disconnect()))),se(1));function Et(e){return sl.pipe($(t=>t.observe(e)),g(t=>$i.pipe(L(({target:r})=>r===e),V(()=>t.unobserve(e)),f(({isIntersecting:r})=>r))))}var cl=Object.create,la=Object.defineProperty,ll=Object.getOwnPropertyDescriptor,ul=Object.getOwnPropertyNames,pl=Object.getPrototypeOf,fl=Object.prototype.hasOwnProperty,ml=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),dl=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of ul(t))!fl.call(e,o)&&o!==r&&la(e,o,{get:()=>t[o],enumerable:!(n=ll(t,o))||n.enumerable});return e},hl=(e,t,r)=>(r=e!=null?cl(pl(e)):{},dl(t||!e||!e.__esModule?la(r,"default",{value:e,enumerable:!0}):r,e)),vl=ml((e,t)=>{var r="Expected a function",n=NaN,o="[object Symbol]",i=/^\s+|\s+$/g,a=/^[-+]0x[0-9a-f]+$/i,s=/^0b[01]+$/i,c=/^0o[0-7]+$/i,l=parseInt,u=typeof global=="object"&&global&&global.Object===Object&&global,p=typeof self=="object"&&self&&self.Object===Object&&self,d=u||p||Function("return this")(),m=Object.prototype,h=m.toString,v=Math.max,x=Math.min,w=function(){return d.Date.now()};function E(O,N,ee){var le,ce,Ne,bt,De,st,tt=0,Yt=!1,Mt=!1,vr=!0;if(typeof O!="function")throw new TypeError(r);N=M(N)||0,_(ee)&&(Yt=!!ee.leading,Mt="maxWait"in ee,Ne=Mt?v(M(ee.maxWait)||0,N):Ne,vr="trailing"in ee?!!ee.trailing:vr);function B(Te){var gt=le,br=ce;return le=ce=void 0,tt=Te,bt=O.apply(br,gt),bt}function C(Te){return tt=Te,De=setTimeout(W,N),Yt?B(Te):bt}function k(Te){var gt=Te-st,br=Te-tt,Ro=N-gt;return Mt?x(Ro,Ne-br):Ro}function D(Te){var gt=Te-st,br=Te-tt;return st===void 0||gt>=N||gt<0||Mt&&br>=Ne}function W(){var Te=w();if(D(Te))return Z(Te);De=setTimeout(W,k(Te))}function Z(Te){return De=void 0,vr&&le?B(Te):(le=ce=void 0,bt)}function We(){De!==void 0&&clearTimeout(De),tt=0,le=st=ce=De=void 0}function Gt(){return De===void 0?bt:Z(w())}function Nr(){var Te=w(),gt=D(Te);if(le=arguments,ce=this,st=Te,gt){if(De===void 0)return C(st);if(Mt)return De=setTimeout(W,N),B(st)}return De===void 0&&(De=setTimeout(W,N)),bt}return Nr.cancel=We,Nr.flush=Gt,Nr}function _(O){var N=typeof O;return!!O&&(N=="object"||N=="function")}function de(O){return!!O&&typeof O=="object"}function be(O){return typeof O=="symbol"||de(O)&&h.call(O)==o}function M(O){if(typeof O=="number")return O;if(be(O))return n;if(_(O)){var N=typeof O.valueOf=="function"?O.valueOf():O;O=_(N)?N+"":N}if(typeof O!="string")return O===0?O:+O;O=O.replace(i,"");var ee=s.test(O);return ee||c.test(O)?l(O.slice(2),ee?2:8):a.test(O)?n:+O}t.exports=E}),yn,K,ua,pa,Nt,Pi,fa,ma,da,lo,to,ro,bl,Ar={},ha=[],gl=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,Pr=Array.isArray;function pt(e,t){for(var r in t)e[r]=t[r];return e}function uo(e){e&&e.parentNode&&e.parentNode.removeChild(e)}function Wt(e,t,r){var n,o,i,a={};for(i in t)i=="key"?n=t[i]:i=="ref"?o=t[i]:a[i]=t[i];if(arguments.length>2&&(a.children=arguments.length>3?yn.call(arguments,2):r),typeof e=="function"&&e.defaultProps!=null)for(i in e.defaultProps)a[i]===void 0&&(a[i]=e.defaultProps[i]);return fn(e,a,n,o,null)}function fn(e,t,r,n,o){var i={type:e,props:t,key:r,ref:n,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:o!=null?o:++ua,__i:-1,__u:0};return o==null&&K.vnode!=null&&K.vnode(i),i}function ft(e){return e.children}function at(e,t){this.props=e,this.context=t}function cr(e,t){if(t==null)return e.__?cr(e.__,e.__i+1):null;for(var r;ts&&Nt.sort(ma),e=Nt.shift(),s=Nt.length,e.__d&&(r=void 0,n=void 0,o=(n=(t=e).__v).__e,i=[],a=[],t.__P&&((r=pt({},n)).__v=n.__v+1,K.vnode&&K.vnode(r),po(t.__P,r,n,t.__n,t.__P.namespaceURI,32&n.__u?[o]:null,i,o!=null?o:cr(n),!!(32&n.__u),a),r.__v=n.__v,r.__.__k[r.__i]=r,_a(i,r,a),n.__e=n.__=null,r.__e!=o&&va(r)));vn.__r=0}function ba(e,t,r,n,o,i,a,s,c,l,u){var p,d,m,h,v,x,w,E=n&&n.__k||ha,_=t.length;for(c=_l(r,t,E,c,_),p=0;p<_;p++)(m=r.__k[p])!=null&&(d=m.__i==-1?Ar:E[m.__i]||Ar,m.__i=p,x=po(e,m,d,o,i,a,s,c,l,u),h=m.__e,m.ref&&d.ref!=m.ref&&(d.ref&&fo(d.ref,null,m),u.push(m.ref,m.__c||h,m)),v==null&&h!=null&&(v=h),(w=!!(4&m.__u))||d.__k===m.__k?c=ga(m,c,e,w):typeof m.type=="function"&&x!==void 0?c=x:h&&(c=h.nextSibling),m.__u&=-7);return r.__e=v,c}function _l(e,t,r,n,o){var i,a,s,c,l,u=r.length,p=u,d=0;for(e.__k=new Array(o),i=0;i0?fn(a.type,a.props,a.key,a.ref?a.ref:null,a.__v):a).__=e,a.__b=e.__b+1,s=null,(l=a.__i=yl(a,r,c,p))!=-1&&(p--,(s=r[l])&&(s.__u|=2)),s==null||s.__v==null?(l==-1&&(o>u?d--:oc?d--:d++,a.__u|=4))):e.__k[i]=null;if(p)for(i=0;i(u?1:0)){for(o=r-1,i=r+1;o>=0||i=0?o--:i++])!=null&&!(2&l.__u)&&s==l.key&&c==l.type)return a}return-1}function Ri(e,t,r){t[0]=="-"?e.setProperty(t,r!=null?r:""):e[t]=r==null?"":typeof r!="number"||gl.test(t)?r:r+"px"}function un(e,t,r,n,o){var i,a;e:if(t=="style")if(typeof r=="string")e.style.cssText=r;else{if(typeof n=="string"&&(e.style.cssText=n=""),n)for(t in n)r&&t in r||Ri(e.style,t,"");if(r)for(t in r)n&&r[t]==n[t]||Ri(e.style,t,r[t])}else if(t[0]=="o"&&t[1]=="n")i=t!=(t=t.replace(da,"$1")),a=t.toLowerCase(),t=a in e||t=="onFocusOut"||t=="onFocusIn"?a.slice(2):t.slice(2),e.l||(e.l={}),e.l[t+i]=r,r?n?r.u=n.u:(r.u=lo,e.addEventListener(t,i?ro:to,i)):e.removeEventListener(t,i?ro:to,i);else{if(o=="http://www.w3.org/2000/svg")t=t.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if(t!="width"&&t!="height"&&t!="href"&&t!="list"&&t!="form"&&t!="tabIndex"&&t!="download"&&t!="rowSpan"&&t!="colSpan"&&t!="role"&&t!="popover"&&t in e)try{e[t]=r!=null?r:"";break e}catch(s){}typeof r=="function"||(r==null||r===!1&&t[4]!="-"?e.removeAttribute(t):e.setAttribute(t,t=="popover"&&r==1?"":r))}}function ji(e){return function(t){if(this.l){var r=this.l[t.type+e];if(t.t==null)t.t=lo++;else if(t.t0?e:Pr(e)?e.map(ya):pt({},e)}function xl(e,t,r,n,o,i,a,s,c){var l,u,p,d,m,h,v,x=r.props,w=t.props,E=t.type;if(E=="svg"?o="http://www.w3.org/2000/svg":E=="math"?o="http://www.w3.org/1998/Math/MathML":o||(o="http://www.w3.org/1999/xhtml"),i!=null){for(l=0;l=r.__.length&&r.__.push({}),r.__[e]}function bn(e){return $r=1,Tl(Ta,e)}function Tl(e,t,r){var n=mo(Hr++,2);if(n.t=e,!n.__c&&(n.__=[r?r(t):Ta(void 0,t),function(s){var c=n.__N?n.__N[0]:n.__[0],l=n.t(c,s);c!==l&&(n.__N=[l,n.__[1]],n.__c.setState({}))}],n.__c=ve,!ve.__f)){var o=function(s,c,l){if(!n.__c.__H)return!0;var u=n.__c.__H.__.filter(function(d){return!!d.__c});if(u.every(function(d){return!d.__N}))return!i||i.call(this,s,c,l);var p=n.__c.props!==s;return u.forEach(function(d){if(d.__N){var m=d.__[0];d.__=d.__N,d.__N=void 0,m!==d.__[0]&&(p=!0)}}),i&&i.call(this,s,c,l)||p};ve.__f=!0;var i=ve.shouldComponentUpdate,a=ve.componentWillUpdate;ve.componentWillUpdate=function(s,c,l){if(this.__e){var u=i;i=void 0,o(s,c,l),i=u}a&&a.call(this,s,c,l)},ve.shouldComponentUpdate=o}return n.__N||n.__}function mt(e,t){var r=mo(Hr++,3);!we.__s&&Ea(r.__H,t)&&(r.__=e,r.u=t,ve.__H.__h.push(r))}function Vt(e){return $r=5,ur(function(){return{current:e}},[])}function ur(e,t){var r=mo(Hr++,7);return Ea(r.__H,t)&&(r.__=e(),r.__H=t,r.__h=e),r.__}function Sl(e,t){return $r=8,ur(function(){return e},t)}function Ol(){for(var e;e=wa.shift();)if(e.__P&&e.__H)try{e.__H.__h.forEach(mn),e.__H.__h.forEach(oo),e.__H.__h=[]}catch(t){e.__H.__h=[],we.__e(t,e.__v)}}we.__b=function(e){ve=null,Ui&&Ui(e)},we.__=function(e,t){e&&t.__k&&t.__k.__m&&(e.__m=t.__k.__m),zi&&zi(e,t)},we.__r=function(e){Ni&&Ni(e),Hr=0;var t=(ve=e.__c).__H;t&&(Zn===ve?(t.__h=[],ve.__h=[],t.__.forEach(function(r){r.__N&&(r.__=r.__N),r.u=r.__N=void 0})):(t.__h.forEach(mn),t.__h.forEach(oo),t.__h=[],Hr=0)),Zn=ve},we.diffed=function(e){Di&&Di(e);var t=e.__c;t&&t.__H&&(t.__H.__h.length&&(wa.push(t)!==1&&Fi===we.requestAnimationFrame||((Fi=we.requestAnimationFrame)||Ll)(Ol)),t.__H.__.forEach(function(r){r.u&&(r.__H=r.u),r.u=void 0})),Zn=ve=null},we.__c=function(e,t){t.some(function(r){try{r.__h.forEach(mn),r.__h=r.__h.filter(function(n){return!n.__||oo(n)})}catch(n){t.some(function(o){o.__h&&(o.__h=[])}),t=[],we.__e(n,r.__v)}}),Wi&&Wi(e,t)},we.unmount=function(e){Vi&&Vi(e);var t,r=e.__c;r&&r.__H&&(r.__H.__.forEach(function(n){try{mn(n)}catch(o){t=o}}),r.__H=void 0,t&&we.__e(t,r.__v))};var qi=typeof requestAnimationFrame=="function";function Ll(e){var t,r=function(){clearTimeout(n),qi&&cancelAnimationFrame(t),setTimeout(e)},n=setTimeout(r,35);qi&&(t=requestAnimationFrame(r))}function mn(e){var t=ve,r=e.__c;typeof r=="function"&&(e.__c=void 0,r()),ve=t}function oo(e){var t=ve;e.__c=e.__(),ve=t}function Ea(e,t){return!e||e.length!==t.length||t.some(function(r,n){return r!==e[n]})}function Ta(e,t){return typeof t=="function"?t(e):t}function Ml(e,t){for(var r in t)e[r]=t[r];return e}function Ki(e,t){for(var r in e)if(r!=="__source"&&!(r in t))return!0;for(var n in t)if(n!=="__source"&&e[n]!==t[n])return!0;return!1}function Bi(e,t){this.props=e,this.context=t}(Bi.prototype=new at).isPureReactComponent=!0,Bi.prototype.shouldComponentUpdate=function(e,t){return Ki(this.props,e)||Ki(this.state,t)};var Yi=K.__b;K.__b=function(e){e.type&&e.type.__f&&e.ref&&(e.props.ref=e.ref,e.ref=null),Yi&&Yi(e)};var Yx=typeof Symbol<"u"&&Symbol.for&&Symbol.for("react.forward_ref")||3911,kl=K.__e;K.__e=function(e,t,r,n){if(e.then){for(var o,i=t;i=i.__;)if((o=i.__c)&&o.__c)return t.__e==null&&(t.__e=r.__e,t.__k=r.__k),o.__c(e,t)}kl(e,t,r,n)};var Gi=K.unmount;function Sa(e,t,r){return e&&(e.__c&&e.__c.__H&&(e.__c.__H.__.forEach(function(n){typeof n.__c=="function"&&n.__c()}),e.__c.__H=null),(e=Ml({},e)).__c!=null&&(e.__c.__P===r&&(e.__c.__P=t),e.__c.__e=!0,e.__c=null),e.__k=e.__k&&e.__k.map(function(n){return Sa(n,t,r)})),e}function Oa(e,t,r){return e&&r&&(e.__v=null,e.__k=e.__k&&e.__k.map(function(n){return Oa(n,t,r)}),e.__c&&e.__c.__P===t&&(e.__e&&r.appendChild(e.__e),e.__c.__e=!0,e.__c.__P=r)),e}function Qn(){this.__u=0,this.o=null,this.__b=null}function La(e){var t=e.__.__c;return t&&t.__a&&t.__a(e)}function pn(){this.i=null,this.l=null}K.unmount=function(e){var t=e.__c;t&&t.__R&&t.__R(),t&&32&e.__u&&(e.type=null),Gi&&Gi(e)},(Qn.prototype=new at).__c=function(e,t){var r=t.__c,n=this;n.o==null&&(n.o=[]),n.o.push(r);var o=La(n.__v),i=!1,a=function(){i||(i=!0,r.__R=null,o?o(s):s())};r.__R=a;var s=function(){if(!--n.__u){if(n.state.__a){var c=n.state.__a;n.__v.__k[0]=Oa(c,c.__c.__P,c.__c.__O)}var l;for(n.setState({__a:n.__b=null});l=n.o.pop();)l.forceUpdate()}};n.__u++||32&t.__u||n.setState({__a:n.__b=n.__v.__k[0]}),e.then(a,a)},Qn.prototype.componentWillUnmount=function(){this.o=[]},Qn.prototype.render=function(e,t){if(this.__b){if(this.__v.__k){var r=document.createElement("div"),n=this.__v.__k[0].__c;this.__v.__k[0]=Sa(this.__b,r,n.__O=n.__P)}this.__b=null}var o=t.__a&&Wt(ft,null,e.fallback);return o&&(o.__u&=-33),[Wt(ft,null,t.__a?null:e.children),o]};var Ji=function(e,t,r){if(++r[1]===r[0]&&e.l.delete(t),e.props.revealOrder&&(e.props.revealOrder[0]!=="t"||!e.l.size))for(r=e.i;r;){for(;r.length>3;)r.pop()();if(r[1]Object.freeze({get current(){return t.current}}),[])}var Nl=typeof globalThis<"u"&&typeof navigator<"u"&&typeof document<"u";function Dl(e,...t){var r;(r=e==null?void 0:e.addEventListener)==null||r.call(e,...t)}function Wl(e,...t){var r;(r=e==null?void 0:e.removeEventListener)==null||r.call(e,...t)}var Vl=(e,t)=>Object.hasOwn(e,t),zl=()=>!0,ql=()=>!1;function Kl(e=!1){let t=Vt(e),r=Sl(()=>t.current,[]);return mt(()=>(t.current=!0,()=>{t.current=!1}),[]),r}function Bl(e,...t){let r=Kl(),n=ka(t[1]),o=ur(()=>function(...i){r()&&(typeof n.current=="function"?n.current.apply(this,i):typeof n.current.handleEvent=="function"&&n.current.handleEvent.apply(this,i))},[]);mt(()=>{let i=Yl(e)?e.current:e;if(!i)return;let a=t.slice(2);return Dl(i,t[0],o,...a),()=>{Wl(i,t[0],o,...a)}},[e,t[0]])}function Yl(e){return e!==null&&typeof e=="object"&&Vl(e,"current")}var Gl=e=>typeof e=="function"?e:typeof e=="string"?t=>t.key===e:e?zl:ql,Jl=Nl?globalThis:null;function Aa(e,t,r=[],n={}){let{event:o="keydown",target:i=Jl,eventOptions:a}=n,s=ka(t),c=ur(()=>{let l=Gl(e);return function(u){l(u)&&s.current.call(this,u)}},r);Bl(i,o,c,a)}function Ca(e){var t,r,n="";if(typeof e=="string"||typeof e=="number")n+=e;else if(typeof e=="object")if(Array.isArray(e)){var o=e.length;for(t=0;t1)St--;else{for(var e,t=!1;kr!==void 0;){var r=kr;for(kr=void 0,io++;r!==void 0;){var n=r.o;if(r.o=void 0,r.f&=-3,!(8&r.f)&&Pa(r))try{r.c()}catch(o){t||(e=o,t=!0)}r=n}}if(io=0,St--,t)throw e}}function Ql(e){if(St>0)return e();St++;try{return e()}finally{xn()}}var ae=void 0;function Ha(e){var t=ae;ae=void 0;try{return e()}finally{ae=t}}var kr=void 0,St=0,io=0,gn=0;function $a(e){if(ae!==void 0){var t=e.n;if(t===void 0||t.t!==ae)return t={i:0,S:e,p:ae.s,n:void 0,t:ae,e:void 0,x:void 0,r:t},ae.s!==void 0&&(ae.s.n=t),ae.s=t,e.n=t,32&ae.f&&e.S(t),t;if(t.i===-1)return t.i=0,t.n!==void 0&&(t.n.p=t.p,t.p!==void 0&&(t.p.n=t.n),t.p=ae.s,t.n=void 0,ae.s.n=t,ae.s=t),t}}function Ce(e,t){this.v=e,this.i=0,this.n=void 0,this.t=void 0,this.W=t==null?void 0:t.watched,this.Z=t==null?void 0:t.unwatched,this.name=t==null?void 0:t.name}Ce.prototype.brand=Zl;Ce.prototype.h=function(){return!0};Ce.prototype.S=function(e){var t=this,r=this.t;r!==e&&e.e===void 0&&(e.x=r,this.t=e,r!==void 0?r.e=e:Ha(function(){var n;(n=t.W)==null||n.call(t)}))};Ce.prototype.U=function(e){var t=this;if(this.t!==void 0){var r=e.e,n=e.x;r!==void 0&&(r.x=n,e.e=void 0),n!==void 0&&(n.e=r,e.x=void 0),e===this.t&&(this.t=n,n===void 0&&Ha(function(){var o;(o=t.Z)==null||o.call(t)}))}};Ce.prototype.subscribe=function(e){var t=this;return qt(function(){var r=t.value,n=ae;ae=void 0;try{e(r)}finally{ae=n}},{name:"sub"})};Ce.prototype.valueOf=function(){return this.value};Ce.prototype.toString=function(){return this.value+""};Ce.prototype.toJSON=function(){return this.value};Ce.prototype.peek=function(){var e=ae;ae=void 0;try{return this.value}finally{ae=e}};Object.defineProperty(Ce.prototype,"value",{get:function(){var e=$a(this);return e!==void 0&&(e.i=this.i),this.v},set:function(e){if(e!==this.v){if(io>100)throw new Error("Cycle detected");this.v=e,this.i++,gn++,St++;try{for(var t=this.t;t!==void 0;t=t.x)t.t.N()}finally{xn()}}}});function Ot(e,t){return new Ce(e,t)}function Pa(e){for(var t=e.s;t!==void 0;t=t.n)if(t.S.i!==t.i||!t.S.h()||t.S.i!==t.i)return!0;return!1}function Ia(e){for(var t=e.s;t!==void 0;t=t.n){var r=t.S.n;if(r!==void 0&&(t.r=r),t.S.n=t,t.i=-1,t.n===void 0){e.s=t;break}}}function Ra(e){for(var t=e.s,r=void 0;t!==void 0;){var n=t.p;t.i===-1?(t.S.U(t),n!==void 0&&(n.n=t.n),t.n!==void 0&&(t.n.p=n)):r=t,t.S.n=t.r,t.r!==void 0&&(t.r=void 0),t=n}e.s=r}function Kt(e,t){Ce.call(this,void 0),this.x=e,this.s=void 0,this.g=gn-1,this.f=4,this.W=t==null?void 0:t.watched,this.Z=t==null?void 0:t.unwatched,this.name=t==null?void 0:t.name}Kt.prototype=new Ce;Kt.prototype.h=function(){if(this.f&=-3,1&this.f)return!1;if((36&this.f)==32||(this.f&=-5,this.g===gn))return!0;if(this.g=gn,this.f|=1,this.i>0&&!Pa(this))return this.f&=-2,!0;var e=ae;try{Ia(this),ae=this;var t=this.x();(16&this.f||this.v!==t||this.i===0)&&(this.v=t,this.f&=-17,this.i++)}catch(r){this.v=r,this.f|=16,this.i++}return ae=e,Ra(this),this.f&=-2,!0};Kt.prototype.S=function(e){if(this.t===void 0){this.f|=36;for(var t=this.s;t!==void 0;t=t.n)t.S.S(t)}Ce.prototype.S.call(this,e)};Kt.prototype.U=function(e){if(this.t!==void 0&&(Ce.prototype.U.call(this,e),this.t===void 0)){this.f&=-33;for(var t=this.s;t!==void 0;t=t.n)t.S.U(t)}};Kt.prototype.N=function(){if(!(2&this.f)){this.f|=6;for(var e=this.t;e!==void 0;e=e.x)e.t.N()}};Object.defineProperty(Kt.prototype,"value",{get:function(){if(1&this.f)throw new Error("Cycle detected");var e=$a(this);if(this.h(),e!==void 0&&(e.i=this.i),16&this.f)throw this.v;return this.v}});function ta(e,t){return new Kt(e,t)}function ja(e){var t=e.u;if(e.u=void 0,typeof t=="function"){St++;var r=ae;ae=void 0;try{t()}catch(n){throw e.f&=-2,e.f|=8,ho(e),n}finally{ae=r,xn()}}}function ho(e){for(var t=e.s;t!==void 0;t=t.n)t.S.U(t);e.x=void 0,e.s=void 0,ja(e)}function eu(e){if(ae!==this)throw new Error("Out-of-order effect");Ra(this),ae=e,this.f&=-2,8&this.f&&ho(this),xn()}function pr(e,t){this.x=e,this.u=void 0,this.s=void 0,this.o=void 0,this.f=32,this.name=t==null?void 0:t.name}pr.prototype.c=function(){var e=this.S();try{if(8&this.f||this.x===void 0)return;var t=this.x();typeof t=="function"&&(this.u=t)}finally{e()}};pr.prototype.S=function(){if(1&this.f)throw new Error("Cycle detected");this.f|=1,this.f&=-9,ja(this),Ia(this),St++;var e=ae;return ae=this,eu.bind(this,e)};pr.prototype.N=function(){2&this.f||(this.f|=2,this.o=kr,kr=this)};pr.prototype.d=function(){this.f|=8,1&this.f||ho(this)};pr.prototype.dispose=function(){this.d()};function qt(e,t){var r=new pr(e,t);try{r.c()}catch(o){throw r.d(),o}var n=r.d.bind(r);return n[Symbol.dispose]=n,n}var Fa,vo,eo,Ua=[];qt(function(){Fa=this.N})();function fr(e,t){K[e]=t.bind(null,K[e]||function(){})}function _n(e){eo&&eo(),eo=e&&e.S()}function Na(e){var t=this,r=e.data,n=ru(r);n.value=r;var o=ur(function(){for(var s=t,c=t.__v;c=c.__;)if(c.__c){c.__c.__$f|=4;break}var l=ta(function(){var m=n.value.value;return m===0?0:m===!0?"":m||""}),u=ta(function(){return!Array.isArray(l.value)&&!pa(l.value)}),p=qt(function(){if(this.N=Da,u.value){var m=l.value;s.__v&&s.__v.__e&&s.__v.__e.nodeType===3&&(s.__v.__e.data=m)}}),d=t.__$u.d;return t.__$u.d=function(){p(),d.call(this)},[u,l]},[]),i=o[0],a=o[1];return i.value?a.peek():a.value}Na.displayName="ReactiveTextNode";Object.defineProperties(Ce.prototype,{constructor:{configurable:!0,value:void 0},type:{configurable:!0,value:Na},props:{configurable:!0,get:function(){return{data:this}}},__b:{configurable:!0,value:1}});fr("__b",function(e,t){if(typeof t.type=="function"&&typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.exitComponent(),typeof t.type=="string"){var r,n=t.props;for(var o in n)if(o!=="children"){var i=n[o];i instanceof Ce&&(r||(t.__np=r={}),r[o]=i,n[o]=i.peek())}}e(t)});fr("__r",function(e,t){if(typeof t.type=="function"&&typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.enterComponent(t),t.type!==ft){_n();var r,n=t.__c;n&&(n.__$f&=-2,(r=n.__$u)===void 0&&(n.__$u=r=(function(o){var i;return qt(function(){i=this}),i.c=function(){n.__$f|=1,n.setState({})},i})())),vo=n,_n(r)}e(t)});fr("__e",function(e,t,r,n){typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.exitComponent(),_n(),vo=void 0,e(t,r,n)});fr("diffed",function(e,t){typeof t.type=="function"&&typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.exitComponent(),_n(),vo=void 0;var r;if(typeof t.type=="string"&&(r=t.__e)){var n=t.__np,o=t.props;if(n){var i=r.U;if(i)for(var a in i){var s=i[a];s!==void 0&&!(a in n)&&(s.d(),i[a]=void 0)}else i={},r.U=i;for(var c in n){var l=i[c],u=n[c];l===void 0?(l=tu(r,c,u,o),i[c]=l):l.o(u,o)}}}e(t)});function tu(e,t,r,n){var o=t in e&&e.ownerSVGElement===void 0,i=Ot(r);return{o:function(a,s){i.value=a,n=s},d:qt(function(){this.N=Da;var a=i.value.value;n[t]!==a&&(n[t]=a,o?e[t]=a:a?e.setAttribute(t,a):e.removeAttribute(t))})}}fr("unmount",function(e,t){if(typeof t.type=="string"){var r=t.__e;if(r){var n=r.U;if(n){r.U=void 0;for(var o in n){var i=n[o];i&&i.d()}}}}else{var a=t.__c;if(a){var s=a.__$u;s&&(a.__$u=void 0,s.d())}}e(t)});fr("__h",function(e,t,r,n){(n<3||n===9)&&(t.__$f|=2),e(t,r,n)});at.prototype.shouldComponentUpdate=function(e,t){var r=this.__$u,n=r&&r.s!==void 0;for(var o in t)return!0;if(this.__f||typeof this.u=="boolean"&&this.u===!0){var i=2&this.__$f;if(!(n||i||4&this.__$f)||1&this.__$f)return!0}else if(!(n||4&this.__$f)||3&this.__$f)return!0;for(var a in e)if(a!=="__source"&&e[a]!==this.props[a])return!0;for(var s in this.props)if(!(s in e))return!0;return!1};function ru(e,t){return bn(function(){return Ot(e,t)})[0]}var nu=function(e){queueMicrotask(function(){queueMicrotask(e)})};function ou(){Ql(function(){for(var e;e=Ua.shift();)Fa.call(e)})}function Da(){Ua.push(this)===1&&(K.requestAnimationFrame||nu)(ou)}var ao=[0];for(let e=0;e<32;e++)ao.push(ao[e]|1<>>5]>>>e&1}set(e){this.data[e>>>5]|=1<<(e&31)}forEach(e){let t=this.size&31;for(let r=0;r{var r;return(r=t.tags)==null?void 0:r.length})&&(matchMedia("(max-width: 768px)").matches||Wa())}function Dt(){Qe.value=He(H({},Qe.value),{hideSearch:!Qe.value.hideSearch})}function Wa(){Qe.value=He(H({},Qe.value),{hideFilters:!Qe.value.hideFilters})}function dn(){return Qe.value.selectedItem}function so(e){Qe.value=He(H({},Qe.value),{selectedItem:e})}function su(){var e,t;return(t=(e=lr.value)==null?void 0:e.items)!=null?t:[]}function wn(){return typeof Se.value.input=="string"?Se.value.input:""}function Va(e){let t=za();e.length&&!t.length?Se.value=He(H({},Se.value),{page:void 0,input:e}):!e.length&&t.length?Se.value=He(H({},Se.value),{page:void 0,input:{type:"operator",data:{operator:"not",operands:[]}}}):Se.value=He(H({},Se.value),{page:void 0,input:e})}function cu(){typeof it.value.pagination.next<"u"&&(Se.value=He(H({},Se.value),{page:it.value.pagination.next}))}function lu(e){let t=Se.value.filter.input;if("type"in t&&t.type==="operator"){for(let r of t.data.operands)if("type"in r&&r.type==="value"&&typeof r.data.value=="string"&&r.data.value===e)return!0}return!1}function za(){let e=Se.value.filter.input,t=[];if("type"in e&&e.type==="operator")for(let r of e.data.operands)"type"in r&&r.type==="value"&&typeof r.data.value=="string"&&t.push(r.data.value);return t}function uu(e){let t=Se.value.filter.input,r=[];if("type"in t&&t.type==="operator")for(let n of t.data.operands)"type"in n&&n.type==="value"&&typeof n.data.value=="string"&&r.push(n.data.value);if(r.includes(e)){let n=r.indexOf(e);n>-1&&r.splice(n,1)}else r.push(e);Se.value=He(H({},Se.value),{page:void 0,filter:He(H({},Se.value.filter),{input:{type:"operator",data:{operator:"and",operands:r.map(n=>({type:"value",data:{field:"tags",value:n}}))}}})}),Va(wn())}function pu(){return it.value.items}function fu(){return it.value.total}function mu(){var e;for(let t of(e=it.value.aggregations)!=null?e:[])if(t.type==="term")return t.data.value;return[]}function sr(){return Qe.value.hideSearch}function du(){return Qe.value.hideFilters}function qa(){var e;return(e=Ka.value.highlight)!=null?e:!1}var Qe=Ot({hideSearch:!0,hideFilters:!0,selectedItem:0}),Ka=Ot({}),lr=Ot(),na=Ot(),Se=Ot({input:"",filter:{input:{type:"operator",data:{operator:"and",operands:[]}},aggregation:{input:[{type:"term",data:{field:"tags"}}]}}}),it=Ot({items:[],query:{select:{documents:new ra(0),terms:new ra(0)},values:[]},pagination:{total:0}});function hu(e,t,r){for(let n=0;tr&&t(0,o,r,r=i);continue;case 62:e.charCodeAt(r+1)===47?t(2,--o,r,r=i+1):hu(e,r,n)?t(3,o,r,r=i+1):t(1,o++,r,r=i+1)}i>r&&t(0,o,r,i)}function bu(e,t=0,r=e.length){let n=++t;e:for(let l=0;n{let i=[],a=[],{onElement:s,onText:c=gu}=typeof r=="function"?{onElement:r}:r,l=0,u=0;return e(t,(p,d,m,h)=>{if(p===0)i[l++]=c(t,m,h),a[u++]={value:null,depth:d};else if(p&1&&(a[u++]={value:bu(t,m,h),depth:d}),p&2)for(let v=0;u>=0;v++){let{value:x,depth:w}=a[--u];if(w>d)continue;let E=i.slice(l-=v,l+v);i[l++]=s(x,E),u++;break}},n,o),i.slice(0,l)}}function yu(e){return e.replace(/[&<>]/g,t=>{switch(t.charCodeAt(0)){case 38:return"&";case 60:return"<";case 62:return">"}})}function hn(e){return e.replace(/&(amp|[lg]t);/g,t=>{switch(t.charCodeAt(1)){case 97:return"&";case 108:return"<";case 103:return">"}})}function xu(e,t){return{start:e.start+t,end:e.end+t,value:e.value}}function wu(e,t,r){return e.slice(t,r)}function Eu(e){let{onHighlight:t,onText:r=wu}=typeof e=="function"?{onHighlight:e}:e;return(n,o,i=0,a=n.length)=>{var l;let s=[],c=(l=o==null?void 0:o.ranges)!=null?l:[];for(let u=0,p=i;ua)break;let m=c[u].end;if(mi&&s.push(r(n,i,d));let{value:h}=c[u];s.push(t(n,{start:d,end:i=m,value:h}))}return i{let o=n.data;switch(o.type){case 1:na.value=!0;break;case 3:typeof o.data.pagination.prev<"u"?it.value=He(H({},it.value),{pagination:o.data.pagination,items:[...it.value.items,...o.data.items]}):(it.value=o.data,so(0));break}},qt(()=>{lr.value&&r.postMessage({type:0,data:lr.value})}),qt(()=>{na.value&&r.postMessage({type:2,data:Se.value})})}var oa={container:"p",hidden:"m"};function ku(e){return z("div",{class:zt(oa.container,{[oa.hidden]:e.hidden}),onClick:()=>Dt()})}var ia={container:"r",disabled:"c"};function co(e){return z("button",{class:zt(ia.container,{[ia.disabled]:!e.onClick}),onClick:e.onClick,children:e.children})}var aa=e=>e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase(),Au=e=>e.replace(/^([A-Z])|[\s-_]+(\w)/g,(t,r,n)=>n?n.toUpperCase():r.toLowerCase()),sa=e=>{let t=Au(e);return t.charAt(0).toUpperCase()+t.slice(1)},Cu=(...e)=>e.filter((t,r,n)=>!!t&&t.trim()!==""&&n.indexOf(t)===r).join(" ").trim(),Hu={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"},$u=c=>{var l=c,{color:e="currentColor",size:t=24,strokeWidth:r=2,absoluteStrokeWidth:n,children:o,iconNode:i,class:a=""}=l,s=gr(l,["color","size","strokeWidth","absoluteStrokeWidth","children","iconNode","class"]);return Wt("svg",H(He(H({},Hu),{width:String(t),height:t,stroke:e,"stroke-width":n?Number(r)*24/Number(t):r,class:["lucide",a].join(" ")}),s),[...i.map(([u,p])=>Wt(u,p)),...Cr(o)])},bo=(e,t)=>{let r=a=>{var s=a,{class:n="",children:o}=s,i=gr(s,["class","children"]);return Wt($u,He(H({},i),{iconNode:t,class:Cu(`lucide-${aa(sa(e))}`,`lucide-${aa(e)}`,n)}),o)};return r.displayName=sa(e),r},Pu=bo("corner-down-left",[["path",{d:"M20 4v7a4 4 0 0 1-4 4H4",key:"6o5b7l"}],["path",{d:"m9 10-5 5 5 5",key:"1kshq7"}]]),Iu=bo("list-filter",[["path",{d:"M2 5h20",key:"1fs1ex"}],["path",{d:"M6 12h12",key:"8npq4p"}],["path",{d:"M9 19h6",key:"456am0"}]]),Ru=bo("search",[["path",{d:"m21 21-4.34-4.34",key:"14j7rj"}],["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}]]),Gx=hl(vl(),1);function ju({threshold:e=0,root:t=null,rootMargin:r="0%",freezeOnceVisible:n=!1,initialIsIntersecting:o=!1,onChange:i}={}){var a;let[s,c]=bn(null),[l,u]=bn(()=>({isIntersecting:o,entry:void 0})),p=Vt();p.current=i;let d=((a=l.entry)==null?void 0:a.isIntersecting)&&n;mt(()=>{if(!s||!("IntersectionObserver"in window)||d)return;let v,x=new IntersectionObserver(w=>{let E=Array.isArray(x.thresholds)?x.thresholds:[x.thresholds];w.forEach(_=>{let de=_.isIntersecting&&E.some(be=>_.intersectionRatio>=be);u({isIntersecting:de,entry:_}),p.current&&p.current(de,_),de&&n&&v&&(v(),v=void 0)})},{threshold:e,root:t,rootMargin:r});return x.observe(s),()=>{x.disconnect()}},[s,JSON.stringify(e),t,r,d,n]);let m=Vt(null);mt(()=>{var v;!s&&(v=l.entry)!=null&&v.target&&!n&&!d&&m.current!==l.entry.target&&(m.current=l.entry.target,u({isIntersecting:o,entry:void 0}))},[s,l.entry,n,d,o]);let h=[c,!!l.isIntersecting,l.entry];return h.ref=h[0],h.isIntersecting=h[1],h.entry=h[2],h}var lt={container:"n",hidden:"l",content:"u",pop:"d",badge:"y",sidebar:"i",controls:"w",results:"k",loadmore:"z"};function Fu(e){let{isIntersecting:t,ref:r}=ju({threshold:0});mt(()=>{t&&cu()},[t]);let n=Vt(null);mt(()=>{n.current&&typeof Se.value.page>"u"&&n.current.scrollTo({top:0,behavior:"smooth"})},[Se.value]);let o=za();return z("div",{class:zt(lt.container,{[lt.hidden]:e.hidden}),children:[z("div",{class:lt.content,children:[z("div",{class:lt.controls,children:[z(co,{onClick:Dt,children:z(Ru,{})}),z(Nu,{focus:!e.hidden}),z(co,{onClick:Wa,children:[z(Iu,{}),o.length>0&&z("span",{class:lt.badge,children:o.length})]})]}),z("div",{class:lt.results,ref:n,children:[z(Du,{keyboard:!e.hidden}),z("div",{class:lt.loadmore,ref:r})]})]}),z("div",{class:zt(lt.sidebar,{[lt.hidden]:du()}),children:z(Uu,{})})]})}var Tt={container:"X",list:"j",heading:"F",title:"I",item:"o",active:"g",value:"R",count:"q"};function Uu(e){let t=mu();return t.sort((r,n)=>n.node.count-r.node.count),z("div",{class:Tt.container,children:[z("h3",{class:Tt.heading,children:"Filters"}),z("h4",{class:Tt.title,children:"Tags"}),z("ol",{class:Tt.list,children:t.map(r=>z("li",{class:zt(Tt.item,{[Tt.active]:lu(r.node.value)}),onClick:()=>uu(r.node.value),children:[z("span",{class:Tt.value,children:r.node.value}),z("span",{class:Tt.count,children:r.node.count})]}))})]})}var ca={container:"f"};function Nu(e){let t=Vt(null);return mt(()=>{var r,n;e.focus?(r=t.current)==null||r.focus():(n=t.current)==null||n.blur()},[e.focus]),z("div",{class:ca.container,children:z("input",{ref:t,type:"text",class:ca.content,value:hn(wn()),onInput:r=>Va(yu(r.currentTarget.value)),autocapitalize:"off",autocomplete:"off",autocorrect:"off",placeholder:"Search",spellcheck:!1,role:"combobox"})})}var ut={container:"b",heading:"A",item:"a",active:"h",wrapper:"B",actions:"s",title:"x",path:"t"};function Ga(){let[e,t]=bn(!1);return mt(()=>{let r=()=>t(!0),n=()=>t(!1);return document.addEventListener("compositionstart",r),document.addEventListener("compositionend",n),()=>{document.removeEventListener("compositionstart",r),document.removeEventListener("compositionend",n)}},[]),e}function Du(e){var s;let t=su(),r=pu(),n=dn(),o=Vt([]),i=Ga();mt(()=>{let c=o.current[n];c&&c.scrollIntoView({block:"center",behavior:"smooth"})},[n]),Aa(e.keyboard,c=>{if(i)return;let l=dn();c.key==="ArrowDown"?(c.preventDefault(),so(Math.min(l+1,r.length-1))):c.key==="ArrowUp"&&(c.preventDefault(),so(Math.max(l-1,0)))},[e.keyboard,i]);let a=(s=fu())!=null?s:0;return z(ft,{children:[r.length>0&&z("h3",{class:ut.heading,children:[z("span",{class:ut.bubble,children:new Intl.NumberFormat("en-US").format(a)})," ","results"]}),z("ol",{class:ut.container,children:r.map((c,l)=>{var m;let u=Ba(t[c.id].title,c.matches.find(({field:h})=>h==="title")),p=Mu((m=t[c.id].path)!=null?m:[],c.matches.find(({field:h})=>h==="path")),d=t[c.id].location;if(qa()){let h=encodeURIComponent(wn()),[v,x]=d.split("#",2);d=`${v}?h=${h.replace(/%20/g,"+")}`,typeof x<"u"&&(d+=`#${x}`)}return z("li",{children:z("a",{ref:h=>{o.current[l]=h},href:d,onClick:()=>Dt(),class:zt(ut.item,{[ut.active]:l===dn()}),children:[z("div",{class:ut.wrapper,children:[z("h2",{class:ut.title,children:u}),z("menu",{class:ut.path,children:p.map(h=>z("li",{children:h}))})]}),z("nav",{class:ut.actions,children:z(co,{children:z(Pu,{})})})]})})})})]})}var Wu={container:"e"};function Vu(e){let t=Ga();return Aa(!0,r=>{var n,o,i,a,s;if(!t)if((r.metaKey||r.ctrlKey)&&r.key==="k")r.preventDefault(),Dt();else if((r.metaKey||r.ctrlKey)&&r.key==="j")document.body.classList.toggle("dark");else if(r.key==="Enter"&&!sr()){r.preventDefault();let c=dn(),l=(o=(n=it.value)==null?void 0:n.items[c])==null?void 0:o.id;if((a=(i=lr.value)==null?void 0:i.items[l])!=null&&a.location){Dt();let u=(s=lr.value)==null?void 0:s.items[l].location;if(qa()){let p=encodeURIComponent(wn()),[d,m]=u.split("#",2);u=`${d}?h=${p.replace(/%20/g,"+")}`,typeof m<"u"&&(u+=`#${m}`)}window.location.href=u}}else r.key==="Escape"&&!sr()&&(r.preventDefault(),Dt())},[t]),z("div",{class:Wu.container,children:[z(ku,{hidden:sr()}),z(Fu,{hidden:sr()})]})}function Ja(e,t){au(e),El(z(Vu,{}),t)}function go(){Dt()}function zu(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function qu(){return R(b(window,"compositionstart").pipe(f(()=>!0)),b(window,"compositionend").pipe(f(()=>!1))).pipe(J(!1))}function Xa(){let e=b(window,"keydown").pipe(f(t=>({mode:sr()?"global":"search",type:t.key,meta:t.ctrlKey||t.metaKey,claim(){t.preventDefault(),t.stopPropagation()}})),L(({mode:t,type:r})=>{if(t==="global"){let n=xt();if(typeof n!="undefined")return!zu(n,r)}return!0}),xe());return qu().pipe(g(t=>t?y:e))}function Ye(){return new URL(location.href)}function dt(e,t=!1){if(X("navigation.instant")&&!t){let r=A("a",{href:e.href});document.body.appendChild(r),r.click(),r.remove()}else location.href=e.href}function Za(){return new I}function Qa(){return location.hash.slice(1)}function es(e){let t=A("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function _o(e){return R(b(window,"hashchange"),e).pipe(f(Qa),J(Qa()),L(t=>t.length>0),se(1))}function ts(e){return _o(e).pipe(f(t=>Le(`[id="${t}"]`)),L(t=>typeof t!="undefined"))}function Ir(e){let t=matchMedia(e);return an(r=>t.addListener(()=>r(t.matches))).pipe(J(t.matches))}function rs(){let e=matchMedia("print");return R(b(window,"beforeprint").pipe(f(()=>!0)),b(window,"afterprint").pipe(f(()=>!1))).pipe(J(e.matches))}function yo(e,t){return e.pipe(g(r=>r?t():y))}function xo(e,t){return new U(r=>{let n=new XMLHttpRequest;return n.open("GET",`${e}`),n.responseType="blob",n.addEventListener("load",()=>{n.status>=200&&n.status<300?(r.next(n.response),r.complete()):r.error(new Error(n.statusText))}),n.addEventListener("error",()=>{r.error(new Error("Network error"))}),n.addEventListener("abort",()=>{r.complete()}),typeof(t==null?void 0:t.progress$)!="undefined"&&(n.addEventListener("progress",o=>{var i;if(o.lengthComputable)t.progress$.next(o.loaded/o.total*100);else{let a=(i=n.getResponseHeader("Content-Length"))!=null?i:0;t.progress$.next(o.loaded/+a*100)}}),t.progress$.next(5)),n.send(),()=>n.abort()})}function et(e,t){return xo(e,t).pipe(g(r=>r.text()),f(r=>JSON.parse(r)),se(1))}function En(e,t){let r=new DOMParser;return xo(e,t).pipe(g(n=>n.text()),f(n=>r.parseFromString(n,"text/html")),se(1))}function ns(e,t){let r=new DOMParser;return xo(e,t).pipe(g(n=>n.text()),f(n=>r.parseFromString(n,"text/xml")),se(1))}var wo={drawer:G("[data-md-toggle=drawer]"),search:G("[data-md-toggle=search]")};function Eo(e,t){wo[e].checked!==t&&wo[e].click()}function Tn(e){let t=wo[e];return b(t,"change").pipe(f(()=>t.checked),J(t.checked))}function os(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function is(){return R(b(window,"scroll",{passive:!0}),b(window,"resize",{passive:!0})).pipe(f(os),J(os()))}function as(){return{width:innerWidth,height:innerHeight}}function ss(){return b(window,"resize",{passive:!0}).pipe(f(as),J(as()))}function cs(){return re([is(),ss()]).pipe(f(([e,t])=>({offset:e,size:t})),se(1))}function Sn(e,{viewport$:t,header$:r}){let n=t.pipe(fe("size")),o=re([n,r]).pipe(f(()=>wt(e)));return re([r,t,o]).pipe(f(([{height:i},{offset:a,size:s},{x:c,y:l}])=>({offset:{x:a.x-c,y:a.y-l+i},size:s})))}var Ku=G("#__config"),mr=JSON.parse(Ku.textContent);mr.base=`${new URL(mr.base,Ye())}`;function Ue(){return mr}function X(e){return mr.features.includes(e)}function Bt(e,t){return typeof t!="undefined"?mr.translations[e].replace("#",t.toString()):mr.translations[e]}function ht(e,t=document){return G(`[data-md-component=${e}]`,t)}function Ee(e,t=document){return P(`[data-md-component=${e}]`,t)}function Bu(e){let t=G(".md-typeset > :first-child",e);return b(t,"click",{once:!0}).pipe(f(()=>G(".md-typeset",e)),f(r=>({hash:__md_hash(r.innerHTML)})))}function ls(e){if(!X("announce.dismiss")||!e.childElementCount)return y;if(!e.hidden){let t=G(".md-typeset",e);__md_hash(t.innerHTML)===__md_get("__announce")&&(e.hidden=!0)}return j(()=>{let t=new I;return t.subscribe(({hash:r})=>{e.hidden=!0,__md_set("__announce",r)}),Bu(e).pipe($(r=>t.next(r)),V(()=>t.complete()),f(r=>H({ref:e},r)))})}function Yu(e,{target$:t}){return t.pipe(f(r=>({hidden:r!==e})))}function us(e,t){let r=new I;return r.subscribe(({hidden:n})=>{e.hidden=n}),Yu(e,t).pipe($(n=>r.next(n)),V(()=>r.complete()),f(n=>H({ref:e},n)))}function To(e,t){return t==="inline"?A("div",{class:"md-tooltip md-tooltip--inline",id:e,role:"tooltip"},A("div",{class:"md-tooltip__inner md-typeset"})):A("div",{class:"md-tooltip",id:e,role:"tooltip"},A("div",{class:"md-tooltip__inner md-typeset"}))}function On(...e){return A("div",{class:"md-tooltip2",role:"dialog"},A("div",{class:"md-tooltip2__inner md-typeset"},e))}function ps(...e){return A("div",{class:"md-tooltip2",role:"tooltip"},A("div",{class:"md-tooltip2__inner md-typeset"},e))}function fs(e,t){if(t=t?`${t}_annotation_${e}`:void 0,t){let r=t?`#${t}`:void 0;return A("aside",{class:"md-annotation",tabIndex:0},To(t),A("a",{href:r,class:"md-annotation__index",tabIndex:-1},A("span",{"data-md-annotation-id":e})))}else return A("aside",{class:"md-annotation",tabIndex:0},To(t),A("span",{class:"md-annotation__index",tabIndex:-1},A("span",{"data-md-annotation-id":e})))}function ms(e){return A("button",{class:"md-code__button",title:Bt("clipboard.copy"),"data-clipboard-target":`#${e} > code`,"data-md-type":"copy"})}function ds(){return A("button",{class:"md-code__button",title:"Toggle line selection","data-md-type":"select"})}function hs(){return A("nav",{class:"md-code__nav"})}var Xu=_r(So());function bs(e){return A("ul",{class:"md-source__facts"},Object.entries(e).map(([t,r])=>A("li",{class:`md-source__fact md-source__fact--${t}`},typeof r=="number"?Li(r):r)))}function Oo(e){let t=`tabbed-control tabbed-control--${e}`;return A("div",{class:t,hidden:!0},A("button",{class:"tabbed-button",tabIndex:-1,"aria-hidden":"true"}))}function gs(e){return A("div",{class:"md-typeset__scrollwrap"},A("div",{class:"md-typeset__table"},e))}function Zu(e){var n;let t=Ue(),r=new URL(`../${e.version}/`,t.base);return A("li",{class:"md-version__item"},A("a",{href:`${r}`,class:"md-version__link"},e.title,((n=t.version)==null?void 0:n.alias)&&e.aliases.length>0&&A("span",{class:"md-version__alias"},e.aliases[0])))}function _s(e,t){var n;let r=Ue();return e=e.filter(o=>{var i;return!((i=o.properties)!=null&&i.hidden)}),A("div",{class:"md-version"},A("button",{class:"md-version__current","aria-label":Bt("select.version")},t.title,((n=r.version)==null?void 0:n.alias)&&t.aliases.length>0&&A("span",{class:"md-version__alias"},t.aliases[0])),A("ul",{class:"md-version__list"},e.map(Zu)))}var Qu=0;function ep(e,t=250){let r=re([ir(e),Ft(e,t)]).pipe(f(([o,i])=>o||i),ie()),n=j(()=>Ai(e)).pipe(oe(Ut),Lr(1),Ze(r),f(()=>Ci(e)));return r.pipe(Sr(o=>o),g(()=>re([r,n])),f(([o,i])=>({active:o,offset:i})),xe())}function Rr(e,t,r=250){let{content$:n,viewport$:o}=t,i=`__tooltip2_${Qu++}`;return j(()=>{let a=new I,s=new Un(!1);a.pipe(he(),ye(!1)).subscribe(s);let c=s.pipe(Tr(u=>Ve(+!u*250,Wn)),ie(),g(u=>u?n:y),$(u=>u.id=i),xe());re([a.pipe(f(({active:u})=>u)),c.pipe(g(u=>Ft(u,250)),J(!1))]).pipe(f(u=>u.some(p=>p))).subscribe(s);let l=s.pipe(L(u=>u),pe(c,o),f(([u,p,{size:d}])=>{let m=e.getBoundingClientRect(),h=m.width/2;if(p.role==="tooltip")return{x:h,y:8+m.height};if(m.y>=d.height/2){let{height:v}=Ae(p);return{x:h,y:-16-v}}else return{x:h,y:16+m.height}}));return re([c,a,l]).subscribe(([u,{offset:p},d])=>{u.style.setProperty("--md-tooltip-host-x",`${p.x}px`),u.style.setProperty("--md-tooltip-host-y",`${p.y}px`),u.style.setProperty("--md-tooltip-x",`${d.x}px`),u.style.setProperty("--md-tooltip-y",`${d.y}px`),u.classList.toggle("md-tooltip2--top",d.y<0),u.classList.toggle("md-tooltip2--bottom",d.y>=0)}),s.pipe(L(u=>u),pe(c,(u,p)=>p),L(u=>u.role==="tooltip")).subscribe(u=>{let p=Ae(G(":scope > *",u));u.style.setProperty("--md-tooltip-width",`${p.width}px`),u.style.setProperty("--md-tooltip-tail","0px")}),s.pipe(ie(),Ie(je),pe(c)).subscribe(([u,p])=>{p.classList.toggle("md-tooltip2--active",u)}),re([s.pipe(L(u=>u)),c]).subscribe(([u,p])=>{p.role==="dialog"?(e.setAttribute("aria-controls",i),e.setAttribute("aria-haspopup","dialog")):e.setAttribute("aria-describedby",i)}),s.pipe(L(u=>!u)).subscribe(()=>{e.removeAttribute("aria-controls"),e.removeAttribute("aria-describedby"),e.removeAttribute("aria-haspopup")}),ep(e,r).pipe($(u=>a.next(u)),V(()=>a.complete()),f(u=>H({ref:e},u)))})}function Ge(e,{viewport$:t},r=document.body){return Rr(e,{content$:new U(n=>{let o=e.title,i=ps(o);return n.next(i),e.removeAttribute("title"),r.append(i),()=>{i.remove(),e.setAttribute("title",o)}}),viewport$:t},0)}function tp(e,t){let r=j(()=>re([Hi(e),Ut(t)])).pipe(f(([{x:n,y:o},i])=>{let{width:a,height:s}=Ae(e);return{x:n-i.x+a/2,y:o-i.y+s/2}}));return ir(e).pipe(g(n=>r.pipe(f(o=>({active:n,offset:o})),Me(+!n||1/0))))}function ys(e,t,{target$:r}){let[n,o]=Array.from(e.children);return j(()=>{let i=new I,a=i.pipe(he(),ye(!0));return i.subscribe({next({offset:s}){e.style.setProperty("--md-tooltip-x",`${s.x}px`),e.style.setProperty("--md-tooltip-y",`${s.y}px`)},complete(){e.style.removeProperty("--md-tooltip-x"),e.style.removeProperty("--md-tooltip-y")}}),Et(e).pipe(Q(a)).subscribe(s=>{e.toggleAttribute("data-md-visible",s)}),R(i.pipe(L(({active:s})=>s)),i.pipe(Be(250),L(({active:s})=>!s))).subscribe({next({active:s}){s?e.prepend(n):n.remove()},complete(){e.prepend(n)}}),i.pipe(Xe(16,je)).subscribe(({active:s})=>{n.classList.toggle("md-tooltip--active",s)}),i.pipe(Lr(125,je),L(()=>!!e.offsetParent),f(()=>e.offsetParent.getBoundingClientRect()),f(({x:s})=>s)).subscribe({next(s){s?e.style.setProperty("--md-tooltip-0",`${-s}px`):e.style.removeProperty("--md-tooltip-0")},complete(){e.style.removeProperty("--md-tooltip-0")}}),b(o,"click").pipe(Q(a),L(s=>!(s.metaKey||s.ctrlKey))).subscribe(s=>{s.stopPropagation(),s.preventDefault()}),b(o,"mousedown").pipe(Q(a),pe(i)).subscribe(([s,{active:c}])=>{var l;if(s.button!==0||s.metaKey||s.ctrlKey)s.preventDefault();else if(c){s.preventDefault();let u=e.parentElement.closest(".md-annotation");u instanceof HTMLElement?u.focus():(l=xt())==null||l.blur()}}),r.pipe(Q(a),L(s=>s===n),It(125)).subscribe(()=>e.focus()),tp(e,t).pipe($(s=>i.next(s)),V(()=>i.complete()),f(s=>H({ref:e},s)))})}function rp(e){let t=Ue();if(e.tagName!=="CODE")return[e];let r=[".c",".c1",".cm"];if(t.annotate){let n=e.closest("[class|=language]");if(n)for(let o of Array.from(n.classList)){if(!o.startsWith("language-"))continue;let[,i]=o.split("-");i in t.annotate&&r.push(...t.annotate[i])}}return P(r.join(", "),e)}function np(e){let t=[];for(let r of rp(e)){let n=[],o=document.createNodeIterator(r,NodeFilter.SHOW_TEXT);for(let i=o.nextNode();i;i=o.nextNode())n.push(i);for(let i of n){let a;for(;a=/(\(\d+\))(!)?/.exec(i.textContent);){let[,s,c]=a;if(typeof c=="undefined"){let l=i.splitText(a.index);i=l.splitText(s.length),t.push(l)}else{i.textContent=s,t.push(i);break}}}}return t}function xs(e,t){t.append(...Array.from(e.childNodes))}function Ln(e,t,{target$:r,print$:n}){let o=t.closest("[id]"),i=o==null?void 0:o.id,a=new Map;for(let s of np(t)){let[,c]=s.textContent.match(/\((\d+)\)/);Le(`:scope > li:nth-child(${c})`,e)&&(a.set(c,fs(c,i)),s.replaceWith(a.get(c)))}return a.size===0?y:j(()=>{let s=new I,c=s.pipe(he(),ye(!0)),l=[];for(let[u,p]of a)l.push([G(".md-typeset",p),G(`:scope > li:nth-child(${u})`,e)]);return n.pipe(Q(c)).subscribe(u=>{e.hidden=!u,e.classList.toggle("md-annotation-list",u);for(let[p,d]of l)u?xs(p,d):xs(d,p)}),R(...[...a].map(([,u])=>ys(u,t,{target$:r}))).pipe(V(()=>s.complete()),xe())})}function ws(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return ws(t)}}function Es(e,t){return j(()=>{let r=ws(e);return typeof r!="undefined"?Ln(r,e,t):y})}var Ss=_r(Mo());var op=0,Ts=R(b(window,"keydown").pipe(f(()=>!0)),R(b(window,"keyup"),b(window,"contextmenu")).pipe(f(()=>!1))).pipe(J(!1),se(1));function Os(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return Os(t)}}function ip(e){return Re(e).pipe(f(({width:t})=>({scrollable:Mr(e).width>t})),fe("scrollable"))}function Ls(e,t){let{matches:r}=matchMedia("(hover)"),n=j(()=>{let o=new I,i=o.pipe(Gn(1));o.subscribe(({scrollable:m})=>{m&&r?e.setAttribute("tabindex","0"):e.removeAttribute("tabindex")});let a=[],s=e.closest("pre"),c=s.closest("[id]"),l=c?c.id:op++;s.id=`__code_${l}`;let u=[],p=e.closest(".highlight");if(p instanceof HTMLElement){let m=Os(p);if(typeof m!="undefined"&&(p.classList.contains("annotate")||X("content.code.annotate"))){let h=Ln(m,e,t);u.push(Re(p).pipe(Q(i),f(({width:v,height:x})=>v&&x),ie(),g(v=>v?h:y)))}}let d=P(":scope > span[id]",e);if(d.length&&(e.classList.add("md-code__content"),e.closest(".select")||X("content.code.select")&&!e.closest(".no-select"))){let m=+d[0].id.split("-").pop(),h=ds();a.push(h),X("content.tooltips")&&u.push(Ge(h,{viewport$}));let v=b(h,"click").pipe(Or(M=>!M,!1),$(()=>h.blur()),xe());v.subscribe(M=>{h.classList.toggle("md-code__button--active",M)});let x=me(d).pipe(oe(M=>Ft(M).pipe(f(O=>[M,O]))));v.pipe(g(M=>M?x:y)).subscribe(([M,O])=>{let N=Le(".hll.select",M);if(N&&!O)N.replaceWith(...Array.from(N.childNodes));else if(!N&&O){let ee=document.createElement("span");ee.className="hll select",ee.append(...Array.from(M.childNodes).slice(1)),M.append(ee)}});let w=me(d).pipe(oe(M=>b(M,"mousedown").pipe($(O=>O.preventDefault()),f(()=>M)))),E=v.pipe(g(M=>M?w:y),pe(Ts),f(([M,O])=>{var ee;let N=d.indexOf(M)+m;if(O===!1)return[N,N];{let le=P(".hll",e).map(ce=>d.indexOf(ce.parentElement)+m);return(ee=window.getSelection())==null||ee.removeAllRanges(),[Math.min(N,...le),Math.max(N,...le)]}})),_=_o(y).pipe(L(M=>M.startsWith(`__codelineno-${l}-`)));_.subscribe(M=>{let[,,O]=M.split("-"),N=O.split(":").map(le=>+le-m+1);N.length===1&&N.push(N[0]);for(let le of P(".hll:not(.select)",e))le.replaceWith(...Array.from(le.childNodes));let ee=d.slice(N[0]-1,N[1]);for(let le of ee){let ce=document.createElement("span");ce.className="hll",ce.append(...Array.from(le.childNodes).slice(1)),le.append(ce)}}),_.pipe(Me(1),Ie(ge)).subscribe(M=>{if(M.includes(":")){let O=document.getElementById(M.split(":")[0]);O&&setTimeout(()=>{let N=O,ee=-64;for(;N!==document.body;)ee+=N.offsetTop,N=N.offsetParent;window.scrollTo({top:ee})},1)}});let be=me(P('a[href^="#__codelineno"]',p)).pipe(oe(M=>b(M,"click").pipe($(O=>O.preventDefault()),f(()=>M)))).pipe(Q(i),pe(Ts),f(([M,O])=>{let ee=+G(`[id="${M.hash.slice(1)}"]`).parentElement.id.split("-").pop();if(O===!1)return[ee,ee];{let le=P(".hll",e).map(ce=>+ce.parentElement.id.split("-").pop());return[Math.min(ee,...le),Math.max(ee,...le)]}}));R(E,be).subscribe(M=>{let O=`#__codelineno-${l}-`;M[0]===M[1]?O+=M[0]:O+=`${M[0]}:${M[1]}`,history.replaceState({},"",O),window.dispatchEvent(new HashChangeEvent("hashchange",{newURL:window.location.origin+window.location.pathname+O,oldURL:window.location.href}))})}if(Ss.default.isSupported()&&(e.closest(".copy")||X("content.code.copy")&&!e.closest(".no-copy"))){let m=ms(s.id);a.push(m),X("content.tooltips")&&u.push(Ge(m,{viewport$}))}if(a.length){let m=hs();m.append(...a),s.insertBefore(m,e)}return ip(e).pipe($(m=>o.next(m)),V(()=>o.complete()),f(m=>H({ref:e},m)),Rt(R(...u).pipe(Q(i))))});return X("content.lazy")?Et(e).pipe(L(o=>o),Me(1),g(()=>n)):n}function ap(e,{target$:t,print$:r}){let n=!0;return R(t.pipe(f(o=>o.closest("details:not([open])")),L(o=>e===o),f(()=>({action:"open",reveal:!0}))),r.pipe(L(o=>o||!n),$(()=>n=e.open),f(o=>({action:o?"open":"close"}))))}function Ms(e,t){return j(()=>{let r=new I;return r.subscribe(({action:n,reveal:o})=>{e.toggleAttribute("open",n==="open"),o&&e.scrollIntoView()}),ap(e,t).pipe($(n=>r.next(n)),V(()=>r.complete()),f(n=>H({ref:e},n)))})}var ks=0,As=new Map;function sp(e){let t=document.createElement("h3");t.innerHTML=e.innerHTML;let r=[t],n=e.nextElementSibling;for(;n&&!(n instanceof HTMLHeadingElement);)r.push(n.cloneNode(!0)),n=n.nextElementSibling;return r}function cp(e,t){for(let r of P("[href], [src]",e))for(let n of["href","src"]){let o=r.getAttribute(n);if(o&&!/^(?:[a-z]+:)?\/\//i.test(o)){r[n]=new URL(r.getAttribute(n),t).toString();break}}for(let r of P("[name^=__], [for]",e))for(let n of["id","for","name"]){let o=r.getAttribute(n);o&&r.setAttribute(n,`${o}$preview_${ks}`)}return ks++,Y(e)}function lp(e){let t=As.get(e.toString());return t?Y(t):En(e).pipe(g(r=>cp(r,e)),f(r=>(As.set(e.toString(),r),r)))}function Cs(e,t){let{sitemap$:r}=t;if(!(e instanceof HTMLAnchorElement))return y;if(!(X("navigation.instant.preview")||e.hasAttribute("data-preview")))return y;e.removeAttribute("title");let n=re([ir(e),Ft(e).pipe(ke(1))]).pipe(f(([i,a])=>i||a),ie(),L(i=>i));return $t([r,n]).pipe(g(([i])=>{let a=new URL(e.href);return a.search=a.hash="",i.has(`${a}`)?Y(a):y}),g(i=>lp(i)),g(i=>{let a=e.hash?`article [id="${decodeURIComponent(e.hash.slice(1))}"]`:"article h1",s=Le(a,i);return typeof s=="undefined"?y:Y(sp(s))})).pipe(g(i=>{let a=new U(s=>{let c=On(...i);return s.next(c),document.body.append(c),()=>c.remove()});return Rr(e,H({content$:a},t))}))}var Hs=".node circle,.node ellipse,.node path,.node polygon,.node rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}marker{fill:var(--md-mermaid-edge-color)!important}.edgeLabel .label rect{fill:#0000}.flowchartTitleText{fill:var(--md-mermaid-label-fg-color)}.label{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.label foreignObject{line-height:normal;overflow:visible}.label div .edgeLabel{color:var(--md-mermaid-label-fg-color)}.edgeLabel,.edgeLabel p,.label div .edgeLabel{background-color:var(--md-mermaid-label-bg-color)}.edgeLabel,.edgeLabel p{fill:var(--md-mermaid-label-bg-color);color:var(--md-mermaid-edge-color)}.edgePath .path,.flowchart-link{stroke:var(--md-mermaid-edge-color)}.edgePath .arrowheadPath{fill:var(--md-mermaid-edge-color);stroke:none}.cluster rect{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}.cluster span{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}g #flowchart-circleEnd,g #flowchart-circleStart,g #flowchart-crossEnd,g #flowchart-crossStart,g #flowchart-pointEnd,g #flowchart-pointStart{stroke:none}.classDiagramTitleText{fill:var(--md-mermaid-label-fg-color)}g.classGroup line,g.classGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.classGroup text{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.classLabel .box{fill:var(--md-mermaid-label-bg-color);background-color:var(--md-mermaid-label-bg-color);opacity:1}.classLabel .label{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.node .divider{stroke:var(--md-mermaid-node-fg-color)}.relation{stroke:var(--md-mermaid-edge-color)}.cardinality{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.cardinality text{fill:inherit!important}defs marker.marker.composition.class path,defs marker.marker.dependency.class path,defs marker.marker.extension.class path{fill:var(--md-mermaid-edge-color)!important;stroke:var(--md-mermaid-edge-color)!important}defs marker.marker.aggregation.class path{fill:var(--md-mermaid-label-bg-color)!important;stroke:var(--md-mermaid-edge-color)!important}.statediagramTitleText{fill:var(--md-mermaid-label-fg-color)}g.stateGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.stateGroup .state-title{fill:var(--md-mermaid-label-fg-color)!important;font-family:var(--md-mermaid-font-family)}g.stateGroup .composit{fill:var(--md-mermaid-label-bg-color)}.nodeLabel,.nodeLabel p{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}a .nodeLabel{text-decoration:underline}.node circle.state-end,.node circle.state-start,.start-state{fill:var(--md-mermaid-edge-color);stroke:none}.end-state-inner,.end-state-outer{fill:var(--md-mermaid-edge-color)}.end-state-inner,.node circle.state-end{stroke:var(--md-mermaid-label-bg-color)}.transition{stroke:var(--md-mermaid-edge-color)}[id^=state-fork] rect,[id^=state-join] rect{fill:var(--md-mermaid-edge-color)!important;stroke:none!important}.statediagram-cluster.statediagram-cluster .inner{fill:var(--md-default-bg-color)}.statediagram-cluster rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.statediagram-state rect.divider{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}defs #statediagram-barbEnd{stroke:var(--md-mermaid-edge-color)}[id^=entity] path,[id^=entity] rect{fill:var(--md-default-bg-color)}.relationshipLine{stroke:var(--md-mermaid-edge-color)}defs .marker.oneOrMore.er *,defs .marker.onlyOne.er *,defs .marker.zeroOrMore.er *,defs .marker.zeroOrOne.er *{stroke:var(--md-mermaid-edge-color)!important}text:not([class]):last-child{fill:var(--md-mermaid-label-fg-color)}.actor{fill:var(--md-mermaid-sequence-actor-bg-color);stroke:var(--md-mermaid-sequence-actor-border-color)}text.actor>tspan{fill:var(--md-mermaid-sequence-actor-fg-color);font-family:var(--md-mermaid-font-family)}line{stroke:var(--md-mermaid-sequence-actor-line-color)}.actor-man circle,.actor-man line{fill:var(--md-mermaid-sequence-actorman-bg-color);stroke:var(--md-mermaid-sequence-actorman-line-color)}.messageLine0,.messageLine1{stroke:var(--md-mermaid-sequence-message-line-color)}.note{fill:var(--md-mermaid-sequence-note-bg-color);stroke:var(--md-mermaid-sequence-note-border-color)}.loopText,.loopText>tspan,.messageText,.noteText>tspan{stroke:none;font-family:var(--md-mermaid-font-family)!important}.messageText{fill:var(--md-mermaid-sequence-message-fg-color)}.loopText,.loopText>tspan{fill:var(--md-mermaid-sequence-loop-fg-color)}.noteText>tspan{fill:var(--md-mermaid-sequence-note-fg-color)}#arrowhead path{fill:var(--md-mermaid-sequence-message-line-color);stroke:none}.loopLine{fill:var(--md-mermaid-sequence-loop-bg-color);stroke:var(--md-mermaid-sequence-loop-border-color)}.labelBox{fill:var(--md-mermaid-sequence-label-bg-color);stroke:none}.labelText,.labelText>span{fill:var(--md-mermaid-sequence-label-fg-color);font-family:var(--md-mermaid-font-family)}.sequenceNumber{fill:var(--md-mermaid-sequence-number-fg-color)}rect.rect{fill:var(--md-mermaid-sequence-box-bg-color);stroke:none}rect.rect+text.text{fill:var(--md-mermaid-sequence-box-fg-color)}defs #sequencenumber{fill:var(--md-mermaid-sequence-number-bg-color)!important}";var ko,pp=0;function fp(){return typeof mermaid=="undefined"||mermaid instanceof Element?ar("https://unpkg.com/mermaid@11/dist/mermaid.min.js"):Y(void 0)}function $s(e){return e.classList.remove("mermaid"),ko||(ko=fp().pipe($(()=>mermaid.initialize({startOnLoad:!1,themeCSS:Hs,sequence:{actorFontSize:"16px",messageFontSize:"16px",noteFontSize:"16px"}})),f(()=>{}),se(1))),ko.subscribe(()=>Uo(null,null,function*(){e.classList.add("mermaid");let t=`__mermaid_${pp++}`,r=A("div",{class:"mermaid"}),n=e.textContent,{svg:o,fn:i}=yield mermaid.render(t,n),a=r.attachShadow({mode:"closed"});a.innerHTML=o,e.replaceWith(r),i==null||i(a)})),ko.pipe(f(()=>({ref:e})))}var Ps=A("table");function Is(e){return e.replaceWith(Ps),Ps.replaceWith(gs(e)),Y({ref:e})}function mp(e){let t=e.find(r=>r.checked)||e[0];return R(...e.map(r=>b(r,"change").pipe(f(()=>G(`label[for="${r.id}"]`))))).pipe(J(G(`label[for="${t.id}"]`)),f(r=>({active:r})))}function Rs(e,{viewport$:t,target$:r}){let n=G(".tabbed-labels",e),o=P(":scope > input",e),i=Oo("prev");e.append(i);let a=Oo("next");return e.append(a),j(()=>{let s=new I,c=s.pipe(he(),ye(!0));re([s,Re(e),Et(e)]).pipe(Q(c),Xe(1,je)).subscribe({next([{active:l},u]){let p=wt(l),{width:d}=Ae(l);e.style.setProperty("--md-indicator-x",`${p.x}px`),e.style.setProperty("--md-indicator-width",`${d}px`);let m=ln(n);(p.xm.x+u.width)&&n.scrollTo({left:Math.max(0,p.x-16),behavior:"smooth"})},complete(){e.style.removeProperty("--md-indicator-x"),e.style.removeProperty("--md-indicator-width")}}),re([Ut(n),Re(n)]).pipe(Q(c)).subscribe(([l,u])=>{let p=Mr(n);i.hidden=l.x<16,a.hidden=l.x>p.width-u.width-16}),R(b(i,"click").pipe(f(()=>-1)),b(a,"click").pipe(f(()=>1))).pipe(Q(c)).subscribe(l=>{let{width:u}=Ae(n);n.scrollBy({left:u*l,behavior:"smooth"})}),r.pipe(Q(c),L(l=>o.includes(l))).subscribe(l=>l.click()),n.classList.add("tabbed-labels--linked");for(let l of o){let u=G(`label[for="${l.id}"]`);u.replaceChildren(A("a",{href:`#${u.htmlFor}`,tabIndex:-1},...Array.from(u.childNodes))),b(u.firstElementChild,"click").pipe(Q(c),L(p=>!(p.metaKey||p.ctrlKey)),$(p=>{p.preventDefault(),p.stopPropagation()})).subscribe(()=>{history.replaceState({},"",`#${u.htmlFor}`),u.click()})}return X("content.tabs.link")&&s.pipe(ke(1),pe(t)).subscribe(([{active:l},{offset:u}])=>{let p=l.innerText.trim();if(l.hasAttribute("data-md-switching"))l.removeAttribute("data-md-switching");else{let d=e.offsetTop-u.y;for(let h of P("[data-tabs]"))for(let v of P(":scope > input",h)){let x=G(`label[for="${v.id}"]`);if(x!==l&&x.innerText.trim()===p){x.setAttribute("data-md-switching",""),v.click();break}}window.scrollTo({top:e.offsetTop-d});let m=__md_get("__tabs")||[];__md_set("__tabs",[...new Set([p,...m])])}}),s.pipe(Q(c)).subscribe(()=>{for(let l of P("audio, video",e))l.offsetWidth&&l.autoplay?l.play().catch(()=>{}):l.pause()}),mp(o).pipe($(l=>s.next(l)),V(()=>s.complete()),f(l=>H({ref:e},l)))}).pipe(Ht(ge))}function js(e,t){let{viewport$:r,target$:n,print$:o}=t;return R(...P(".annotate:not(.highlight)",e).map(i=>Es(i,{target$:n,print$:o})),...P("pre:not(.mermaid) > code",e).map(i=>Ls(i,{target$:n,print$:o})),...P("a",e).map(i=>Cs(i,t)),...P("pre.mermaid",e).map(i=>$s(i)),...P("table:not([class])",e).map(i=>Is(i)),...P("details",e).map(i=>Ms(i,{target$:n,print$:o})),...P("[data-tabs]",e).map(i=>Rs(i,{viewport$:r,target$:n})),...P("[title]:not([data-preview])",e).filter(()=>X("content.tooltips")).map(i=>Ge(i,{viewport$:r})),...P(".footnote-ref",e).filter(()=>X("content.footnote.tooltips")).map(i=>Rr(i,{content$:new U(a=>{let s=new URL(i.href).hash.slice(1),c=Array.from(document.getElementById(s).cloneNode(!0).children),l=On(...c);return a.next(l),document.body.append(l),()=>l.remove()}),viewport$:r})))}function dp(e,{alert$:t}){return t.pipe(g(r=>R(Y(!0),Y(!1).pipe(It(2e3))).pipe(f(n=>({message:r,active:n})))))}function Fs(e,t){let r=G(".md-typeset",e);return j(()=>{let n=new I;return n.subscribe(({message:o,active:i})=>{e.classList.toggle("md-dialog--active",i),r.textContent=o}),dp(e,t).pipe($(o=>n.next(o)),V(()=>n.complete()),f(o=>H({ref:e},o)))})}function hp({viewport$:e}){if(!X("header.autohide"))return Y(!1);let t=e.pipe(f(({offset:{y:o}})=>o),Pt(2,1),f(([o,i])=>[oMath.abs(i-o.y)>100),f(([,[o]])=>o),ie()),n=Tn("search");return re([e,n]).pipe(f(([{offset:o},i])=>o.y>400&&!i),ie(),g(o=>o?r:Y(!1)),J(!1))}function Us(e,t){return j(()=>re([Re(e),hp(t)])).pipe(f(([{height:r},n])=>({height:r,hidden:n})),ie((r,n)=>r.height===n.height&&r.hidden===n.hidden),se(1))}function Ns(e,{viewport$:t,header$:r,main$:n}){return j(()=>{let o=new I,i=o.pipe(he(),ye(!0));o.pipe(fe("active"),Ze(r)).subscribe(([{active:s},{hidden:c}])=>{e.classList.toggle("md-header--shadow",s&&!c),e.hidden=c});let a=me(P("[title]",e)).pipe(L(()=>X("content.tooltips")),oe(s=>Ge(s,{viewport$:t})));return n.subscribe(o),r.pipe(Q(i),f(s=>H({ref:e},s)),Rt(a.pipe(Q(i))))})}function vp(e,{viewport$:t,header$:r}){return Sn(e,{viewport$:t,header$:r}).pipe(f(({offset:{y:n}})=>{let{height:o}=Ae(e);return{active:o>0&&n>=o}}),fe("active"))}function Ds(e,t){return j(()=>{let r=new I;r.subscribe({next({active:o}){e.classList.toggle("md-header__title--active",o)},complete(){e.classList.remove("md-header__title--active")}});let n=Le(".md-content h1");return typeof n=="undefined"?y:vp(n,t).pipe($(o=>r.next(o)),V(()=>r.complete()),f(o=>H({ref:e},o)))})}function Ws(e,{viewport$:t,header$:r}){let n=r.pipe(f(({height:i})=>i),ie()),o=n.pipe(g(()=>Re(e).pipe(f(({height:i})=>({top:e.offsetTop,bottom:e.offsetTop+i})),fe("bottom"))));return re([n,o,t]).pipe(f(([i,{top:a,bottom:s},{offset:{y:c},size:{height:l}}])=>(l=Math.max(0,l-Math.max(0,a-c,i)-Math.max(0,l+c-s)),{offset:a-i,height:l,active:a-i<=c})),ie((i,a)=>i.offset===a.offset&&i.height===a.height&&i.active===a.active))}function bp(e){let t=__md_get("__palette")||{index:e.findIndex(n=>matchMedia(n.getAttribute("data-md-color-media")).matches)},r=Math.max(0,Math.min(t.index,e.length-1));return Y(...e).pipe(oe(n=>b(n,"change").pipe(f(()=>n))),J(e[r]),f(n=>({index:e.indexOf(n),color:{media:n.getAttribute("data-md-color-media"),scheme:n.getAttribute("data-md-color-scheme"),primary:n.getAttribute("data-md-color-primary"),accent:n.getAttribute("data-md-color-accent")}})),se(1))}function Vs(e){let t=P("input",e),r=A("meta",{name:"theme-color"});document.head.appendChild(r);let n=A("meta",{name:"color-scheme"});document.head.appendChild(n);let o=Ir("(prefers-color-scheme: light)");return j(()=>{let i=new I;return i.subscribe(a=>{if(document.body.setAttribute("data-md-color-switching",""),a.color.media==="(prefers-color-scheme)"){let s=matchMedia("(prefers-color-scheme: light)"),c=document.querySelector(s.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");a.color.scheme=c.getAttribute("data-md-color-scheme"),a.color.primary=c.getAttribute("data-md-color-primary"),a.color.accent=c.getAttribute("data-md-color-accent")}for(let[s,c]of Object.entries(a.color))document.body.setAttribute(`data-md-color-${s}`,c);for(let s=0;sa.key==="Enter"),pe(i,(a,s)=>s)).subscribe(({index:a})=>{a=(a+1)%t.length,t[a].click(),t[a].focus()}),i.pipe(f(()=>{let a=ht("header"),s=window.getComputedStyle(a);return n.content=s.colorScheme,s.backgroundColor.match(/\d+/g).map(c=>(+c).toString(16).padStart(2,"0")).join("")})).subscribe(a=>r.content=`#${a}`),i.pipe(Ie(ge)).subscribe(()=>{document.body.removeAttribute("data-md-color-switching")}),bp(t).pipe(Q(o.pipe(ke(1))),jt(),$(a=>i.next(a)),V(()=>i.complete()),f(a=>H({ref:e},a)))})}function zs(e,{progress$:t}){return j(()=>{let r=new I;return r.subscribe(({value:n})=>{e.style.setProperty("--md-progress-value",`${n}`)}),t.pipe($(n=>r.next({value:n})),V(()=>r.complete()),f(n=>({ref:e,value:n})))})}var qs='.v u{text-decoration:underline!important;text-decoration-style:wavy!important;text-decoration-thickness:1px!important}.p{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);background-color:rgba(var(--color-backdrop)/var(--alpha-lighter));cursor:pointer;height:100%;pointer-events:auto;position:absolute;transition:opacity .25s;width:100%}.p.m{opacity:0;pointer-events:none;transition:opacity .35s}.r{align-items:center;background-color:initial;border:none;border-radius:var(--space-2);cursor:pointer;display:flex;flex-shrink:0;font-family:var(--font-family);height:36px;justify-content:center;outline:none;padding:0;position:relative;transition:background-color .25s,color .25s;width:36px;z-index:1}.r svg{stroke:rgb(var(--color-foreground));height:18px;opacity:.5;width:18px}.r:before{background-color:rgb(var(--color-background-subtle));border-radius:var(--border-radius-2);content:"";inset:0;opacity:0;position:absolute;transform:scale(.75);transition:transform 125ms,opacity 125ms;z-index:0}.r:hover:before{opacity:1;transform:scale(1)}.r.c{cursor:auto}.r.c:before{display:none}.n{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);background-color:rgba(var(--color-background)/var(--alpha-light));border-radius:var(--space-3);box-shadow:0 0 60px #0000000d;display:flex;height:480px;overflow:hidden;pointer-events:auto;position:absolute;transition:transform .25s cubic-bezier(.16,1,.3,1),opacity .25s;width:640px}.n.l{opacity:0;pointer-events:none;transform:scale(1.1);transition:transform .25s .15s,opacity .15s}@media (max-width:680px){.n{border-radius:0;height:100%;width:100%}}.u{display:flex;flex-basis:min-content;flex-direction:column;flex-grow:1;flex-shrink:0}@keyframes d{0%{transform:scale(0)}50%{transform:scale(1.2)}to{transform:scale(1)}}.y{animation:d .25s ease-in-out;background:var(--color-highlight);border-radius:100%;color:#fff;font-size:8px;font-weight:700;height:12px;padding-top:1px;position:absolute;right:4px;top:4px;width:12px}.i{background-color:rgb(var(--color-background-subtle)/var(--alpha-lighter));flex-shrink:0;overflow:scroll;position:relative;transition:width .35s cubic-bezier(.16,1,.3,1),opacity .25s;width:200px}.i>*{transform:translate(0);transition:transform .25s cubic-bezier(.16,1,.3,1)}.i.l{opacity:0;width:0}.i.l>*{transform:translate(-48px)}@media (max-width:680px){.i{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);background-color:rgba(var(--color-background-subtle)/var(--alpha-light));box-shadow:0 0 60px #00000026;height:100%;position:absolute;right:0;top:0}}.w{border-bottom:1px solid rgb(var(--color-foreground)/var(--alpha-lightest));display:flex;gap:var(--space-1);padding:var(--space-2)}.k{-webkit-overflow-scrolling:touch;overflow:auto;overscroll-behavior:contain}.z{padding:8px 10px}.X{color:rgb(var(--color-foreground)/var(--alpha-light));padding:var(--space-2);position:absolute;width:200px}.X,.j{display:flex;flex-direction:column}.j{gap:2px;list-style:none;padding:0}.F,.j{margin:0}.F{font-size:16px;font-weight:400}.F,.I{padding:8px}.I{font-size:14px;margin:4px 0 0;opacity:.5}.I,.o{font-size:12px}.o{cursor:pointer;display:flex;padding:4px 8px;position:relative}.o:before{background-color:var(--color-highlight-transparent);border-radius:var(--space-1);content:"";inset:0;opacity:0;position:absolute;transform:scale(.75);transition:transform 125ms,opacity 125ms;z-index:0}.o.g:before,.o:hover:before{opacity:1;transform:scale(1)}.o.g,.o:hover{color:var(--color-highlight)}.R{flex-grow:1}.R,.q{position:relative}.q{font-weight:700}.f{flex-grow:1}.f input{background:#0000;border:none;color:rgb(var(--color-foreground));font-family:var(--font-family);font-size:16px;height:100%;letter-spacing:-.25px;outline:none;width:100%}.b{color:rgb(var(--color-foreground)/var(--alpha-light));display:flex;flex-direction:column;gap:2px;line-height:1.3;list-style:none;margin:var(--space-2);margin-top:0;padding:0}.A,.b li{margin:0}.A{color:rgb(var(--color-foreground)/var(--alpha-lighter));font-size:12px;margin-top:var(--space-2);padding:0 18px}.a{border-radius:var(--space-2);color:inherit;cursor:pointer;display:flex;flex-direction:row;flex-grow:1;padding:8px 10px;position:relative;text-decoration:none}.a:before{background-color:rgb(var(--color-background-subtle));border-radius:var(--border-radius-2);content:"";display:block;inset:0;opacity:0;position:absolute;transform:scale(.9);transition:transform 125ms,opacity 125ms;z-index:0}@media (pointer:fine){.a.h:before,.a:hover:before{opacity:1;transform:scale(1)}}.a mark{background:#0000;color:var(--color-highlight)}.a u{background-color:var(--color-highlight-transparent);border-radius:2px;box-shadow:0 0 0 1px var(--color-highlight-transparent);text-decoration:none}.B{flex-grow:1}.s{margin-right:-8px;opacity:0;position:relative;transform:translate(-2px);transition:transform 125ms,opacity 125ms;z-index:0}@media (pointer:fine){.h>.s,:hover>.s{opacity:1;transform:none}}.x{font-size:14px;margin:0;position:relative}.x code{background:rgb(var(--color-background-subtle));border-radius:var(--space-1);font-size:13px;padding:2px 4px}.t{color:rgb(var(--color-foreground)/var(--alpha-lighter));display:inline-flex;flex-wrap:wrap;font-size:12px;gap:var(--space-1);list-style:none;margin:0;padding:0;position:relative}.t li{white-space:nowrap}.t li:after{content:"/";display:inline;margin-left:var(--space-1)}.t li:last-child:after{content:"";display:none}.e{--space-1:4px;--space-2:calc(var(--space-1)*2);--space-3:calc(var(--space-2)*2);--space-4:calc(var(--space-3)*2);--space-5:calc(var(--space-4)*2);--alpha-light:.7;--alpha-lighter:.54;--alpha-lightest:.1;--color-highlight:var(--md-accent-fg-color,#526cfe);--color-highlight-transparent:var(--md-accent-fg-color--transparent,#526cfe1a);--border-radius-1:var(--space-1);--border-radius-2:var(--space-2);--border-radius-3:calc(var(--space-1) + var(--space-2));--font-family:var(--md-text-font-family,Inter,Roboto Flex,system-ui,sans-serif);--font-size:16px;--line-height:1.5;--letter-spacing:-.5px;-webkit-font-smoothing:antialiased;align-items:center;display:flex;font-family:var(--font-family);font-size:var(--font-size);height:100vh;justify-content:center;letter-spacing:var(--letter-spacing);line-height:var(--line-height);pointer-events:none;position:absolute;width:100vw}@media (pointer:coarse){.e{height:-webkit-fill-available}}.e *,.e :after,.e :before{box-sizing:border-box}';function Ks(e,{index$:t}){let r=Ue(),n=document.createElement("div");document.body.appendChild(n),n.style.position="fixed",n.style.height="100%",n.style.top="0",n.style.zIndex="4";let o=n.attachShadow({mode:"open"});o.appendChild(A("style",{},qs.toString()));try{Ya(r.search,{highlight:r.features.includes("search.highlight")}),me(t).subscribe(i=>{for(let a of i.items)a.location=new URL(a.location,r.base).toString();Ja(i,o)}),b(e,"click").subscribe(()=>{go()}),Tn("search").pipe(ke(1)).subscribe(()=>go())}catch(i){e.hidden=!0;let a=G("label[for=__search]");a.hidden=!0}return Ke}var Bs=_r(So());function Ys(e,{index$:t,location$:r}){return re([t,r.pipe(J(Ye()),L(n=>!!n.searchParams.get("h")))]).pipe(f(([n,o])=>_p(n.config)(o.searchParams.get("h"))),f(n=>{var a;let o=new Map,i=document.createNodeIterator(e,NodeFilter.SHOW_TEXT);for(let s=i.nextNode();s;s=i.nextNode())if((a=s.parentElement)!=null&&a.offsetHeight){let c=s.textContent,l=n(c);l.length>c.length&&o.set(s,l)}for(let[s,c]of o){let{childNodes:l}=A("span",null,c);s.replaceWith(...Array.from(l))}return{ref:e,nodes:o}}))}function _p(e){let t=e.separator.split("|").map(o=>o.replace(/(\(\?[!=<][^)]+\))/g,"").length===0?"\uFFFD":o).join("|"),r=new RegExp(t,"img"),n=(o,i,a)=>`${i}${a}`;return o=>{o=o.replace(/\s+/g," ").replace(/&/g,"&").trim();let i=new RegExp(`(^|${e.separator}|)(${o.split(r).map(a=>a.replace(/[|\\{}()[\]^$+*?.-]/g,"\\$&")).filter(a=>a.length>0).join("|")})`,"img");return a=>(0,Bs.default)(a).replace(i,n).replace(/<\/mark>(\s+)]*>/img,"$1")}}function yp(e,{viewport$:t,main$:r}){let n=e.closest(".md-grid"),o=n.offsetTop-n.parentElement.offsetTop;return re([r,t]).pipe(f(([{offset:i,height:a},{offset:{y:s}}])=>(a=a+Math.min(o,Math.max(0,s-i))-o,{height:a,locked:s>=i+o})),ie((i,a)=>i.height===a.height&&i.locked===a.locked))}function Ao(e,n){var o=n,{header$:t}=o,r=gr(o,["header$"]);let i=G(".md-sidebar__scrollwrap",e),{y:a}=wt(i);return j(()=>{let s=new I,c=s.pipe(he(),ye(!0)),l=s.pipe(Xe(0,je));return l.pipe(pe(t)).subscribe({next([{height:u},{height:p}]){i.style.height=`${u-2*a}px`,e.style.top=`${p}px`},complete(){i.style.height="",e.style.top=""}}),l.pipe(Sr()).subscribe(()=>{for(let u of P(".md-nav__link--active[href]",e)){if(!u.clientHeight)continue;let p=u.closest(".md-sidebar__scrollwrap");if(typeof p!="undefined"){let d=u.offsetTop-p.offsetTop,{height:m}=Ae(p);p.scrollTo({top:d-m/2})}}}),me(P("label[tabindex]",e)).pipe(oe(u=>b(u,"click").pipe(Ie(ge),f(()=>u),Q(c)))).subscribe(u=>{let p=G(`[id="${u.htmlFor}"]`);G(`[aria-labelledby="${u.id}"]`).setAttribute("aria-expanded",`${p.checked}`)}),X("content.tooltips")&&me(P("abbr[title]",e)).pipe(oe(u=>Ge(u,{viewport$})),Q(c)).subscribe(),yp(e,r).pipe($(u=>s.next(u)),V(()=>s.complete()),f(u=>H({ref:e},u)))})}function Gs(e,t){if(typeof t!="undefined"){let r=`https://api.github.com/repos/${e}/${t}`;return $t(et(`${r}/releases/latest`).pipe(_e(()=>y),f(n=>({version:n.tag_name})),ot({})),et(r).pipe(_e(()=>y),f(n=>({stars:n.stargazers_count,forks:n.forks_count})),ot({}))).pipe(f(([n,o])=>H(H({},n),o)))}else{let r=`https://api.github.com/users/${e}`;return et(r).pipe(f(n=>({repositories:n.public_repos})),ot({}))}}function Js(e,t){let r=`https://${e}/api/v4/projects/${encodeURIComponent(t)}`;return $t(et(`${r}/releases/permalink/latest`).pipe(_e(()=>y),f(({tag_name:n})=>({version:n})),ot({})),et(r).pipe(_e(()=>y),f(({star_count:n,forks_count:o})=>({stars:n,forks:o})),ot({}))).pipe(f(([n,o])=>H(H({},n),o)))}function Xs(e){let t=e.match(/^.+github\.com\/([^/]+)\/?([^/]+)?/i);if(t){let[,r,n]=t;return Gs(r,n)}if(t=e.match(/^.+?([^/]*gitlab[^/]+)\/(.+?)\/?$/i),t){let[,r,n]=t;return Js(r,n)}return y}var xp;function wp(e){return xp||(xp=j(()=>{let t=__md_get("__source",sessionStorage);if(t)return Y(t);if(Ee("consent").length){let n=__md_get("__consent");if(!(n&&n.github))return y}return Xs(e.href).pipe($(n=>__md_set("__source",n,sessionStorage)))}).pipe(_e(()=>y),L(t=>Object.keys(t).length>0),f(t=>({facts:t})),se(1)))}function Zs(e){let t=G(":scope > :last-child",e);return j(()=>{let r=new I;return r.subscribe(({facts:n})=>{t.appendChild(bs(n)),t.classList.add("md-source__repository--active")}),wp(e).pipe($(n=>r.next(n)),V(()=>r.complete()),f(n=>H({ref:e},n)))})}function Ep(e,{viewport$:t,header$:r}){return Re(document.body).pipe(g(()=>Sn(e,{header$:r,viewport$:t})),f(({offset:{y:n}})=>({hidden:n>=10})),fe("hidden"))}function Qs(e,t){return j(()=>{let r=new I;return r.subscribe({next({hidden:n}){e.hidden=n},complete(){e.hidden=!1}}),(X("navigation.tabs.sticky")?Y({hidden:!1}):Ep(e,t)).pipe($(n=>r.next(n)),V(()=>r.complete()),f(n=>H({ref:e},n)))})}function Tp(e,{viewport$:t,header$:r}){let n=new Map,o=P(".md-nav__link",e);for(let s of o){let c=decodeURIComponent(s.hash.substring(1)),l=Le(`[id="${c}"]`);typeof l!="undefined"&&n.set(s,l)}let i=r.pipe(fe("height"),f(({height:s})=>{let c=ht("main"),l=G(":scope > :first-child",c);return s+.9*(l.offsetTop-c.offsetTop)}),xe());return Re(document.body).pipe(fe("height"),g(s=>j(()=>{let c=[];return Y([...n].reduce((l,[u,p])=>{for(;c.length&&n.get(c[c.length-1]).tagName>=p.tagName;)c.pop();let d=p.offsetTop;for(;!d&&p.parentElement;)p=p.parentElement,d=p.offsetTop;let m=p.offsetParent;for(;m;m=m.offsetParent)d+=m.offsetTop;return l.set([...c=[...c,u]].reverse(),d)},new Map))}).pipe(f(c=>new Map([...c].sort(([,l],[,u])=>l-u))),Ze(i),g(([c,l])=>t.pipe(Or(([u,p],{offset:{y:d},size:m})=>{let h=d+m.height>=Math.floor(s.height);for(;p.length;){let[,v]=p[0];if(v-l=d&&!h)p=[u.pop(),...p];else break}return[u,p]},[[],[...c]]),ie((u,p)=>u[0]===p[0]&&u[1]===p[1])))))).pipe(f(([s,c])=>({prev:s.map(([l])=>l),next:c.map(([l])=>l)})),J({prev:[],next:[]}),Pt(2,1),f(([s,c])=>s.prev.length{let i=new I,a=i.pipe(he(),ye(!0));if(i.subscribe(({prev:s,next:c})=>{for(let[l]of c)l.classList.remove("md-nav__link--passed"),l.classList.remove("md-nav__link--active");for(let[l,[u]]of s.entries())u.classList.add("md-nav__link--passed"),u.classList.toggle("md-nav__link--active",l===s.length-1)}),X("toc.follow")){let s=R(t.pipe(Be(1),f(()=>{})),t.pipe(Be(250),f(()=>"smooth")));i.pipe(L(({prev:c})=>c.length>0),Ze(n.pipe(Ie(ge))),pe(s)).subscribe(([[{prev:c}],l])=>{let[u]=c[c.length-1];if(u.offsetHeight){let p=ki(u);if(typeof p!="undefined"){let d=u.offsetTop-p.offsetTop,{height:m}=Ae(p);p.scrollTo({top:d-m/2,behavior:l})}}})}return X("navigation.tracking")&&t.pipe(Q(a),fe("offset"),Be(250),ke(1),Q(o.pipe(ke(1))),jt({delay:250}),pe(i)).subscribe(([,{prev:s}])=>{let c=Ye(),l=s[s.length-1];if(l&&l.length){let[u]=l,{hash:p}=new URL(u.href);c.hash!==p&&(c.hash=p,history.replaceState({},"",`${c}`))}else c.hash="",history.replaceState({},"",`${c}`)}),Tp(e,{viewport$:t,header$:r}).pipe($(s=>i.next(s)),V(()=>i.complete()),f(s=>H({ref:e},s)))})}function Sp(e,{viewport$:t,main$:r,target$:n}){let o=t.pipe(f(({offset:{y:a}})=>a),Pt(2,1),f(([a,s])=>a>s&&s>0),ie()),i=r.pipe(f(({active:a})=>a));return re([i,o]).pipe(f(([a,s])=>!(a&&s)),ie(),Q(n.pipe(ke(1))),ye(!0),jt({delay:250}),f(a=>({hidden:a})))}function tc(e,{viewport$:t,header$:r,main$:n,target$:o}){let i=new I,a=i.pipe(he(),ye(!0));return i.subscribe({next({hidden:s}){e.hidden=s,s?(e.setAttribute("tabindex","-1"),e.blur()):e.removeAttribute("tabindex")},complete(){e.style.top="",e.hidden=!0,e.removeAttribute("tabindex")}}),r.pipe(Q(a),fe("height")).subscribe(({height:s})=>{e.style.top=`${s+16}px`}),b(e,"click").subscribe(s=>{s.preventDefault(),window.scrollTo({top:0})}),Sp(e,{viewport$:t,main$:n,target$:o}).pipe($(s=>i.next(s)),V(()=>i.complete()),f(s=>H({ref:e},s)))}function rc(e,t){return e.protocol=t.protocol,e.hostname=t.hostname,t.port&&(e.port=t.port),e}function Op(e,t){let r=new Map;for(let n of P("url",e)){let o=G("loc",n),i=[rc(new URL(o.textContent),t)];r.set(`${i[0]}`,i);for(let a of P("[rel=alternate]",n)){let s=a.getAttribute("href");s!=null&&i.push(rc(new URL(s),t))}}return r}function dr(e){return ns(new URL("sitemap.xml",e)).pipe(f(t=>Op(t,new URL(e))),_e(()=>Y(new Map)),xe())}function __ha_langroot(e){let t=new URL(e),r=t.pathname.match(/^\/(zh-hant|en|ja|ru)(?:\/|$)/);return t.pathname=r?`/${r[1]}/`:"/",t.search="",t.hash="",t}function nc({document$:e}){let t=new Map;e.pipe(g(()=>P("link[rel=alternate]")),f(r=>__ha_langroot(r.href)),L(r=>!t.has(r.toString())),oe(r=>dr(r).pipe(f(n=>[r,n]),_e(()=>y)))).subscribe(([r,n])=>{t.set(r.toString().replace(/\/$/,""),n)}),b(document.body,"click").pipe(L(r=>!r.metaKey&&!r.ctrlKey),g(r=>{if(r.target instanceof Element){let n=r.target.closest("a");if(n&&!n.target){let o=[...t].find(([p])=>n.href.startsWith(`${p}/`));if(typeof o=="undefined")return y;let[i,a]=o,s=Ye();if(s.href.startsWith(i))return y;let c=Ue(),l=s.href.replace(c.base,"");l=`${i}/${l}`;let u=a.has(l.split("#")[0])?new URL(l,c.base):new URL(i);return r.preventDefault(),Y(u)}}return y})).subscribe(r=>dt(r,!0))}var Co=_r(Mo());function Lp(e){e.setAttribute("data-md-copying","");let t=e.closest("[data-copy]"),r=t?t.getAttribute("data-copy"):e.innerText;return e.removeAttribute("data-md-copying"),r.trimEnd()}function oc({alert$:e}){Co.default.isSupported()&&new U(t=>{new Co.default("[data-clipboard-target], [data-clipboard-text]",{text:r=>r.getAttribute("data-clipboard-text")||Lp(G(r.getAttribute("data-clipboard-target")))}).on("success",r=>t.next(r))}).pipe($(t=>{t.trigger.focus()}),f(()=>Bt("clipboard.copied"))).subscribe(e)}function ic(e,t){if(!(e.target instanceof Element))return y;let r=e.target.closest("a");if(r===null)return y;if(r.target||e.metaKey||e.ctrlKey)return y;let n=new URL(r.href);return n.search=n.hash="",t.has(`${n}`)?(e.preventDefault(),Y(r)):y}function ac(e){let t=new Map;for(let r of P(":scope > *",e.head))t.set(r.outerHTML,r);return t}function sc(e){for(let t of P("[href], [src]",e))for(let r of["href","src"]){let n=t.getAttribute(r);if(n&&!/^(?:[a-z]+:)?\/\//i.test(n)){t[r]=t[r];break}}return Y(e)}function Mp(e){for(let n of["[data-md-component=announce]","[data-md-component=container]","[data-md-component=header-topic]","[data-md-component=outdated]","[data-md-component=logo]","[data-md-component=skip]",...X("navigation.tabs.sticky")?["[data-md-component=tabs]"]:[]]){let o=Le(n),i=Le(n,e);typeof o!="undefined"&&typeof i!="undefined"&&o.replaceWith(i)}let t=ac(document);for(let[n,o]of ac(e))t.has(n)?t.delete(n):document.head.appendChild(o);for(let n of t.values()){let o=n.getAttribute("name");o!=="theme-color"&&o!=="color-scheme"&&n.remove()}let r=ht("container");return nt(P("script",r)).pipe(g(n=>{let o=e.createElement("script");if(n.src){for(let i of n.getAttributeNames())o.setAttribute(i,n.getAttribute(i));return n.replaceWith(o),new U(i=>{o.onload=()=>i.complete()})}else return o.textContent=n.textContent,n.replaceWith(o),y}),he(),ye(document))}function cc({sitemap$:e,location$:t,viewport$:r,progress$:n}){if(location.protocol==="file:")return Ke;Y(document).subscribe(sc);let o=b(document.body,"click").pipe(Ze(e),g(([s,c])=>ic(s,c)),f(({href:s})=>new URL(s)),xe()),i=b(window,"popstate").pipe(f(Ye),xe());o.pipe(pe(r)).subscribe(([s,{offset:c}])=>{history.replaceState(c,""),history.pushState(null,"",s)}),R(o,i).subscribe(t);let a=t.pipe(fe("pathname"),g(s=>En(s,{progress$:n}).pipe(_e(()=>(dt(s,!0),y)))),g(sc),g(Mp),xe());return R(a.pipe(pe(t,(s,c)=>c)),a.pipe(g(()=>t),fe("hash")),t.pipe(ie((s,c)=>s.pathname===c.pathname&&s.hash===c.hash),g(()=>o),$(()=>history.back()))).subscribe(s=>{var c,l;history.state!==null||!s.hash?window.scrollTo(0,(l=(c=history.state)==null?void 0:c.y)!=null?l:0):(history.scrollRestoration="auto",es(s.hash),history.scrollRestoration="manual")}),t.subscribe(()=>{history.scrollRestoration="manual"}),b(window,"beforeunload").subscribe(()=>{history.scrollRestoration="auto"}),r.pipe(fe("offset"),Be(100)).subscribe(({offset:s})=>{history.replaceState(s,"")}),X("navigation.instant.prefetch")&&R(b(document.body,"mousemove"),b(document.body,"focusin")).pipe(Ze(e),g(([s,c])=>ic(s,c)),Be(25),Yn(({href:s})=>s),cn(s=>{let c=document.createElement("link");return c.rel="prefetch",c.href=s.toString(),document.head.appendChild(c),b(c,"load").pipe(f(()=>c),Me(1))})).subscribe(s=>s.remove()),a}function lc(e){var u;let{selectedVersionSitemap:t,selectedVersionBaseURL:r,currentLocation:n,currentBaseURL:o}=e,i=(u=Ho(o))==null?void 0:u.pathname;if(i===void 0)return;let a=kp(n.pathname,i);if(a===void 0)return;let s=Cp(t.keys());if(!t.has(s))return;let c=Ho(a,s);if(!c||!t.has(c.href))return;let l=Ho(a,r);if(l)return l.hash=n.hash,l.search=n.search,l}function Ho(e,t){try{return new URL(e,t)}catch(r){return}}function kp(e,t){if(e.startsWith(t))return e.slice(t.length)}function Ap(e,t){let r=Math.min(e.length,t.length),n;for(n=0;ny)),n=r.pipe(f(o=>{let[,i]=t.base.match(/([^/]+)\/?$/);return o.find(({version:a,aliases:s})=>a===i||s.includes(i))||o[0]}));r.pipe(f(o=>new Map(o.map(i=>[`${new URL(`../${i.version}/`,t.base)}`,i]))),g(o=>b(document.body,"click").pipe(L(i=>!i.metaKey&&!i.ctrlKey),pe(n),g(([i,a])=>{if(i.target instanceof Element){let s=i.target.closest("a");if(s&&!s.target&&o.has(s.href)){let c=s.href;return!i.target.closest(".md-version")&&o.get(c)===a?y:(i.preventDefault(),Y(new URL(c)))}}return y}),g(i=>dr(i).pipe(f(a=>{var s;return(s=lc({selectedVersionSitemap:a,selectedVersionBaseURL:i,currentLocation:Ye(),currentBaseURL:t.base}))!=null?s:i})))))).subscribe(o=>dt(o,!0)),re([r,n]).subscribe(([o,i])=>{G(".md-header__topic").appendChild(_s(o,i))}),e.pipe(g(()=>n)).subscribe(o=>{var s;let i=new URL(t.base),a=__md_get("__outdated",sessionStorage,i);if(a===null){a=!0;let c=((s=t.version)==null?void 0:s.default)||"latest";Array.isArray(c)||(c=[c]);e:for(let l of c)for(let u of o.aliases.concat(o.version))if(new RegExp(l,"i").test(u)){a=!1;break e}__md_set("__outdated",a,sessionStorage,i)}if(a)for(let c of Ee("outdated"))c.hidden=!1})}function pc({document$:e,viewport$:t}){e.pipe(g(()=>P(".md-ellipsis")),oe(r=>Et(r).pipe(Q(e.pipe(ke(1))),L(n=>n),f(()=>r),Me(1))),L(r=>r.offsetWidth{let n=r.innerText,o=r.closest("a")||r;return o.title=n,X("content.tooltips")?Ge(o,{viewport$:t}).pipe(Q(e.pipe(ke(1))),V(()=>o.removeAttribute("title"))):y})).subscribe(),X("content.tooltips")&&e.pipe(g(()=>P(".md-status")),oe(r=>Ge(r,{viewport$:t}))).subscribe()}function fc({document$:e,tablet$:t}){e.pipe(g(()=>P(".md-toggle--indeterminate")),$(r=>{r.indeterminate=!0,r.checked=!1}),oe(r=>b(r,"change").pipe(Xn(()=>r.classList.contains("md-toggle--indeterminate")),f(()=>r))),pe(t)).subscribe(([r,n])=>{r.classList.remove("md-toggle--indeterminate"),n&&(r.checked=!1)})}function Hp(){return/(iPad|iPhone|iPod)/.test(navigator.userAgent)}function mc({document$:e}){e.pipe(g(()=>P("[data-md-scrollfix]")),$(t=>t.removeAttribute("data-md-scrollfix")),L(Hp),oe(t=>b(t,"touchstart").pipe(f(()=>t)))).subscribe(t=>{let r=t.scrollTop;r===0?t.scrollTop=1:r+t.offsetHeight===t.scrollHeight&&(t.scrollTop=r-1)})}Object.entries||(Object.entries=function(e){let t=[];for(let r of Object.keys(e))t.push([r,e[r]]);return t});Object.values||(Object.values=function(e){let t=[];for(let r of Object.keys(e))t.push(e[r]);return t});typeof Element!="undefined"&&(Element.prototype.scrollTo||(Element.prototype.scrollTo=function(e,t){typeof e=="object"?(this.scrollLeft=e.left,this.scrollTop=e.top):(this.scrollLeft=e,this.scrollTop=t)}),Element.prototype.replaceWith||(Element.prototype.replaceWith=function(...e){let t=this.parentNode;if(t){e.length===0&&t.removeChild(this);for(let r=e.length-1;r>=0;r--){let n=e[r];typeof n=="string"?n=document.createTextNode(n):n.parentNode&&n.parentNode.removeChild(n),r?t.insertBefore(this.previousSibling,n):t.replaceChild(n,this)}}}));function $p(){return location.protocol==="file:"?ar(`${new URL("search.js",Mn.base)}`).pipe(f(()=>__index),_e(()=>Ke),se(1)):et(new URL("search.json",Mn.base))}document.documentElement.classList.remove("no-js");document.documentElement.classList.add("js");var vt=Si(),Ur=Za(),hr=ts(Ur),hc=Xa(),ze=cs(),$o=Ir("(min-width: 60em)"),vc=Ir("(min-width: 76.25em)"),bc=rs(),Mn=Ue(),gc=Le(".md-search")?$p():Ke,Po=new I;oc({alert$:Po});nc({document$:vt});var Io=new I,_c=dr(Mn.base);X("navigation.instant")&&cc({sitemap$:_c,location$:Ur,viewport$:ze,progress$:Io}).subscribe(vt);var dc;((dc=Mn.version)==null?void 0:dc.provider)==="mike"&&uc({document$:vt});R(Ur,hr).pipe(It(125)).subscribe(()=>{Eo("drawer",!1),Eo("search",!1)});hc.pipe(L(({mode:e,meta:t})=>e==="global"&&!t)).subscribe(e=>{switch(e.type){case",":case"p":let t=document.querySelector("link[rel=prev]");t instanceof HTMLLinkElement&&dt(t);break;case".":case"n":let r=document.querySelector("link[rel=next]");r instanceof HTMLLinkElement&&dt(r);break;case"/":let n=document.querySelector("[data-md-component=search] button");n instanceof HTMLButtonElement&&n.click();break;case"Enter":let o=xt();o instanceof HTMLLabelElement&&o.click()}});pc({viewport$:ze,document$:vt});fc({document$:vt,tablet$:$o});mc({document$:vt});var Lt=Us(ht("header"),{viewport$:ze}),Fr=vt.pipe(f(()=>ht("main")),g(e=>Ws(e,{viewport$:ze,header$:Lt})),se(1)),Pp=R(...Ee("consent").map(e=>us(e,{target$:hr})),...Ee("dialog").map(e=>Fs(e,{alert$:Po})),...Ee("palette").map(e=>Vs(e)),...Ee("progress").map(e=>zs(e,{progress$:Io})),...Ee("search").map(e=>Ks(e,{index$:gc})),...Ee("source").map(e=>Zs(e))),Ip=j(()=>R(...Ee("announce").map(e=>ls(e)),...Ee("content").map(e=>js(e,{sitemap$:_c,viewport$:ze,target$:hr,print$:bc})),...Ee("content").map(e=>X("search.highlight")?Ys(e,{index$:gc,location$:Ur}):y),...Ee("header").map(e=>Ns(e,{viewport$:ze,header$:Lt,main$:Fr})),...Ee("header-title").map(e=>Ds(e,{viewport$:ze,header$:Lt})),...Ee("sidebar").map(e=>e.getAttribute("data-md-type")==="navigation"?yo(vc,()=>Ao(e,{viewport$:ze,header$:Lt,main$:Fr})):yo($o,()=>Ao(e,{viewport$:ze,header$:Lt,main$:Fr}))),...Ee("tabs").map(e=>Qs(e,{viewport$:ze,header$:Lt})),...Ee("toc").map(e=>ec(e,{viewport$:ze,header$:Lt,main$:Fr,target$:hr})),...Ee("top").map(e=>tc(e,{viewport$:ze,header$:Lt,main$:Fr,target$:hr})))),yc=vt.pipe(g(()=>Ip),Rt(Pp),se(1));yc.subscribe();window.document$=vt;window.location$=Ur;window.target$=hr;window.keyboard$=hc;window.viewport$=ze;window.tablet$=$o;window.screen$=vc;window.print$=bc;window.alert$=Po;window.progress$=Io;window.component$=yc;})(); -/*! update cache: 20260410225926 */ +/*! update cache: 20260414173614 */ diff --git a/en/javascripts/animation_player.js b/en/javascripts/animation_player.js index 7937e3771..eaa98218e 100644 --- a/en/javascripts/animation_player.js +++ b/en/javascripts/animation_player.js @@ -251,4 +251,4 @@ initAutoSlide(); } })(); -/*! update cache: 20260410225926 */ +/*! update cache: 20260414173614 */ diff --git a/en/javascripts/katex.js b/en/javascripts/katex.js index 0045dab56..d2e640ee4 100644 --- a/en/javascripts/katex.js +++ b/en/javascripts/katex.js @@ -8,4 +8,4 @@ document$.subscribe(({ body }) => { ], }); }); -/*! update cache: 20260410225926 */ +/*! update cache: 20260414173614 */ diff --git a/en/javascripts/mathjax.js b/en/javascripts/mathjax.js index 68e87368e..3d1f5fee6 100644 --- a/en/javascripts/mathjax.js +++ b/en/javascripts/mathjax.js @@ -15,4 +15,4 @@ window.MathJax = { document$.subscribe(() => { MathJax.typesetPromise(); }); -/*! update cache: 20260410225926 */ +/*! update cache: 20260414173614 */ diff --git a/en/javascripts/starfield.js b/en/javascripts/starfield.js index f361d523c..6ad36e732 100644 --- a/en/javascripts/starfield.js +++ b/en/javascripts/starfield.js @@ -469,4 +469,4 @@ return Starfield; }); -/*! update cache: 20260410225926 */ +/*! update cache: 20260414173614 */ diff --git a/en/stylesheets/animation_player.css b/en/stylesheets/animation_player.css index ded2759bf..7535c40f0 100644 --- a/en/stylesheets/animation_player.css +++ b/en/stylesheets/animation_player.css @@ -176,4 +176,4 @@ font-size: 0.7rem; } } -/*! update cache: 20260410225926 */ +/*! update cache: 20260414173614 */ diff --git a/en/stylesheets/extra.css b/en/stylesheets/extra.css index 89f924b3e..fcf3f5573 100644 --- a/en/stylesheets/extra.css +++ b/en/stylesheets/extra.css @@ -806,4 +806,4 @@ a:hover .device-on-hover { margin: 0 0 1em; } } -/*! update cache: 20260410225926 */ +/*! update cache: 20260414173614 */ diff --git a/en/stylesheets/giscus-dark.css b/en/stylesheets/giscus-dark.css index d040e6e8d..99b84d5ef 100644 --- a/en/stylesheets/giscus-dark.css +++ b/en/stylesheets/giscus-dark.css @@ -122,4 +122,4 @@ main .gsc-loading-image { .gsc-reply-content::-webkit-scrollbar-track { background: transparent; } -/*! update cache: 20260410225926 */ +/*! update cache: 20260414173614 */ diff --git a/en/stylesheets/giscus-light.css b/en/stylesheets/giscus-light.css index a35697bbd..1cb54074a 100644 --- a/en/stylesheets/giscus-light.css +++ b/en/stylesheets/giscus-light.css @@ -153,4 +153,4 @@ main { .gsc-reply-content::-webkit-scrollbar-track { background: transparent; } -/*! update cache: 20260410225926 */ +/*! update cache: 20260414173614 */ diff --git a/ja/assets/javascripts/bundle.c2b142ea.min.js b/ja/assets/javascripts/bundle.c2b142ea.min.js index fa30b6163..4b1a64941 100644 --- a/ja/assets/javascripts/bundle.c2b142ea.min.js +++ b/ja/assets/javascripts/bundle.c2b142ea.min.js @@ -1,4 +1,4 @@ "use strict";(()=>{var xc=Object.create;var kn=Object.defineProperty,wc=Object.defineProperties,Ec=Object.getOwnPropertyDescriptor,Tc=Object.getOwnPropertyDescriptors,Sc=Object.getOwnPropertyNames,Dr=Object.getOwnPropertySymbols,Oc=Object.getPrototypeOf,An=Object.prototype.hasOwnProperty,Fo=Object.prototype.propertyIsEnumerable;var jo=(e,t,r)=>t in e?kn(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,H=(e,t)=>{for(var r in t||(t={}))An.call(t,r)&&jo(e,r,t[r]);if(Dr)for(var r of Dr(t))Fo.call(t,r)&&jo(e,r,t[r]);return e},He=(e,t)=>wc(e,Tc(t));var gr=(e,t)=>{var r={};for(var n in e)An.call(e,n)&&t.indexOf(n)<0&&(r[n]=e[n]);if(e!=null&&Dr)for(var n of Dr(e))t.indexOf(n)<0&&Fo.call(e,n)&&(r[n]=e[n]);return r};var Cn=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var Lc=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of Sc(t))!An.call(e,o)&&o!==r&&kn(e,o,{get:()=>t[o],enumerable:!(n=Ec(t,o))||n.enumerable});return e};var _r=(e,t,r)=>(r=e!=null?xc(Oc(e)):{},Lc(t||!e||!e.__esModule?kn(r,"default",{value:e,enumerable:!0}):r,e));var Uo=(e,t,r)=>new Promise((n,o)=>{var i=c=>{try{s(r.next(c))}catch(l){o(l)}},a=c=>{try{s(r.throw(c))}catch(l){o(l)}},s=c=>c.done?n(c.value):Promise.resolve(c.value).then(i,a);s((r=r.apply(e,t)).next())});var Do=Cn((Hn,No)=>{(function(e,t){typeof Hn=="object"&&typeof No!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(Hn,(function(){"use strict";function e(r){var n=!0,o=!1,i=null,a={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function s(_){return!!(_&&_!==document&&_.nodeName!=="HTML"&&_.nodeName!=="BODY"&&"classList"in _&&"contains"in _.classList)}function c(_){var de=_.type,be=_.tagName;return!!(be==="INPUT"&&a[de]&&!_.readOnly||be==="TEXTAREA"&&!_.readOnly||_.isContentEditable)}function l(_){_.classList.contains("focus-visible")||(_.classList.add("focus-visible"),_.setAttribute("data-focus-visible-added",""))}function u(_){_.hasAttribute("data-focus-visible-added")&&(_.classList.remove("focus-visible"),_.removeAttribute("data-focus-visible-added"))}function p(_){_.metaKey||_.altKey||_.ctrlKey||(s(r.activeElement)&&l(r.activeElement),n=!0)}function d(_){n=!1}function m(_){s(_.target)&&(n||c(_.target))&&l(_.target)}function h(_){s(_.target)&&(_.target.classList.contains("focus-visible")||_.target.hasAttribute("data-focus-visible-added"))&&(o=!0,window.clearTimeout(i),i=window.setTimeout(function(){o=!1},100),u(_.target))}function v(_){document.visibilityState==="hidden"&&(o&&(n=!0),x())}function x(){document.addEventListener("mousemove",E),document.addEventListener("mousedown",E),document.addEventListener("mouseup",E),document.addEventListener("pointermove",E),document.addEventListener("pointerdown",E),document.addEventListener("pointerup",E),document.addEventListener("touchmove",E),document.addEventListener("touchstart",E),document.addEventListener("touchend",E)}function w(){document.removeEventListener("mousemove",E),document.removeEventListener("mousedown",E),document.removeEventListener("mouseup",E),document.removeEventListener("pointermove",E),document.removeEventListener("pointerdown",E),document.removeEventListener("pointerup",E),document.removeEventListener("touchmove",E),document.removeEventListener("touchstart",E),document.removeEventListener("touchend",E)}function E(_){_.target.nodeName&&_.target.nodeName.toLowerCase()==="html"||(n=!1,w())}document.addEventListener("keydown",p,!0),document.addEventListener("mousedown",d,!0),document.addEventListener("pointerdown",d,!0),document.addEventListener("touchstart",d,!0),document.addEventListener("visibilitychange",v,!0),x(),r.addEventListener("focus",m,!0),r.addEventListener("blur",h,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)}))});var So=Cn((M0,vs)=>{"use strict";var Gu=/["'&<>]/;vs.exports=Ju;function Ju(e){var t=""+e,r=Gu.exec(t);if(!r)return t;var n,o="",i=0,a=0;for(i=r.index;i{(function(t,r){typeof jr=="object"&&typeof Lo=="object"?Lo.exports=r():typeof define=="function"&&define.amd?define([],r):typeof jr=="object"?jr.ClipboardJS=r():t.ClipboardJS=r()})(jr,function(){return(function(){var e={686:(function(n,o,i){"use strict";i.d(o,{default:function(){return vr}});var a=i(279),s=i.n(a),c=i(370),l=i.n(c),u=i(817),p=i.n(u);function d(B){try{return document.execCommand(B)}catch(C){return!1}}var m=function(C){var k=p()(C);return d("cut"),k},h=m;function v(B){var C=document.documentElement.getAttribute("dir")==="rtl",k=document.createElement("textarea");k.style.fontSize="12pt",k.style.border="0",k.style.padding="0",k.style.margin="0",k.style.position="absolute",k.style[C?"right":"left"]="-9999px";var D=window.pageYOffset||document.documentElement.scrollTop;return k.style.top="".concat(D,"px"),k.setAttribute("readonly",""),k.value=B,k}var x=function(C,k){var D=v(C);k.container.appendChild(D);var W=p()(D);return d("copy"),D.remove(),W},w=function(C){var k=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},D="";return typeof C=="string"?D=x(C,k):C instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(C==null?void 0:C.type)?D=x(C.value,k):(D=p()(C),d("copy")),D},E=w;function _(B){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?_=function(k){return typeof k}:_=function(k){return k&&typeof Symbol=="function"&&k.constructor===Symbol&&k!==Symbol.prototype?"symbol":typeof k},_(B)}var de=function(){var C=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},k=C.action,D=k===void 0?"copy":k,W=C.container,Z=C.target,We=C.text;if(D!=="copy"&&D!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(Z!==void 0)if(Z&&_(Z)==="object"&&Z.nodeType===1){if(D==="copy"&&Z.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(D==="cut"&&(Z.hasAttribute("readonly")||Z.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if(We)return E(We,{container:W});if(Z)return D==="cut"?h(Z):E(Z,{container:W})},be=de;function M(B){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?M=function(k){return typeof k}:M=function(k){return k&&typeof Symbol=="function"&&k.constructor===Symbol&&k!==Symbol.prototype?"symbol":typeof k},M(B)}function O(B,C){if(!(B instanceof C))throw new TypeError("Cannot call a class as a function")}function N(B,C){for(var k=0;k0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof W.action=="function"?W.action:this.defaultAction,this.target=typeof W.target=="function"?W.target:this.defaultTarget,this.text=typeof W.text=="function"?W.text:this.defaultText,this.container=M(W.container)==="object"?W.container:document.body}},{key:"listenClick",value:function(W){var Z=this;this.listener=l()(W,"click",function(We){return Z.onClick(We)})}},{key:"onClick",value:function(W){var Z=W.delegateTarget||W.currentTarget,We=this.action(Z)||"copy",Gt=be({action:We,container:this.container,target:this.target(Z),text:this.text(Z)});this.emit(Gt?"success":"error",{action:We,text:Gt,trigger:Z,clearSelection:function(){Z&&Z.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(W){return Yt("action",W)}},{key:"defaultTarget",value:function(W){var Z=Yt("target",W);if(Z)return document.querySelector(Z)}},{key:"defaultText",value:function(W){return Yt("text",W)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(W){var Z=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return E(W,Z)}},{key:"cut",value:function(W){return h(W)}},{key:"isSupported",value:function(){var W=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],Z=typeof W=="string"?[W]:W,We=!!document.queryCommandSupported;return Z.forEach(function(Gt){We=We&&!!document.queryCommandSupported(Gt)}),We}}]),k})(s()),vr=Mt}),828:(function(n){var o=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function a(s,c){for(;s&&s.nodeType!==o;){if(typeof s.matches=="function"&&s.matches(c))return s;s=s.parentNode}}n.exports=a}),438:(function(n,o,i){var a=i(828);function s(u,p,d,m,h){var v=l.apply(this,arguments);return u.addEventListener(d,v,h),{destroy:function(){u.removeEventListener(d,v,h)}}}function c(u,p,d,m,h){return typeof u.addEventListener=="function"?s.apply(null,arguments):typeof d=="function"?s.bind(null,document).apply(null,arguments):(typeof u=="string"&&(u=document.querySelectorAll(u)),Array.prototype.map.call(u,function(v){return s(v,p,d,m,h)}))}function l(u,p,d,m){return function(h){h.delegateTarget=a(h.target,p),h.delegateTarget&&m.call(u,h)}}n.exports=c}),879:(function(n,o){o.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},o.nodeList=function(i){var a=Object.prototype.toString.call(i);return i!==void 0&&(a==="[object NodeList]"||a==="[object HTMLCollection]")&&"length"in i&&(i.length===0||o.node(i[0]))},o.string=function(i){return typeof i=="string"||i instanceof String},o.fn=function(i){var a=Object.prototype.toString.call(i);return a==="[object Function]"}}),370:(function(n,o,i){var a=i(879),s=i(438);function c(d,m,h){if(!d&&!m&&!h)throw new Error("Missing required arguments");if(!a.string(m))throw new TypeError("Second argument must be a String");if(!a.fn(h))throw new TypeError("Third argument must be a Function");if(a.node(d))return l(d,m,h);if(a.nodeList(d))return u(d,m,h);if(a.string(d))return p(d,m,h);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function l(d,m,h){return d.addEventListener(m,h),{destroy:function(){d.removeEventListener(m,h)}}}function u(d,m,h){return Array.prototype.forEach.call(d,function(v){v.addEventListener(m,h)}),{destroy:function(){Array.prototype.forEach.call(d,function(v){v.removeEventListener(m,h)})}}}function p(d,m,h){return s(document.body,d,m,h)}n.exports=c}),817:(function(n){function o(i){var a;if(i.nodeName==="SELECT")i.focus(),a=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var s=i.hasAttribute("readonly");s||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),s||i.removeAttribute("readonly"),a=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var c=window.getSelection(),l=document.createRange();l.selectNodeContents(i),c.removeAllRanges(),c.addRange(l),a=c.toString()}return a}n.exports=o}),279:(function(n){function o(){}o.prototype={on:function(i,a,s){var c=this.e||(this.e={});return(c[i]||(c[i]=[])).push({fn:a,ctx:s}),this},once:function(i,a,s){var c=this;function l(){c.off(i,l),a.apply(s,arguments)}return l._=a,this.on(i,l,s)},emit:function(i){var a=[].slice.call(arguments,1),s=((this.e||(this.e={}))[i]||[]).slice(),c=0,l=s.length;for(c;c0&&i[i.length-1])&&(l[0]===6||l[0]===2)){r=0;continue}if(l[0]===3&&(!i||l[1]>i[0]&&l[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function te(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var n=r.call(e),o,i=[],a;try{for(;(t===void 0||t-- >0)&&!(o=n.next()).done;)i.push(o.value)}catch(s){a={error:s}}finally{try{o&&!o.done&&(r=n.return)&&r.call(n)}finally{if(a)throw a.error}}return i}function ne(e,t,r){if(r||arguments.length===2)for(var n=0,o=t.length,i;n1||c(m,v)})},h&&(o[m]=h(o[m])))}function c(m,h){try{l(n[m](h))}catch(v){d(i[0][3],v)}}function l(m){m.value instanceof kt?Promise.resolve(m.value.v).then(u,p):d(i[0][2],m)}function u(m){c("next",m)}function p(m){c("throw",m)}function d(m,h){m(h),i.shift(),i.length&&c(i[0][0],i[0][1])}}function zo(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],r;return t?t.call(e):(e=typeof $e=="function"?$e(e):e[Symbol.iterator](),r={},n("next"),n("throw"),n("return"),r[Symbol.asyncIterator]=function(){return this},r);function n(i){r[i]=e[i]&&function(a){return new Promise(function(s,c){a=e[i](a),o(s,c,a.done,a.value)})}}function o(i,a,s,c){Promise.resolve(c).then(function(l){i({value:l,done:s})},a)}}function F(e){return typeof e=="function"}function Jt(e){var t=function(n){Error.call(n),n.stack=new Error().stack},r=e(t);return r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r}var Vr=Jt(function(e){return function(r){e(this),this.message=r?r.length+` errors occurred during unsubscription: `+r.map(function(n,o){return o+1+") "+n.toString()}).join(` `):"",this.name="UnsubscriptionError",this.errors=r}});function ct(e,t){if(e){var r=e.indexOf(t);0<=r&&e.splice(r,1)}}var rt=(function(){function e(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var t,r,n,o,i;if(!this.closed){this.closed=!0;var a=this._parentage;if(a)if(this._parentage=null,Array.isArray(a))try{for(var s=$e(a),c=s.next();!c.done;c=s.next()){var l=c.value;l.remove(this)}}catch(v){t={error:v}}finally{try{c&&!c.done&&(r=s.return)&&r.call(s)}finally{if(t)throw t.error}}else a.remove(this);var u=this.initialTeardown;if(F(u))try{u()}catch(v){i=v instanceof Vr?v.errors:[v]}var p=this._finalizers;if(p){this._finalizers=null;try{for(var d=$e(p),m=d.next();!m.done;m=d.next()){var h=m.value;try{qo(h)}catch(v){i=i!=null?i:[],v instanceof Vr?i=ne(ne([],te(i)),te(v.errors)):i.push(v)}}}catch(v){n={error:v}}finally{try{m&&!m.done&&(o=d.return)&&o.call(d)}finally{if(n)throw n.error}}}if(i)throw new Vr(i)}},e.prototype.add=function(t){var r;if(t&&t!==this)if(this.closed)qo(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(r=this._finalizers)!==null&&r!==void 0?r:[]).push(t)}},e.prototype._hasParent=function(t){var r=this._parentage;return r===t||Array.isArray(r)&&r.includes(t)},e.prototype._addParent=function(t){var r=this._parentage;this._parentage=Array.isArray(r)?(r.push(t),r):r?[r,t]:t},e.prototype._removeParent=function(t){var r=this._parentage;r===t?this._parentage=null:Array.isArray(r)&&ct(r,t)},e.prototype.remove=function(t){var r=this._finalizers;r&&ct(r,t),t instanceof e&&t._removeParent(this)},e.EMPTY=(function(){var t=new e;return t.closed=!0,t})(),e})();var Pn=rt.EMPTY;function zr(e){return e instanceof rt||e&&"closed"in e&&F(e.remove)&&F(e.add)&&F(e.unsubscribe)}function qo(e){F(e)?e():e.unsubscribe()}var Je={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var Xt={setTimeout:function(e,t){for(var r=[],n=2;n0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var n=this,o=this,i=o.hasError,a=o.isStopped,s=o.observers;return i||a?Pn:(this.currentObservers=null,s.push(r),new rt(function(){n.currentObservers=null,ct(s,r)}))},t.prototype._checkFinalizedStatuses=function(r){var n=this,o=n.hasError,i=n.thrownError,a=n.isStopped;o?r.error(i):a&&r.complete()},t.prototype.asObservable=function(){var r=new U;return r.source=this,r},t.create=function(r,n){return new Qo(r,n)},t})(U);var Qo=(function(e){ue(t,e);function t(r,n){var o=e.call(this)||this;return o.destination=r,o.source=n,o}return t.prototype.next=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.next)===null||o===void 0||o.call(n,r)},t.prototype.error=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.error)===null||o===void 0||o.call(n,r)},t.prototype.complete=function(){var r,n;(n=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||n===void 0||n.call(r)},t.prototype._subscribe=function(r){var n,o;return(o=(n=this.source)===null||n===void 0?void 0:n.subscribe(r))!==null&&o!==void 0?o:Pn},t})(I);var Un=(function(e){ue(t,e);function t(r){var n=e.call(this)||this;return n._value=r,n}return Object.defineProperty(t.prototype,"value",{get:function(){return this.getValue()},enumerable:!1,configurable:!0}),t.prototype._subscribe=function(r){var n=e.prototype._subscribe.call(this,r);return!n.closed&&r.next(this._value),n},t.prototype.getValue=function(){var r=this,n=r.hasError,o=r.thrownError,i=r._value;if(n)throw o;return this._throwIfClosed(),i},t.prototype.next=function(r){e.prototype.next.call(this,this._value=r)},t})(I);var xr={now:function(){return(xr.delegate||Date).now()},delegate:void 0};var wr=(function(e){ue(t,e);function t(r,n,o){r===void 0&&(r=1/0),n===void 0&&(n=1/0),o===void 0&&(o=xr);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=n,i._timestampProvider=o,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=n===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,n),i}return t.prototype.next=function(r){var n=this,o=n.isStopped,i=n._buffer,a=n._infiniteTimeWindow,s=n._timestampProvider,c=n._windowTime;o||(i.push(r),!a&&i.push(s.now()+c)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var n=this._innerSubscribe(r),o=this,i=o._infiniteTimeWindow,a=o._buffer,s=a.slice(),c=0;c0?e.prototype.schedule.call(this,r,n):(this.delay=n,this.state=r,this.scheduler.flush(this),this)},t.prototype.execute=function(r,n){return n>0||this.closed?e.prototype.execute.call(this,r,n):this._execute(r,n)},t.prototype.requestAsyncId=function(r,n,o){return o===void 0&&(o=0),o!=null&&o>0||o==null&&this.delay>0?e.prototype.requestAsyncId.call(this,r,n,o):(r.flush(this),0)},t})(tr);var ri=(function(e){ue(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t})(rr);var Wn=new ri(ti);var ni=(function(e){ue(t,e);function t(r,n){var o=e.call(this,r,n)||this;return o.scheduler=r,o.work=n,o}return t.prototype.requestAsyncId=function(r,n,o){return o===void 0&&(o=0),o!==null&&o>0?e.prototype.requestAsyncId.call(this,r,n,o):(r.actions.push(this),r._scheduled||(r._scheduled=er.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,n,o){var i;if(o===void 0&&(o=0),o!=null?o>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,n,o);var a=r.actions;n!=null&&n===r._scheduled&&((i=a[a.length-1])===null||i===void 0?void 0:i.id)!==n&&(er.cancelAnimationFrame(n),r._scheduled=void 0)},t})(tr);var oi=(function(e){ue(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var n;r?n=r.id:(n=this._scheduled,this._scheduled=void 0);var o=this.actions,i;r=r||o.shift();do if(i=r.execute(r.state,r.delay))break;while((r=o[0])&&r.id===n&&o.shift());if(this._active=!1,i){for(;(r=o[0])&&r.id===n&&o.shift();)r.unsubscribe();throw i}},t})(rr);var je=new oi(ni);var y=new U(function(e){return e.complete()});function Br(e){return e&&F(e.schedule)}function Vn(e){return e[e.length-1]}function _t(e){return F(Vn(e))?e.pop():void 0}function qe(e){return Br(Vn(e))?e.pop():void 0}function Yr(e,t){return typeof Vn(e)=="number"?e.pop():t}var nr=(function(e){return e&&typeof e.length=="number"&&typeof e!="function"});function Gr(e){return F(e==null?void 0:e.then)}function Jr(e){return F(e[Qt])}function Xr(e){return Symbol.asyncIterator&&F(e==null?void 0:e[Symbol.asyncIterator])}function Zr(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function Rc(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var Qr=Rc();function en(e){return F(e==null?void 0:e[Qr])}function tn(e){return Vo(this,arguments,function(){var r,n,o,i;return Wr(this,function(a){switch(a.label){case 0:r=e.getReader(),a.label=1;case 1:a.trys.push([1,,9,10]),a.label=2;case 2:return[4,kt(r.read())];case 3:return n=a.sent(),o=n.value,i=n.done,i?[4,kt(void 0)]:[3,5];case 4:return[2,a.sent()];case 5:return[4,kt(o)];case 6:return[4,a.sent()];case 7:return a.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function rn(e){return F(e==null?void 0:e.getReader)}function q(e){if(e instanceof U)return e;if(e!=null){if(Jr(e))return jc(e);if(nr(e))return Fc(e);if(Gr(e))return Uc(e);if(Xr(e))return ii(e);if(en(e))return Nc(e);if(rn(e))return Dc(e)}throw Zr(e)}function jc(e){return new U(function(t){var r=e[Qt]();if(F(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function Fc(e){return new U(function(t){for(var r=0;r=2;return function(n){return n.pipe(e?L(function(o,i){return e(o,i,n)}):Oe,Me(1),r?ot(t):wi(function(){return new on}))}}function Gn(e){return e<=0?function(){return y}:S(function(t,r){var n=[];t.subscribe(T(r,function(o){n.push(o),e=2,!0))}function xe(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new I}:t,n=e.resetOnError,o=n===void 0?!0:n,i=e.resetOnComplete,a=i===void 0?!0:i,s=e.resetOnRefCountZero,c=s===void 0?!0:s;return function(l){var u,p,d,m=0,h=!1,v=!1,x=function(){p==null||p.unsubscribe(),p=void 0},w=function(){x(),u=d=void 0,h=v=!1},E=function(){var _=u;w(),_==null||_.unsubscribe()};return S(function(_,de){m++,!v&&!h&&x();var be=d=d!=null?d:r();de.add(function(){m--,m===0&&!v&&!h&&(p=Jn(E,c))}),be.subscribe(de),!u&&m>0&&(u=new Ct({next:function(M){return be.next(M)},error:function(M){v=!0,x(),p=Jn(w,o,M),be.error(M)},complete:function(){h=!0,x(),p=Jn(w,a),be.complete()}}),q(_).subscribe(u))})(l)}}function Jn(e,t){for(var r=[],n=2;ne.next(document)),e}function P(e,t=document){return Array.from(t.querySelectorAll(e))}function G(e,t=document){let r=Le(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function Le(e,t=document){return t.querySelector(e)||void 0}function xt(){var e,t,r,n;return(n=(r=(t=(e=document.activeElement)==null?void 0:e.shadowRoot)==null?void 0:t.activeElement)!=null?r:document.activeElement)!=null?n:void 0}var il=R(b(document.body,"focusin"),b(document.body,"focusout")).pipe(Be(1),J(void 0),f(()=>xt()||document.body),se(1));function ir(e){return il.pipe(f(t=>e.contains(t)),ie())}function Ft(e,t){let{matches:r}=matchMedia("(hover)");return j(()=>(r?R(b(e,"mouseenter").pipe(f(()=>!0)),b(e,"mouseleave").pipe(f(()=>!1))):R(b(e,"touchstart").pipe(f(()=>!0)),b(e,"touchend").pipe(f(()=>!1)),b(e,"touchcancel").pipe(f(()=>!1)))).pipe(t?Tr(o=>Ve(+!o*t)):Oe,J(!0,e.matches(":hover"))))}function Oi(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)Oi(e,r)}function A(e,t,...r){let n=document.createElement(e);if(t)for(let o of Object.keys(t))typeof t[o]!="undefined"&&(typeof t[o]!="boolean"?n.setAttribute(o,t[o]):n.setAttribute(o,""));for(let o of r)Oi(n,o);return n}function Li(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function ar(e){let t=A("script",{src:e});return j(()=>(document.head.appendChild(t),R(b(t,"load"),b(t,"error").pipe(g(()=>zn(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(f(()=>{}),V(()=>document.head.removeChild(t)),Me(1))))}var Mi=new I,al=j(()=>typeof ResizeObserver=="undefined"?ar("https://unpkg.com/resize-observer-polyfill"):Y(void 0)).pipe(f(()=>new ResizeObserver(e=>e.forEach(t=>Mi.next(t)))),g(e=>R(Ke,Y(e)).pipe(V(()=>e.disconnect()))),se(1));function Ae(e){return{width:e.offsetWidth,height:e.offsetHeight}}function Re(e){let t=e;for(;t.clientWidth===0&&t.parentElement;)t=t.parentElement;return al.pipe($(r=>r.observe(t)),g(r=>Mi.pipe(L(n=>n.target===t),V(()=>r.unobserve(t)))),f(()=>Ae(e)),J(Ae(e)))}function Mr(e){return{width:e.scrollWidth,height:e.scrollHeight}}function ki(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}function Ai(e){let t=[],r=e.parentElement;for(;r;)(e.clientWidth>r.clientWidth||e.clientHeight>r.clientHeight)&&t.push(r),r=(e=r).parentElement;return t.length===0&&t.push(document.documentElement),t}function wt(e){return{x:e.offsetLeft,y:e.offsetTop}}function Ci(e){let t=e.getBoundingClientRect();return{x:t.x+window.scrollX,y:t.y+window.scrollY}}function Hi(e){return R(b(window,"load"),b(window,"resize")).pipe(Xe(0,je),f(()=>wt(e)),J(wt(e)))}function ln(e){return{x:e.scrollLeft,y:e.scrollTop}}function Ut(e){return R(b(e,"scroll"),b(window,"scroll"),b(window,"resize")).pipe(Xe(0,je),f(()=>ln(e)),J(ln(e)))}var $i=new I,sl=j(()=>Y(new IntersectionObserver(e=>{for(let t of e)$i.next(t)},{threshold:0}))).pipe(g(e=>R(Ke,Y(e)).pipe(V(()=>e.disconnect()))),se(1));function Et(e){return sl.pipe($(t=>t.observe(e)),g(t=>$i.pipe(L(({target:r})=>r===e),V(()=>t.unobserve(e)),f(({isIntersecting:r})=>r))))}var cl=Object.create,la=Object.defineProperty,ll=Object.getOwnPropertyDescriptor,ul=Object.getOwnPropertyNames,pl=Object.getPrototypeOf,fl=Object.prototype.hasOwnProperty,ml=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),dl=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of ul(t))!fl.call(e,o)&&o!==r&&la(e,o,{get:()=>t[o],enumerable:!(n=ll(t,o))||n.enumerable});return e},hl=(e,t,r)=>(r=e!=null?cl(pl(e)):{},dl(t||!e||!e.__esModule?la(r,"default",{value:e,enumerable:!0}):r,e)),vl=ml((e,t)=>{var r="Expected a function",n=NaN,o="[object Symbol]",i=/^\s+|\s+$/g,a=/^[-+]0x[0-9a-f]+$/i,s=/^0b[01]+$/i,c=/^0o[0-7]+$/i,l=parseInt,u=typeof global=="object"&&global&&global.Object===Object&&global,p=typeof self=="object"&&self&&self.Object===Object&&self,d=u||p||Function("return this")(),m=Object.prototype,h=m.toString,v=Math.max,x=Math.min,w=function(){return d.Date.now()};function E(O,N,ee){var le,ce,Ne,bt,De,st,tt=0,Yt=!1,Mt=!1,vr=!0;if(typeof O!="function")throw new TypeError(r);N=M(N)||0,_(ee)&&(Yt=!!ee.leading,Mt="maxWait"in ee,Ne=Mt?v(M(ee.maxWait)||0,N):Ne,vr="trailing"in ee?!!ee.trailing:vr);function B(Te){var gt=le,br=ce;return le=ce=void 0,tt=Te,bt=O.apply(br,gt),bt}function C(Te){return tt=Te,De=setTimeout(W,N),Yt?B(Te):bt}function k(Te){var gt=Te-st,br=Te-tt,Ro=N-gt;return Mt?x(Ro,Ne-br):Ro}function D(Te){var gt=Te-st,br=Te-tt;return st===void 0||gt>=N||gt<0||Mt&&br>=Ne}function W(){var Te=w();if(D(Te))return Z(Te);De=setTimeout(W,k(Te))}function Z(Te){return De=void 0,vr&&le?B(Te):(le=ce=void 0,bt)}function We(){De!==void 0&&clearTimeout(De),tt=0,le=st=ce=De=void 0}function Gt(){return De===void 0?bt:Z(w())}function Nr(){var Te=w(),gt=D(Te);if(le=arguments,ce=this,st=Te,gt){if(De===void 0)return C(st);if(Mt)return De=setTimeout(W,N),B(st)}return De===void 0&&(De=setTimeout(W,N)),bt}return Nr.cancel=We,Nr.flush=Gt,Nr}function _(O){var N=typeof O;return!!O&&(N=="object"||N=="function")}function de(O){return!!O&&typeof O=="object"}function be(O){return typeof O=="symbol"||de(O)&&h.call(O)==o}function M(O){if(typeof O=="number")return O;if(be(O))return n;if(_(O)){var N=typeof O.valueOf=="function"?O.valueOf():O;O=_(N)?N+"":N}if(typeof O!="string")return O===0?O:+O;O=O.replace(i,"");var ee=s.test(O);return ee||c.test(O)?l(O.slice(2),ee?2:8):a.test(O)?n:+O}t.exports=E}),yn,K,ua,pa,Nt,Pi,fa,ma,da,lo,to,ro,bl,Ar={},ha=[],gl=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,Pr=Array.isArray;function pt(e,t){for(var r in t)e[r]=t[r];return e}function uo(e){e&&e.parentNode&&e.parentNode.removeChild(e)}function Wt(e,t,r){var n,o,i,a={};for(i in t)i=="key"?n=t[i]:i=="ref"?o=t[i]:a[i]=t[i];if(arguments.length>2&&(a.children=arguments.length>3?yn.call(arguments,2):r),typeof e=="function"&&e.defaultProps!=null)for(i in e.defaultProps)a[i]===void 0&&(a[i]=e.defaultProps[i]);return fn(e,a,n,o,null)}function fn(e,t,r,n,o){var i={type:e,props:t,key:r,ref:n,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:o!=null?o:++ua,__i:-1,__u:0};return o==null&&K.vnode!=null&&K.vnode(i),i}function ft(e){return e.children}function at(e,t){this.props=e,this.context=t}function cr(e,t){if(t==null)return e.__?cr(e.__,e.__i+1):null;for(var r;ts&&Nt.sort(ma),e=Nt.shift(),s=Nt.length,e.__d&&(r=void 0,n=void 0,o=(n=(t=e).__v).__e,i=[],a=[],t.__P&&((r=pt({},n)).__v=n.__v+1,K.vnode&&K.vnode(r),po(t.__P,r,n,t.__n,t.__P.namespaceURI,32&n.__u?[o]:null,i,o!=null?o:cr(n),!!(32&n.__u),a),r.__v=n.__v,r.__.__k[r.__i]=r,_a(i,r,a),n.__e=n.__=null,r.__e!=o&&va(r)));vn.__r=0}function ba(e,t,r,n,o,i,a,s,c,l,u){var p,d,m,h,v,x,w,E=n&&n.__k||ha,_=t.length;for(c=_l(r,t,E,c,_),p=0;p<_;p++)(m=r.__k[p])!=null&&(d=m.__i==-1?Ar:E[m.__i]||Ar,m.__i=p,x=po(e,m,d,o,i,a,s,c,l,u),h=m.__e,m.ref&&d.ref!=m.ref&&(d.ref&&fo(d.ref,null,m),u.push(m.ref,m.__c||h,m)),v==null&&h!=null&&(v=h),(w=!!(4&m.__u))||d.__k===m.__k?c=ga(m,c,e,w):typeof m.type=="function"&&x!==void 0?c=x:h&&(c=h.nextSibling),m.__u&=-7);return r.__e=v,c}function _l(e,t,r,n,o){var i,a,s,c,l,u=r.length,p=u,d=0;for(e.__k=new Array(o),i=0;i0?fn(a.type,a.props,a.key,a.ref?a.ref:null,a.__v):a).__=e,a.__b=e.__b+1,s=null,(l=a.__i=yl(a,r,c,p))!=-1&&(p--,(s=r[l])&&(s.__u|=2)),s==null||s.__v==null?(l==-1&&(o>u?d--:oc?d--:d++,a.__u|=4))):e.__k[i]=null;if(p)for(i=0;i(u?1:0)){for(o=r-1,i=r+1;o>=0||i=0?o--:i++])!=null&&!(2&l.__u)&&s==l.key&&c==l.type)return a}return-1}function Ri(e,t,r){t[0]=="-"?e.setProperty(t,r!=null?r:""):e[t]=r==null?"":typeof r!="number"||gl.test(t)?r:r+"px"}function un(e,t,r,n,o){var i,a;e:if(t=="style")if(typeof r=="string")e.style.cssText=r;else{if(typeof n=="string"&&(e.style.cssText=n=""),n)for(t in n)r&&t in r||Ri(e.style,t,"");if(r)for(t in r)n&&r[t]==n[t]||Ri(e.style,t,r[t])}else if(t[0]=="o"&&t[1]=="n")i=t!=(t=t.replace(da,"$1")),a=t.toLowerCase(),t=a in e||t=="onFocusOut"||t=="onFocusIn"?a.slice(2):t.slice(2),e.l||(e.l={}),e.l[t+i]=r,r?n?r.u=n.u:(r.u=lo,e.addEventListener(t,i?ro:to,i)):e.removeEventListener(t,i?ro:to,i);else{if(o=="http://www.w3.org/2000/svg")t=t.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if(t!="width"&&t!="height"&&t!="href"&&t!="list"&&t!="form"&&t!="tabIndex"&&t!="download"&&t!="rowSpan"&&t!="colSpan"&&t!="role"&&t!="popover"&&t in e)try{e[t]=r!=null?r:"";break e}catch(s){}typeof r=="function"||(r==null||r===!1&&t[4]!="-"?e.removeAttribute(t):e.setAttribute(t,t=="popover"&&r==1?"":r))}}function ji(e){return function(t){if(this.l){var r=this.l[t.type+e];if(t.t==null)t.t=lo++;else if(t.t0?e:Pr(e)?e.map(ya):pt({},e)}function xl(e,t,r,n,o,i,a,s,c){var l,u,p,d,m,h,v,x=r.props,w=t.props,E=t.type;if(E=="svg"?o="http://www.w3.org/2000/svg":E=="math"?o="http://www.w3.org/1998/Math/MathML":o||(o="http://www.w3.org/1999/xhtml"),i!=null){for(l=0;l=r.__.length&&r.__.push({}),r.__[e]}function bn(e){return $r=1,Tl(Ta,e)}function Tl(e,t,r){var n=mo(Hr++,2);if(n.t=e,!n.__c&&(n.__=[r?r(t):Ta(void 0,t),function(s){var c=n.__N?n.__N[0]:n.__[0],l=n.t(c,s);c!==l&&(n.__N=[l,n.__[1]],n.__c.setState({}))}],n.__c=ve,!ve.__f)){var o=function(s,c,l){if(!n.__c.__H)return!0;var u=n.__c.__H.__.filter(function(d){return!!d.__c});if(u.every(function(d){return!d.__N}))return!i||i.call(this,s,c,l);var p=n.__c.props!==s;return u.forEach(function(d){if(d.__N){var m=d.__[0];d.__=d.__N,d.__N=void 0,m!==d.__[0]&&(p=!0)}}),i&&i.call(this,s,c,l)||p};ve.__f=!0;var i=ve.shouldComponentUpdate,a=ve.componentWillUpdate;ve.componentWillUpdate=function(s,c,l){if(this.__e){var u=i;i=void 0,o(s,c,l),i=u}a&&a.call(this,s,c,l)},ve.shouldComponentUpdate=o}return n.__N||n.__}function mt(e,t){var r=mo(Hr++,3);!we.__s&&Ea(r.__H,t)&&(r.__=e,r.u=t,ve.__H.__h.push(r))}function Vt(e){return $r=5,ur(function(){return{current:e}},[])}function ur(e,t){var r=mo(Hr++,7);return Ea(r.__H,t)&&(r.__=e(),r.__H=t,r.__h=e),r.__}function Sl(e,t){return $r=8,ur(function(){return e},t)}function Ol(){for(var e;e=wa.shift();)if(e.__P&&e.__H)try{e.__H.__h.forEach(mn),e.__H.__h.forEach(oo),e.__H.__h=[]}catch(t){e.__H.__h=[],we.__e(t,e.__v)}}we.__b=function(e){ve=null,Ui&&Ui(e)},we.__=function(e,t){e&&t.__k&&t.__k.__m&&(e.__m=t.__k.__m),zi&&zi(e,t)},we.__r=function(e){Ni&&Ni(e),Hr=0;var t=(ve=e.__c).__H;t&&(Zn===ve?(t.__h=[],ve.__h=[],t.__.forEach(function(r){r.__N&&(r.__=r.__N),r.u=r.__N=void 0})):(t.__h.forEach(mn),t.__h.forEach(oo),t.__h=[],Hr=0)),Zn=ve},we.diffed=function(e){Di&&Di(e);var t=e.__c;t&&t.__H&&(t.__H.__h.length&&(wa.push(t)!==1&&Fi===we.requestAnimationFrame||((Fi=we.requestAnimationFrame)||Ll)(Ol)),t.__H.__.forEach(function(r){r.u&&(r.__H=r.u),r.u=void 0})),Zn=ve=null},we.__c=function(e,t){t.some(function(r){try{r.__h.forEach(mn),r.__h=r.__h.filter(function(n){return!n.__||oo(n)})}catch(n){t.some(function(o){o.__h&&(o.__h=[])}),t=[],we.__e(n,r.__v)}}),Wi&&Wi(e,t)},we.unmount=function(e){Vi&&Vi(e);var t,r=e.__c;r&&r.__H&&(r.__H.__.forEach(function(n){try{mn(n)}catch(o){t=o}}),r.__H=void 0,t&&we.__e(t,r.__v))};var qi=typeof requestAnimationFrame=="function";function Ll(e){var t,r=function(){clearTimeout(n),qi&&cancelAnimationFrame(t),setTimeout(e)},n=setTimeout(r,35);qi&&(t=requestAnimationFrame(r))}function mn(e){var t=ve,r=e.__c;typeof r=="function"&&(e.__c=void 0,r()),ve=t}function oo(e){var t=ve;e.__c=e.__(),ve=t}function Ea(e,t){return!e||e.length!==t.length||t.some(function(r,n){return r!==e[n]})}function Ta(e,t){return typeof t=="function"?t(e):t}function Ml(e,t){for(var r in t)e[r]=t[r];return e}function Ki(e,t){for(var r in e)if(r!=="__source"&&!(r in t))return!0;for(var n in t)if(n!=="__source"&&e[n]!==t[n])return!0;return!1}function Bi(e,t){this.props=e,this.context=t}(Bi.prototype=new at).isPureReactComponent=!0,Bi.prototype.shouldComponentUpdate=function(e,t){return Ki(this.props,e)||Ki(this.state,t)};var Yi=K.__b;K.__b=function(e){e.type&&e.type.__f&&e.ref&&(e.props.ref=e.ref,e.ref=null),Yi&&Yi(e)};var Yx=typeof Symbol<"u"&&Symbol.for&&Symbol.for("react.forward_ref")||3911,kl=K.__e;K.__e=function(e,t,r,n){if(e.then){for(var o,i=t;i=i.__;)if((o=i.__c)&&o.__c)return t.__e==null&&(t.__e=r.__e,t.__k=r.__k),o.__c(e,t)}kl(e,t,r,n)};var Gi=K.unmount;function Sa(e,t,r){return e&&(e.__c&&e.__c.__H&&(e.__c.__H.__.forEach(function(n){typeof n.__c=="function"&&n.__c()}),e.__c.__H=null),(e=Ml({},e)).__c!=null&&(e.__c.__P===r&&(e.__c.__P=t),e.__c.__e=!0,e.__c=null),e.__k=e.__k&&e.__k.map(function(n){return Sa(n,t,r)})),e}function Oa(e,t,r){return e&&r&&(e.__v=null,e.__k=e.__k&&e.__k.map(function(n){return Oa(n,t,r)}),e.__c&&e.__c.__P===t&&(e.__e&&r.appendChild(e.__e),e.__c.__e=!0,e.__c.__P=r)),e}function Qn(){this.__u=0,this.o=null,this.__b=null}function La(e){var t=e.__.__c;return t&&t.__a&&t.__a(e)}function pn(){this.i=null,this.l=null}K.unmount=function(e){var t=e.__c;t&&t.__R&&t.__R(),t&&32&e.__u&&(e.type=null),Gi&&Gi(e)},(Qn.prototype=new at).__c=function(e,t){var r=t.__c,n=this;n.o==null&&(n.o=[]),n.o.push(r);var o=La(n.__v),i=!1,a=function(){i||(i=!0,r.__R=null,o?o(s):s())};r.__R=a;var s=function(){if(!--n.__u){if(n.state.__a){var c=n.state.__a;n.__v.__k[0]=Oa(c,c.__c.__P,c.__c.__O)}var l;for(n.setState({__a:n.__b=null});l=n.o.pop();)l.forceUpdate()}};n.__u++||32&t.__u||n.setState({__a:n.__b=n.__v.__k[0]}),e.then(a,a)},Qn.prototype.componentWillUnmount=function(){this.o=[]},Qn.prototype.render=function(e,t){if(this.__b){if(this.__v.__k){var r=document.createElement("div"),n=this.__v.__k[0].__c;this.__v.__k[0]=Sa(this.__b,r,n.__O=n.__P)}this.__b=null}var o=t.__a&&Wt(ft,null,e.fallback);return o&&(o.__u&=-33),[Wt(ft,null,t.__a?null:e.children),o]};var Ji=function(e,t,r){if(++r[1]===r[0]&&e.l.delete(t),e.props.revealOrder&&(e.props.revealOrder[0]!=="t"||!e.l.size))for(r=e.i;r;){for(;r.length>3;)r.pop()();if(r[1]Object.freeze({get current(){return t.current}}),[])}var Nl=typeof globalThis<"u"&&typeof navigator<"u"&&typeof document<"u";function Dl(e,...t){var r;(r=e==null?void 0:e.addEventListener)==null||r.call(e,...t)}function Wl(e,...t){var r;(r=e==null?void 0:e.removeEventListener)==null||r.call(e,...t)}var Vl=(e,t)=>Object.hasOwn(e,t),zl=()=>!0,ql=()=>!1;function Kl(e=!1){let t=Vt(e),r=Sl(()=>t.current,[]);return mt(()=>(t.current=!0,()=>{t.current=!1}),[]),r}function Bl(e,...t){let r=Kl(),n=ka(t[1]),o=ur(()=>function(...i){r()&&(typeof n.current=="function"?n.current.apply(this,i):typeof n.current.handleEvent=="function"&&n.current.handleEvent.apply(this,i))},[]);mt(()=>{let i=Yl(e)?e.current:e;if(!i)return;let a=t.slice(2);return Dl(i,t[0],o,...a),()=>{Wl(i,t[0],o,...a)}},[e,t[0]])}function Yl(e){return e!==null&&typeof e=="object"&&Vl(e,"current")}var Gl=e=>typeof e=="function"?e:typeof e=="string"?t=>t.key===e:e?zl:ql,Jl=Nl?globalThis:null;function Aa(e,t,r=[],n={}){let{event:o="keydown",target:i=Jl,eventOptions:a}=n,s=ka(t),c=ur(()=>{let l=Gl(e);return function(u){l(u)&&s.current.call(this,u)}},r);Bl(i,o,c,a)}function Ca(e){var t,r,n="";if(typeof e=="string"||typeof e=="number")n+=e;else if(typeof e=="object")if(Array.isArray(e)){var o=e.length;for(t=0;t1)St--;else{for(var e,t=!1;kr!==void 0;){var r=kr;for(kr=void 0,io++;r!==void 0;){var n=r.o;if(r.o=void 0,r.f&=-3,!(8&r.f)&&Pa(r))try{r.c()}catch(o){t||(e=o,t=!0)}r=n}}if(io=0,St--,t)throw e}}function Ql(e){if(St>0)return e();St++;try{return e()}finally{xn()}}var ae=void 0;function Ha(e){var t=ae;ae=void 0;try{return e()}finally{ae=t}}var kr=void 0,St=0,io=0,gn=0;function $a(e){if(ae!==void 0){var t=e.n;if(t===void 0||t.t!==ae)return t={i:0,S:e,p:ae.s,n:void 0,t:ae,e:void 0,x:void 0,r:t},ae.s!==void 0&&(ae.s.n=t),ae.s=t,e.n=t,32&ae.f&&e.S(t),t;if(t.i===-1)return t.i=0,t.n!==void 0&&(t.n.p=t.p,t.p!==void 0&&(t.p.n=t.n),t.p=ae.s,t.n=void 0,ae.s.n=t,ae.s=t),t}}function Ce(e,t){this.v=e,this.i=0,this.n=void 0,this.t=void 0,this.W=t==null?void 0:t.watched,this.Z=t==null?void 0:t.unwatched,this.name=t==null?void 0:t.name}Ce.prototype.brand=Zl;Ce.prototype.h=function(){return!0};Ce.prototype.S=function(e){var t=this,r=this.t;r!==e&&e.e===void 0&&(e.x=r,this.t=e,r!==void 0?r.e=e:Ha(function(){var n;(n=t.W)==null||n.call(t)}))};Ce.prototype.U=function(e){var t=this;if(this.t!==void 0){var r=e.e,n=e.x;r!==void 0&&(r.x=n,e.e=void 0),n!==void 0&&(n.e=r,e.x=void 0),e===this.t&&(this.t=n,n===void 0&&Ha(function(){var o;(o=t.Z)==null||o.call(t)}))}};Ce.prototype.subscribe=function(e){var t=this;return qt(function(){var r=t.value,n=ae;ae=void 0;try{e(r)}finally{ae=n}},{name:"sub"})};Ce.prototype.valueOf=function(){return this.value};Ce.prototype.toString=function(){return this.value+""};Ce.prototype.toJSON=function(){return this.value};Ce.prototype.peek=function(){var e=ae;ae=void 0;try{return this.value}finally{ae=e}};Object.defineProperty(Ce.prototype,"value",{get:function(){var e=$a(this);return e!==void 0&&(e.i=this.i),this.v},set:function(e){if(e!==this.v){if(io>100)throw new Error("Cycle detected");this.v=e,this.i++,gn++,St++;try{for(var t=this.t;t!==void 0;t=t.x)t.t.N()}finally{xn()}}}});function Ot(e,t){return new Ce(e,t)}function Pa(e){for(var t=e.s;t!==void 0;t=t.n)if(t.S.i!==t.i||!t.S.h()||t.S.i!==t.i)return!0;return!1}function Ia(e){for(var t=e.s;t!==void 0;t=t.n){var r=t.S.n;if(r!==void 0&&(t.r=r),t.S.n=t,t.i=-1,t.n===void 0){e.s=t;break}}}function Ra(e){for(var t=e.s,r=void 0;t!==void 0;){var n=t.p;t.i===-1?(t.S.U(t),n!==void 0&&(n.n=t.n),t.n!==void 0&&(t.n.p=n)):r=t,t.S.n=t.r,t.r!==void 0&&(t.r=void 0),t=n}e.s=r}function Kt(e,t){Ce.call(this,void 0),this.x=e,this.s=void 0,this.g=gn-1,this.f=4,this.W=t==null?void 0:t.watched,this.Z=t==null?void 0:t.unwatched,this.name=t==null?void 0:t.name}Kt.prototype=new Ce;Kt.prototype.h=function(){if(this.f&=-3,1&this.f)return!1;if((36&this.f)==32||(this.f&=-5,this.g===gn))return!0;if(this.g=gn,this.f|=1,this.i>0&&!Pa(this))return this.f&=-2,!0;var e=ae;try{Ia(this),ae=this;var t=this.x();(16&this.f||this.v!==t||this.i===0)&&(this.v=t,this.f&=-17,this.i++)}catch(r){this.v=r,this.f|=16,this.i++}return ae=e,Ra(this),this.f&=-2,!0};Kt.prototype.S=function(e){if(this.t===void 0){this.f|=36;for(var t=this.s;t!==void 0;t=t.n)t.S.S(t)}Ce.prototype.S.call(this,e)};Kt.prototype.U=function(e){if(this.t!==void 0&&(Ce.prototype.U.call(this,e),this.t===void 0)){this.f&=-33;for(var t=this.s;t!==void 0;t=t.n)t.S.U(t)}};Kt.prototype.N=function(){if(!(2&this.f)){this.f|=6;for(var e=this.t;e!==void 0;e=e.x)e.t.N()}};Object.defineProperty(Kt.prototype,"value",{get:function(){if(1&this.f)throw new Error("Cycle detected");var e=$a(this);if(this.h(),e!==void 0&&(e.i=this.i),16&this.f)throw this.v;return this.v}});function ta(e,t){return new Kt(e,t)}function ja(e){var t=e.u;if(e.u=void 0,typeof t=="function"){St++;var r=ae;ae=void 0;try{t()}catch(n){throw e.f&=-2,e.f|=8,ho(e),n}finally{ae=r,xn()}}}function ho(e){for(var t=e.s;t!==void 0;t=t.n)t.S.U(t);e.x=void 0,e.s=void 0,ja(e)}function eu(e){if(ae!==this)throw new Error("Out-of-order effect");Ra(this),ae=e,this.f&=-2,8&this.f&&ho(this),xn()}function pr(e,t){this.x=e,this.u=void 0,this.s=void 0,this.o=void 0,this.f=32,this.name=t==null?void 0:t.name}pr.prototype.c=function(){var e=this.S();try{if(8&this.f||this.x===void 0)return;var t=this.x();typeof t=="function"&&(this.u=t)}finally{e()}};pr.prototype.S=function(){if(1&this.f)throw new Error("Cycle detected");this.f|=1,this.f&=-9,ja(this),Ia(this),St++;var e=ae;return ae=this,eu.bind(this,e)};pr.prototype.N=function(){2&this.f||(this.f|=2,this.o=kr,kr=this)};pr.prototype.d=function(){this.f|=8,1&this.f||ho(this)};pr.prototype.dispose=function(){this.d()};function qt(e,t){var r=new pr(e,t);try{r.c()}catch(o){throw r.d(),o}var n=r.d.bind(r);return n[Symbol.dispose]=n,n}var Fa,vo,eo,Ua=[];qt(function(){Fa=this.N})();function fr(e,t){K[e]=t.bind(null,K[e]||function(){})}function _n(e){eo&&eo(),eo=e&&e.S()}function Na(e){var t=this,r=e.data,n=ru(r);n.value=r;var o=ur(function(){for(var s=t,c=t.__v;c=c.__;)if(c.__c){c.__c.__$f|=4;break}var l=ta(function(){var m=n.value.value;return m===0?0:m===!0?"":m||""}),u=ta(function(){return!Array.isArray(l.value)&&!pa(l.value)}),p=qt(function(){if(this.N=Da,u.value){var m=l.value;s.__v&&s.__v.__e&&s.__v.__e.nodeType===3&&(s.__v.__e.data=m)}}),d=t.__$u.d;return t.__$u.d=function(){p(),d.call(this)},[u,l]},[]),i=o[0],a=o[1];return i.value?a.peek():a.value}Na.displayName="ReactiveTextNode";Object.defineProperties(Ce.prototype,{constructor:{configurable:!0,value:void 0},type:{configurable:!0,value:Na},props:{configurable:!0,get:function(){return{data:this}}},__b:{configurable:!0,value:1}});fr("__b",function(e,t){if(typeof t.type=="function"&&typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.exitComponent(),typeof t.type=="string"){var r,n=t.props;for(var o in n)if(o!=="children"){var i=n[o];i instanceof Ce&&(r||(t.__np=r={}),r[o]=i,n[o]=i.peek())}}e(t)});fr("__r",function(e,t){if(typeof t.type=="function"&&typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.enterComponent(t),t.type!==ft){_n();var r,n=t.__c;n&&(n.__$f&=-2,(r=n.__$u)===void 0&&(n.__$u=r=(function(o){var i;return qt(function(){i=this}),i.c=function(){n.__$f|=1,n.setState({})},i})())),vo=n,_n(r)}e(t)});fr("__e",function(e,t,r,n){typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.exitComponent(),_n(),vo=void 0,e(t,r,n)});fr("diffed",function(e,t){typeof t.type=="function"&&typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.exitComponent(),_n(),vo=void 0;var r;if(typeof t.type=="string"&&(r=t.__e)){var n=t.__np,o=t.props;if(n){var i=r.U;if(i)for(var a in i){var s=i[a];s!==void 0&&!(a in n)&&(s.d(),i[a]=void 0)}else i={},r.U=i;for(var c in n){var l=i[c],u=n[c];l===void 0?(l=tu(r,c,u,o),i[c]=l):l.o(u,o)}}}e(t)});function tu(e,t,r,n){var o=t in e&&e.ownerSVGElement===void 0,i=Ot(r);return{o:function(a,s){i.value=a,n=s},d:qt(function(){this.N=Da;var a=i.value.value;n[t]!==a&&(n[t]=a,o?e[t]=a:a?e.setAttribute(t,a):e.removeAttribute(t))})}}fr("unmount",function(e,t){if(typeof t.type=="string"){var r=t.__e;if(r){var n=r.U;if(n){r.U=void 0;for(var o in n){var i=n[o];i&&i.d()}}}}else{var a=t.__c;if(a){var s=a.__$u;s&&(a.__$u=void 0,s.d())}}e(t)});fr("__h",function(e,t,r,n){(n<3||n===9)&&(t.__$f|=2),e(t,r,n)});at.prototype.shouldComponentUpdate=function(e,t){var r=this.__$u,n=r&&r.s!==void 0;for(var o in t)return!0;if(this.__f||typeof this.u=="boolean"&&this.u===!0){var i=2&this.__$f;if(!(n||i||4&this.__$f)||1&this.__$f)return!0}else if(!(n||4&this.__$f)||3&this.__$f)return!0;for(var a in e)if(a!=="__source"&&e[a]!==this.props[a])return!0;for(var s in this.props)if(!(s in e))return!0;return!1};function ru(e,t){return bn(function(){return Ot(e,t)})[0]}var nu=function(e){queueMicrotask(function(){queueMicrotask(e)})};function ou(){Ql(function(){for(var e;e=Ua.shift();)Fa.call(e)})}function Da(){Ua.push(this)===1&&(K.requestAnimationFrame||nu)(ou)}var ao=[0];for(let e=0;e<32;e++)ao.push(ao[e]|1<>>5]>>>e&1}set(e){this.data[e>>>5]|=1<<(e&31)}forEach(e){let t=this.size&31;for(let r=0;r{var r;return(r=t.tags)==null?void 0:r.length})&&(matchMedia("(max-width: 768px)").matches||Wa())}function Dt(){Qe.value=He(H({},Qe.value),{hideSearch:!Qe.value.hideSearch})}function Wa(){Qe.value=He(H({},Qe.value),{hideFilters:!Qe.value.hideFilters})}function dn(){return Qe.value.selectedItem}function so(e){Qe.value=He(H({},Qe.value),{selectedItem:e})}function su(){var e,t;return(t=(e=lr.value)==null?void 0:e.items)!=null?t:[]}function wn(){return typeof Se.value.input=="string"?Se.value.input:""}function Va(e){let t=za();e.length&&!t.length?Se.value=He(H({},Se.value),{page:void 0,input:e}):!e.length&&t.length?Se.value=He(H({},Se.value),{page:void 0,input:{type:"operator",data:{operator:"not",operands:[]}}}):Se.value=He(H({},Se.value),{page:void 0,input:e})}function cu(){typeof it.value.pagination.next<"u"&&(Se.value=He(H({},Se.value),{page:it.value.pagination.next}))}function lu(e){let t=Se.value.filter.input;if("type"in t&&t.type==="operator"){for(let r of t.data.operands)if("type"in r&&r.type==="value"&&typeof r.data.value=="string"&&r.data.value===e)return!0}return!1}function za(){let e=Se.value.filter.input,t=[];if("type"in e&&e.type==="operator")for(let r of e.data.operands)"type"in r&&r.type==="value"&&typeof r.data.value=="string"&&t.push(r.data.value);return t}function uu(e){let t=Se.value.filter.input,r=[];if("type"in t&&t.type==="operator")for(let n of t.data.operands)"type"in n&&n.type==="value"&&typeof n.data.value=="string"&&r.push(n.data.value);if(r.includes(e)){let n=r.indexOf(e);n>-1&&r.splice(n,1)}else r.push(e);Se.value=He(H({},Se.value),{page:void 0,filter:He(H({},Se.value.filter),{input:{type:"operator",data:{operator:"and",operands:r.map(n=>({type:"value",data:{field:"tags",value:n}}))}}})}),Va(wn())}function pu(){return it.value.items}function fu(){return it.value.total}function mu(){var e;for(let t of(e=it.value.aggregations)!=null?e:[])if(t.type==="term")return t.data.value;return[]}function sr(){return Qe.value.hideSearch}function du(){return Qe.value.hideFilters}function qa(){var e;return(e=Ka.value.highlight)!=null?e:!1}var Qe=Ot({hideSearch:!0,hideFilters:!0,selectedItem:0}),Ka=Ot({}),lr=Ot(),na=Ot(),Se=Ot({input:"",filter:{input:{type:"operator",data:{operator:"and",operands:[]}},aggregation:{input:[{type:"term",data:{field:"tags"}}]}}}),it=Ot({items:[],query:{select:{documents:new ra(0),terms:new ra(0)},values:[]},pagination:{total:0}});function hu(e,t,r){for(let n=0;tr&&t(0,o,r,r=i);continue;case 62:e.charCodeAt(r+1)===47?t(2,--o,r,r=i+1):hu(e,r,n)?t(3,o,r,r=i+1):t(1,o++,r,r=i+1)}i>r&&t(0,o,r,i)}function bu(e,t=0,r=e.length){let n=++t;e:for(let l=0;n{let i=[],a=[],{onElement:s,onText:c=gu}=typeof r=="function"?{onElement:r}:r,l=0,u=0;return e(t,(p,d,m,h)=>{if(p===0)i[l++]=c(t,m,h),a[u++]={value:null,depth:d};else if(p&1&&(a[u++]={value:bu(t,m,h),depth:d}),p&2)for(let v=0;u>=0;v++){let{value:x,depth:w}=a[--u];if(w>d)continue;let E=i.slice(l-=v,l+v);i[l++]=s(x,E),u++;break}},n,o),i.slice(0,l)}}function yu(e){return e.replace(/[&<>]/g,t=>{switch(t.charCodeAt(0)){case 38:return"&";case 60:return"<";case 62:return">"}})}function hn(e){return e.replace(/&(amp|[lg]t);/g,t=>{switch(t.charCodeAt(1)){case 97:return"&";case 108:return"<";case 103:return">"}})}function xu(e,t){return{start:e.start+t,end:e.end+t,value:e.value}}function wu(e,t,r){return e.slice(t,r)}function Eu(e){let{onHighlight:t,onText:r=wu}=typeof e=="function"?{onHighlight:e}:e;return(n,o,i=0,a=n.length)=>{var l;let s=[],c=(l=o==null?void 0:o.ranges)!=null?l:[];for(let u=0,p=i;ua)break;let m=c[u].end;if(mi&&s.push(r(n,i,d));let{value:h}=c[u];s.push(t(n,{start:d,end:i=m,value:h}))}return i{let o=n.data;switch(o.type){case 1:na.value=!0;break;case 3:typeof o.data.pagination.prev<"u"?it.value=He(H({},it.value),{pagination:o.data.pagination,items:[...it.value.items,...o.data.items]}):(it.value=o.data,so(0));break}},qt(()=>{lr.value&&r.postMessage({type:0,data:lr.value})}),qt(()=>{na.value&&r.postMessage({type:2,data:Se.value})})}var oa={container:"p",hidden:"m"};function ku(e){return z("div",{class:zt(oa.container,{[oa.hidden]:e.hidden}),onClick:()=>Dt()})}var ia={container:"r",disabled:"c"};function co(e){return z("button",{class:zt(ia.container,{[ia.disabled]:!e.onClick}),onClick:e.onClick,children:e.children})}var aa=e=>e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase(),Au=e=>e.replace(/^([A-Z])|[\s-_]+(\w)/g,(t,r,n)=>n?n.toUpperCase():r.toLowerCase()),sa=e=>{let t=Au(e);return t.charAt(0).toUpperCase()+t.slice(1)},Cu=(...e)=>e.filter((t,r,n)=>!!t&&t.trim()!==""&&n.indexOf(t)===r).join(" ").trim(),Hu={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"},$u=c=>{var l=c,{color:e="currentColor",size:t=24,strokeWidth:r=2,absoluteStrokeWidth:n,children:o,iconNode:i,class:a=""}=l,s=gr(l,["color","size","strokeWidth","absoluteStrokeWidth","children","iconNode","class"]);return Wt("svg",H(He(H({},Hu),{width:String(t),height:t,stroke:e,"stroke-width":n?Number(r)*24/Number(t):r,class:["lucide",a].join(" ")}),s),[...i.map(([u,p])=>Wt(u,p)),...Cr(o)])},bo=(e,t)=>{let r=a=>{var s=a,{class:n="",children:o}=s,i=gr(s,["class","children"]);return Wt($u,He(H({},i),{iconNode:t,class:Cu(`lucide-${aa(sa(e))}`,`lucide-${aa(e)}`,n)}),o)};return r.displayName=sa(e),r},Pu=bo("corner-down-left",[["path",{d:"M20 4v7a4 4 0 0 1-4 4H4",key:"6o5b7l"}],["path",{d:"m9 10-5 5 5 5",key:"1kshq7"}]]),Iu=bo("list-filter",[["path",{d:"M2 5h20",key:"1fs1ex"}],["path",{d:"M6 12h12",key:"8npq4p"}],["path",{d:"M9 19h6",key:"456am0"}]]),Ru=bo("search",[["path",{d:"m21 21-4.34-4.34",key:"14j7rj"}],["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}]]),Gx=hl(vl(),1);function ju({threshold:e=0,root:t=null,rootMargin:r="0%",freezeOnceVisible:n=!1,initialIsIntersecting:o=!1,onChange:i}={}){var a;let[s,c]=bn(null),[l,u]=bn(()=>({isIntersecting:o,entry:void 0})),p=Vt();p.current=i;let d=((a=l.entry)==null?void 0:a.isIntersecting)&&n;mt(()=>{if(!s||!("IntersectionObserver"in window)||d)return;let v,x=new IntersectionObserver(w=>{let E=Array.isArray(x.thresholds)?x.thresholds:[x.thresholds];w.forEach(_=>{let de=_.isIntersecting&&E.some(be=>_.intersectionRatio>=be);u({isIntersecting:de,entry:_}),p.current&&p.current(de,_),de&&n&&v&&(v(),v=void 0)})},{threshold:e,root:t,rootMargin:r});return x.observe(s),()=>{x.disconnect()}},[s,JSON.stringify(e),t,r,d,n]);let m=Vt(null);mt(()=>{var v;!s&&(v=l.entry)!=null&&v.target&&!n&&!d&&m.current!==l.entry.target&&(m.current=l.entry.target,u({isIntersecting:o,entry:void 0}))},[s,l.entry,n,d,o]);let h=[c,!!l.isIntersecting,l.entry];return h.ref=h[0],h.isIntersecting=h[1],h.entry=h[2],h}var lt={container:"n",hidden:"l",content:"u",pop:"d",badge:"y",sidebar:"i",controls:"w",results:"k",loadmore:"z"};function Fu(e){let{isIntersecting:t,ref:r}=ju({threshold:0});mt(()=>{t&&cu()},[t]);let n=Vt(null);mt(()=>{n.current&&typeof Se.value.page>"u"&&n.current.scrollTo({top:0,behavior:"smooth"})},[Se.value]);let o=za();return z("div",{class:zt(lt.container,{[lt.hidden]:e.hidden}),children:[z("div",{class:lt.content,children:[z("div",{class:lt.controls,children:[z(co,{onClick:Dt,children:z(Ru,{})}),z(Nu,{focus:!e.hidden}),z(co,{onClick:Wa,children:[z(Iu,{}),o.length>0&&z("span",{class:lt.badge,children:o.length})]})]}),z("div",{class:lt.results,ref:n,children:[z(Du,{keyboard:!e.hidden}),z("div",{class:lt.loadmore,ref:r})]})]}),z("div",{class:zt(lt.sidebar,{[lt.hidden]:du()}),children:z(Uu,{})})]})}var Tt={container:"X",list:"j",heading:"F",title:"I",item:"o",active:"g",value:"R",count:"q"};function Uu(e){let t=mu();return t.sort((r,n)=>n.node.count-r.node.count),z("div",{class:Tt.container,children:[z("h3",{class:Tt.heading,children:"Filters"}),z("h4",{class:Tt.title,children:"Tags"}),z("ol",{class:Tt.list,children:t.map(r=>z("li",{class:zt(Tt.item,{[Tt.active]:lu(r.node.value)}),onClick:()=>uu(r.node.value),children:[z("span",{class:Tt.value,children:r.node.value}),z("span",{class:Tt.count,children:r.node.count})]}))})]})}var ca={container:"f"};function Nu(e){let t=Vt(null);return mt(()=>{var r,n;e.focus?(r=t.current)==null||r.focus():(n=t.current)==null||n.blur()},[e.focus]),z("div",{class:ca.container,children:z("input",{ref:t,type:"text",class:ca.content,value:hn(wn()),onInput:r=>Va(yu(r.currentTarget.value)),autocapitalize:"off",autocomplete:"off",autocorrect:"off",placeholder:"Search",spellcheck:!1,role:"combobox"})})}var ut={container:"b",heading:"A",item:"a",active:"h",wrapper:"B",actions:"s",title:"x",path:"t"};function Ga(){let[e,t]=bn(!1);return mt(()=>{let r=()=>t(!0),n=()=>t(!1);return document.addEventListener("compositionstart",r),document.addEventListener("compositionend",n),()=>{document.removeEventListener("compositionstart",r),document.removeEventListener("compositionend",n)}},[]),e}function Du(e){var s;let t=su(),r=pu(),n=dn(),o=Vt([]),i=Ga();mt(()=>{let c=o.current[n];c&&c.scrollIntoView({block:"center",behavior:"smooth"})},[n]),Aa(e.keyboard,c=>{if(i)return;let l=dn();c.key==="ArrowDown"?(c.preventDefault(),so(Math.min(l+1,r.length-1))):c.key==="ArrowUp"&&(c.preventDefault(),so(Math.max(l-1,0)))},[e.keyboard,i]);let a=(s=fu())!=null?s:0;return z(ft,{children:[r.length>0&&z("h3",{class:ut.heading,children:[z("span",{class:ut.bubble,children:new Intl.NumberFormat("en-US").format(a)})," ","results"]}),z("ol",{class:ut.container,children:r.map((c,l)=>{var m;let u=Ba(t[c.id].title,c.matches.find(({field:h})=>h==="title")),p=Mu((m=t[c.id].path)!=null?m:[],c.matches.find(({field:h})=>h==="path")),d=t[c.id].location;if(qa()){let h=encodeURIComponent(wn()),[v,x]=d.split("#",2);d=`${v}?h=${h.replace(/%20/g,"+")}`,typeof x<"u"&&(d+=`#${x}`)}return z("li",{children:z("a",{ref:h=>{o.current[l]=h},href:d,onClick:()=>Dt(),class:zt(ut.item,{[ut.active]:l===dn()}),children:[z("div",{class:ut.wrapper,children:[z("h2",{class:ut.title,children:u}),z("menu",{class:ut.path,children:p.map(h=>z("li",{children:h}))})]}),z("nav",{class:ut.actions,children:z(co,{children:z(Pu,{})})})]})})})})]})}var Wu={container:"e"};function Vu(e){let t=Ga();return Aa(!0,r=>{var n,o,i,a,s;if(!t)if((r.metaKey||r.ctrlKey)&&r.key==="k")r.preventDefault(),Dt();else if((r.metaKey||r.ctrlKey)&&r.key==="j")document.body.classList.toggle("dark");else if(r.key==="Enter"&&!sr()){r.preventDefault();let c=dn(),l=(o=(n=it.value)==null?void 0:n.items[c])==null?void 0:o.id;if((a=(i=lr.value)==null?void 0:i.items[l])!=null&&a.location){Dt();let u=(s=lr.value)==null?void 0:s.items[l].location;if(qa()){let p=encodeURIComponent(wn()),[d,m]=u.split("#",2);u=`${d}?h=${p.replace(/%20/g,"+")}`,typeof m<"u"&&(u+=`#${m}`)}window.location.href=u}}else r.key==="Escape"&&!sr()&&(r.preventDefault(),Dt())},[t]),z("div",{class:Wu.container,children:[z(ku,{hidden:sr()}),z(Fu,{hidden:sr()})]})}function Ja(e,t){au(e),El(z(Vu,{}),t)}function go(){Dt()}function zu(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function qu(){return R(b(window,"compositionstart").pipe(f(()=>!0)),b(window,"compositionend").pipe(f(()=>!1))).pipe(J(!1))}function Xa(){let e=b(window,"keydown").pipe(f(t=>({mode:sr()?"global":"search",type:t.key,meta:t.ctrlKey||t.metaKey,claim(){t.preventDefault(),t.stopPropagation()}})),L(({mode:t,type:r})=>{if(t==="global"){let n=xt();if(typeof n!="undefined")return!zu(n,r)}return!0}),xe());return qu().pipe(g(t=>t?y:e))}function Ye(){return new URL(location.href)}function dt(e,t=!1){if(X("navigation.instant")&&!t){let r=A("a",{href:e.href});document.body.appendChild(r),r.click(),r.remove()}else location.href=e.href}function Za(){return new I}function Qa(){return location.hash.slice(1)}function es(e){let t=A("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function _o(e){return R(b(window,"hashchange"),e).pipe(f(Qa),J(Qa()),L(t=>t.length>0),se(1))}function ts(e){return _o(e).pipe(f(t=>Le(`[id="${t}"]`)),L(t=>typeof t!="undefined"))}function Ir(e){let t=matchMedia(e);return an(r=>t.addListener(()=>r(t.matches))).pipe(J(t.matches))}function rs(){let e=matchMedia("print");return R(b(window,"beforeprint").pipe(f(()=>!0)),b(window,"afterprint").pipe(f(()=>!1))).pipe(J(e.matches))}function yo(e,t){return e.pipe(g(r=>r?t():y))}function xo(e,t){return new U(r=>{let n=new XMLHttpRequest;return n.open("GET",`${e}`),n.responseType="blob",n.addEventListener("load",()=>{n.status>=200&&n.status<300?(r.next(n.response),r.complete()):r.error(new Error(n.statusText))}),n.addEventListener("error",()=>{r.error(new Error("Network error"))}),n.addEventListener("abort",()=>{r.complete()}),typeof(t==null?void 0:t.progress$)!="undefined"&&(n.addEventListener("progress",o=>{var i;if(o.lengthComputable)t.progress$.next(o.loaded/o.total*100);else{let a=(i=n.getResponseHeader("Content-Length"))!=null?i:0;t.progress$.next(o.loaded/+a*100)}}),t.progress$.next(5)),n.send(),()=>n.abort()})}function et(e,t){return xo(e,t).pipe(g(r=>r.text()),f(r=>JSON.parse(r)),se(1))}function En(e,t){let r=new DOMParser;return xo(e,t).pipe(g(n=>n.text()),f(n=>r.parseFromString(n,"text/html")),se(1))}function ns(e,t){let r=new DOMParser;return xo(e,t).pipe(g(n=>n.text()),f(n=>r.parseFromString(n,"text/xml")),se(1))}var wo={drawer:G("[data-md-toggle=drawer]"),search:G("[data-md-toggle=search]")};function Eo(e,t){wo[e].checked!==t&&wo[e].click()}function Tn(e){let t=wo[e];return b(t,"change").pipe(f(()=>t.checked),J(t.checked))}function os(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function is(){return R(b(window,"scroll",{passive:!0}),b(window,"resize",{passive:!0})).pipe(f(os),J(os()))}function as(){return{width:innerWidth,height:innerHeight}}function ss(){return b(window,"resize",{passive:!0}).pipe(f(as),J(as()))}function cs(){return re([is(),ss()]).pipe(f(([e,t])=>({offset:e,size:t})),se(1))}function Sn(e,{viewport$:t,header$:r}){let n=t.pipe(fe("size")),o=re([n,r]).pipe(f(()=>wt(e)));return re([r,t,o]).pipe(f(([{height:i},{offset:a,size:s},{x:c,y:l}])=>({offset:{x:a.x-c,y:a.y-l+i},size:s})))}var Ku=G("#__config"),mr=JSON.parse(Ku.textContent);mr.base=`${new URL(mr.base,Ye())}`;function Ue(){return mr}function X(e){return mr.features.includes(e)}function Bt(e,t){return typeof t!="undefined"?mr.translations[e].replace("#",t.toString()):mr.translations[e]}function ht(e,t=document){return G(`[data-md-component=${e}]`,t)}function Ee(e,t=document){return P(`[data-md-component=${e}]`,t)}function Bu(e){let t=G(".md-typeset > :first-child",e);return b(t,"click",{once:!0}).pipe(f(()=>G(".md-typeset",e)),f(r=>({hash:__md_hash(r.innerHTML)})))}function ls(e){if(!X("announce.dismiss")||!e.childElementCount)return y;if(!e.hidden){let t=G(".md-typeset",e);__md_hash(t.innerHTML)===__md_get("__announce")&&(e.hidden=!0)}return j(()=>{let t=new I;return t.subscribe(({hash:r})=>{e.hidden=!0,__md_set("__announce",r)}),Bu(e).pipe($(r=>t.next(r)),V(()=>t.complete()),f(r=>H({ref:e},r)))})}function Yu(e,{target$:t}){return t.pipe(f(r=>({hidden:r!==e})))}function us(e,t){let r=new I;return r.subscribe(({hidden:n})=>{e.hidden=n}),Yu(e,t).pipe($(n=>r.next(n)),V(()=>r.complete()),f(n=>H({ref:e},n)))}function To(e,t){return t==="inline"?A("div",{class:"md-tooltip md-tooltip--inline",id:e,role:"tooltip"},A("div",{class:"md-tooltip__inner md-typeset"})):A("div",{class:"md-tooltip",id:e,role:"tooltip"},A("div",{class:"md-tooltip__inner md-typeset"}))}function On(...e){return A("div",{class:"md-tooltip2",role:"dialog"},A("div",{class:"md-tooltip2__inner md-typeset"},e))}function ps(...e){return A("div",{class:"md-tooltip2",role:"tooltip"},A("div",{class:"md-tooltip2__inner md-typeset"},e))}function fs(e,t){if(t=t?`${t}_annotation_${e}`:void 0,t){let r=t?`#${t}`:void 0;return A("aside",{class:"md-annotation",tabIndex:0},To(t),A("a",{href:r,class:"md-annotation__index",tabIndex:-1},A("span",{"data-md-annotation-id":e})))}else return A("aside",{class:"md-annotation",tabIndex:0},To(t),A("span",{class:"md-annotation__index",tabIndex:-1},A("span",{"data-md-annotation-id":e})))}function ms(e){return A("button",{class:"md-code__button",title:Bt("clipboard.copy"),"data-clipboard-target":`#${e} > code`,"data-md-type":"copy"})}function ds(){return A("button",{class:"md-code__button",title:"Toggle line selection","data-md-type":"select"})}function hs(){return A("nav",{class:"md-code__nav"})}var Xu=_r(So());function bs(e){return A("ul",{class:"md-source__facts"},Object.entries(e).map(([t,r])=>A("li",{class:`md-source__fact md-source__fact--${t}`},typeof r=="number"?Li(r):r)))}function Oo(e){let t=`tabbed-control tabbed-control--${e}`;return A("div",{class:t,hidden:!0},A("button",{class:"tabbed-button",tabIndex:-1,"aria-hidden":"true"}))}function gs(e){return A("div",{class:"md-typeset__scrollwrap"},A("div",{class:"md-typeset__table"},e))}function Zu(e){var n;let t=Ue(),r=new URL(`../${e.version}/`,t.base);return A("li",{class:"md-version__item"},A("a",{href:`${r}`,class:"md-version__link"},e.title,((n=t.version)==null?void 0:n.alias)&&e.aliases.length>0&&A("span",{class:"md-version__alias"},e.aliases[0])))}function _s(e,t){var n;let r=Ue();return e=e.filter(o=>{var i;return!((i=o.properties)!=null&&i.hidden)}),A("div",{class:"md-version"},A("button",{class:"md-version__current","aria-label":Bt("select.version")},t.title,((n=r.version)==null?void 0:n.alias)&&t.aliases.length>0&&A("span",{class:"md-version__alias"},t.aliases[0])),A("ul",{class:"md-version__list"},e.map(Zu)))}var Qu=0;function ep(e,t=250){let r=re([ir(e),Ft(e,t)]).pipe(f(([o,i])=>o||i),ie()),n=j(()=>Ai(e)).pipe(oe(Ut),Lr(1),Ze(r),f(()=>Ci(e)));return r.pipe(Sr(o=>o),g(()=>re([r,n])),f(([o,i])=>({active:o,offset:i})),xe())}function Rr(e,t,r=250){let{content$:n,viewport$:o}=t,i=`__tooltip2_${Qu++}`;return j(()=>{let a=new I,s=new Un(!1);a.pipe(he(),ye(!1)).subscribe(s);let c=s.pipe(Tr(u=>Ve(+!u*250,Wn)),ie(),g(u=>u?n:y),$(u=>u.id=i),xe());re([a.pipe(f(({active:u})=>u)),c.pipe(g(u=>Ft(u,250)),J(!1))]).pipe(f(u=>u.some(p=>p))).subscribe(s);let l=s.pipe(L(u=>u),pe(c,o),f(([u,p,{size:d}])=>{let m=e.getBoundingClientRect(),h=m.width/2;if(p.role==="tooltip")return{x:h,y:8+m.height};if(m.y>=d.height/2){let{height:v}=Ae(p);return{x:h,y:-16-v}}else return{x:h,y:16+m.height}}));return re([c,a,l]).subscribe(([u,{offset:p},d])=>{u.style.setProperty("--md-tooltip-host-x",`${p.x}px`),u.style.setProperty("--md-tooltip-host-y",`${p.y}px`),u.style.setProperty("--md-tooltip-x",`${d.x}px`),u.style.setProperty("--md-tooltip-y",`${d.y}px`),u.classList.toggle("md-tooltip2--top",d.y<0),u.classList.toggle("md-tooltip2--bottom",d.y>=0)}),s.pipe(L(u=>u),pe(c,(u,p)=>p),L(u=>u.role==="tooltip")).subscribe(u=>{let p=Ae(G(":scope > *",u));u.style.setProperty("--md-tooltip-width",`${p.width}px`),u.style.setProperty("--md-tooltip-tail","0px")}),s.pipe(ie(),Ie(je),pe(c)).subscribe(([u,p])=>{p.classList.toggle("md-tooltip2--active",u)}),re([s.pipe(L(u=>u)),c]).subscribe(([u,p])=>{p.role==="dialog"?(e.setAttribute("aria-controls",i),e.setAttribute("aria-haspopup","dialog")):e.setAttribute("aria-describedby",i)}),s.pipe(L(u=>!u)).subscribe(()=>{e.removeAttribute("aria-controls"),e.removeAttribute("aria-describedby"),e.removeAttribute("aria-haspopup")}),ep(e,r).pipe($(u=>a.next(u)),V(()=>a.complete()),f(u=>H({ref:e},u)))})}function Ge(e,{viewport$:t},r=document.body){return Rr(e,{content$:new U(n=>{let o=e.title,i=ps(o);return n.next(i),e.removeAttribute("title"),r.append(i),()=>{i.remove(),e.setAttribute("title",o)}}),viewport$:t},0)}function tp(e,t){let r=j(()=>re([Hi(e),Ut(t)])).pipe(f(([{x:n,y:o},i])=>{let{width:a,height:s}=Ae(e);return{x:n-i.x+a/2,y:o-i.y+s/2}}));return ir(e).pipe(g(n=>r.pipe(f(o=>({active:n,offset:o})),Me(+!n||1/0))))}function ys(e,t,{target$:r}){let[n,o]=Array.from(e.children);return j(()=>{let i=new I,a=i.pipe(he(),ye(!0));return i.subscribe({next({offset:s}){e.style.setProperty("--md-tooltip-x",`${s.x}px`),e.style.setProperty("--md-tooltip-y",`${s.y}px`)},complete(){e.style.removeProperty("--md-tooltip-x"),e.style.removeProperty("--md-tooltip-y")}}),Et(e).pipe(Q(a)).subscribe(s=>{e.toggleAttribute("data-md-visible",s)}),R(i.pipe(L(({active:s})=>s)),i.pipe(Be(250),L(({active:s})=>!s))).subscribe({next({active:s}){s?e.prepend(n):n.remove()},complete(){e.prepend(n)}}),i.pipe(Xe(16,je)).subscribe(({active:s})=>{n.classList.toggle("md-tooltip--active",s)}),i.pipe(Lr(125,je),L(()=>!!e.offsetParent),f(()=>e.offsetParent.getBoundingClientRect()),f(({x:s})=>s)).subscribe({next(s){s?e.style.setProperty("--md-tooltip-0",`${-s}px`):e.style.removeProperty("--md-tooltip-0")},complete(){e.style.removeProperty("--md-tooltip-0")}}),b(o,"click").pipe(Q(a),L(s=>!(s.metaKey||s.ctrlKey))).subscribe(s=>{s.stopPropagation(),s.preventDefault()}),b(o,"mousedown").pipe(Q(a),pe(i)).subscribe(([s,{active:c}])=>{var l;if(s.button!==0||s.metaKey||s.ctrlKey)s.preventDefault();else if(c){s.preventDefault();let u=e.parentElement.closest(".md-annotation");u instanceof HTMLElement?u.focus():(l=xt())==null||l.blur()}}),r.pipe(Q(a),L(s=>s===n),It(125)).subscribe(()=>e.focus()),tp(e,t).pipe($(s=>i.next(s)),V(()=>i.complete()),f(s=>H({ref:e},s)))})}function rp(e){let t=Ue();if(e.tagName!=="CODE")return[e];let r=[".c",".c1",".cm"];if(t.annotate){let n=e.closest("[class|=language]");if(n)for(let o of Array.from(n.classList)){if(!o.startsWith("language-"))continue;let[,i]=o.split("-");i in t.annotate&&r.push(...t.annotate[i])}}return P(r.join(", "),e)}function np(e){let t=[];for(let r of rp(e)){let n=[],o=document.createNodeIterator(r,NodeFilter.SHOW_TEXT);for(let i=o.nextNode();i;i=o.nextNode())n.push(i);for(let i of n){let a;for(;a=/(\(\d+\))(!)?/.exec(i.textContent);){let[,s,c]=a;if(typeof c=="undefined"){let l=i.splitText(a.index);i=l.splitText(s.length),t.push(l)}else{i.textContent=s,t.push(i);break}}}}return t}function xs(e,t){t.append(...Array.from(e.childNodes))}function Ln(e,t,{target$:r,print$:n}){let o=t.closest("[id]"),i=o==null?void 0:o.id,a=new Map;for(let s of np(t)){let[,c]=s.textContent.match(/\((\d+)\)/);Le(`:scope > li:nth-child(${c})`,e)&&(a.set(c,fs(c,i)),s.replaceWith(a.get(c)))}return a.size===0?y:j(()=>{let s=new I,c=s.pipe(he(),ye(!0)),l=[];for(let[u,p]of a)l.push([G(".md-typeset",p),G(`:scope > li:nth-child(${u})`,e)]);return n.pipe(Q(c)).subscribe(u=>{e.hidden=!u,e.classList.toggle("md-annotation-list",u);for(let[p,d]of l)u?xs(p,d):xs(d,p)}),R(...[...a].map(([,u])=>ys(u,t,{target$:r}))).pipe(V(()=>s.complete()),xe())})}function ws(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return ws(t)}}function Es(e,t){return j(()=>{let r=ws(e);return typeof r!="undefined"?Ln(r,e,t):y})}var Ss=_r(Mo());var op=0,Ts=R(b(window,"keydown").pipe(f(()=>!0)),R(b(window,"keyup"),b(window,"contextmenu")).pipe(f(()=>!1))).pipe(J(!1),se(1));function Os(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return Os(t)}}function ip(e){return Re(e).pipe(f(({width:t})=>({scrollable:Mr(e).width>t})),fe("scrollable"))}function Ls(e,t){let{matches:r}=matchMedia("(hover)"),n=j(()=>{let o=new I,i=o.pipe(Gn(1));o.subscribe(({scrollable:m})=>{m&&r?e.setAttribute("tabindex","0"):e.removeAttribute("tabindex")});let a=[],s=e.closest("pre"),c=s.closest("[id]"),l=c?c.id:op++;s.id=`__code_${l}`;let u=[],p=e.closest(".highlight");if(p instanceof HTMLElement){let m=Os(p);if(typeof m!="undefined"&&(p.classList.contains("annotate")||X("content.code.annotate"))){let h=Ln(m,e,t);u.push(Re(p).pipe(Q(i),f(({width:v,height:x})=>v&&x),ie(),g(v=>v?h:y)))}}let d=P(":scope > span[id]",e);if(d.length&&(e.classList.add("md-code__content"),e.closest(".select")||X("content.code.select")&&!e.closest(".no-select"))){let m=+d[0].id.split("-").pop(),h=ds();a.push(h),X("content.tooltips")&&u.push(Ge(h,{viewport$}));let v=b(h,"click").pipe(Or(M=>!M,!1),$(()=>h.blur()),xe());v.subscribe(M=>{h.classList.toggle("md-code__button--active",M)});let x=me(d).pipe(oe(M=>Ft(M).pipe(f(O=>[M,O]))));v.pipe(g(M=>M?x:y)).subscribe(([M,O])=>{let N=Le(".hll.select",M);if(N&&!O)N.replaceWith(...Array.from(N.childNodes));else if(!N&&O){let ee=document.createElement("span");ee.className="hll select",ee.append(...Array.from(M.childNodes).slice(1)),M.append(ee)}});let w=me(d).pipe(oe(M=>b(M,"mousedown").pipe($(O=>O.preventDefault()),f(()=>M)))),E=v.pipe(g(M=>M?w:y),pe(Ts),f(([M,O])=>{var ee;let N=d.indexOf(M)+m;if(O===!1)return[N,N];{let le=P(".hll",e).map(ce=>d.indexOf(ce.parentElement)+m);return(ee=window.getSelection())==null||ee.removeAllRanges(),[Math.min(N,...le),Math.max(N,...le)]}})),_=_o(y).pipe(L(M=>M.startsWith(`__codelineno-${l}-`)));_.subscribe(M=>{let[,,O]=M.split("-"),N=O.split(":").map(le=>+le-m+1);N.length===1&&N.push(N[0]);for(let le of P(".hll:not(.select)",e))le.replaceWith(...Array.from(le.childNodes));let ee=d.slice(N[0]-1,N[1]);for(let le of ee){let ce=document.createElement("span");ce.className="hll",ce.append(...Array.from(le.childNodes).slice(1)),le.append(ce)}}),_.pipe(Me(1),Ie(ge)).subscribe(M=>{if(M.includes(":")){let O=document.getElementById(M.split(":")[0]);O&&setTimeout(()=>{let N=O,ee=-64;for(;N!==document.body;)ee+=N.offsetTop,N=N.offsetParent;window.scrollTo({top:ee})},1)}});let be=me(P('a[href^="#__codelineno"]',p)).pipe(oe(M=>b(M,"click").pipe($(O=>O.preventDefault()),f(()=>M)))).pipe(Q(i),pe(Ts),f(([M,O])=>{let ee=+G(`[id="${M.hash.slice(1)}"]`).parentElement.id.split("-").pop();if(O===!1)return[ee,ee];{let le=P(".hll",e).map(ce=>+ce.parentElement.id.split("-").pop());return[Math.min(ee,...le),Math.max(ee,...le)]}}));R(E,be).subscribe(M=>{let O=`#__codelineno-${l}-`;M[0]===M[1]?O+=M[0]:O+=`${M[0]}:${M[1]}`,history.replaceState({},"",O),window.dispatchEvent(new HashChangeEvent("hashchange",{newURL:window.location.origin+window.location.pathname+O,oldURL:window.location.href}))})}if(Ss.default.isSupported()&&(e.closest(".copy")||X("content.code.copy")&&!e.closest(".no-copy"))){let m=ms(s.id);a.push(m),X("content.tooltips")&&u.push(Ge(m,{viewport$}))}if(a.length){let m=hs();m.append(...a),s.insertBefore(m,e)}return ip(e).pipe($(m=>o.next(m)),V(()=>o.complete()),f(m=>H({ref:e},m)),Rt(R(...u).pipe(Q(i))))});return X("content.lazy")?Et(e).pipe(L(o=>o),Me(1),g(()=>n)):n}function ap(e,{target$:t,print$:r}){let n=!0;return R(t.pipe(f(o=>o.closest("details:not([open])")),L(o=>e===o),f(()=>({action:"open",reveal:!0}))),r.pipe(L(o=>o||!n),$(()=>n=e.open),f(o=>({action:o?"open":"close"}))))}function Ms(e,t){return j(()=>{let r=new I;return r.subscribe(({action:n,reveal:o})=>{e.toggleAttribute("open",n==="open"),o&&e.scrollIntoView()}),ap(e,t).pipe($(n=>r.next(n)),V(()=>r.complete()),f(n=>H({ref:e},n)))})}var ks=0,As=new Map;function sp(e){let t=document.createElement("h3");t.innerHTML=e.innerHTML;let r=[t],n=e.nextElementSibling;for(;n&&!(n instanceof HTMLHeadingElement);)r.push(n.cloneNode(!0)),n=n.nextElementSibling;return r}function cp(e,t){for(let r of P("[href], [src]",e))for(let n of["href","src"]){let o=r.getAttribute(n);if(o&&!/^(?:[a-z]+:)?\/\//i.test(o)){r[n]=new URL(r.getAttribute(n),t).toString();break}}for(let r of P("[name^=__], [for]",e))for(let n of["id","for","name"]){let o=r.getAttribute(n);o&&r.setAttribute(n,`${o}$preview_${ks}`)}return ks++,Y(e)}function lp(e){let t=As.get(e.toString());return t?Y(t):En(e).pipe(g(r=>cp(r,e)),f(r=>(As.set(e.toString(),r),r)))}function Cs(e,t){let{sitemap$:r}=t;if(!(e instanceof HTMLAnchorElement))return y;if(!(X("navigation.instant.preview")||e.hasAttribute("data-preview")))return y;e.removeAttribute("title");let n=re([ir(e),Ft(e).pipe(ke(1))]).pipe(f(([i,a])=>i||a),ie(),L(i=>i));return $t([r,n]).pipe(g(([i])=>{let a=new URL(e.href);return a.search=a.hash="",i.has(`${a}`)?Y(a):y}),g(i=>lp(i)),g(i=>{let a=e.hash?`article [id="${decodeURIComponent(e.hash.slice(1))}"]`:"article h1",s=Le(a,i);return typeof s=="undefined"?y:Y(sp(s))})).pipe(g(i=>{let a=new U(s=>{let c=On(...i);return s.next(c),document.body.append(c),()=>c.remove()});return Rr(e,H({content$:a},t))}))}var Hs=".node circle,.node ellipse,.node path,.node polygon,.node rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}marker{fill:var(--md-mermaid-edge-color)!important}.edgeLabel .label rect{fill:#0000}.flowchartTitleText{fill:var(--md-mermaid-label-fg-color)}.label{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.label foreignObject{line-height:normal;overflow:visible}.label div .edgeLabel{color:var(--md-mermaid-label-fg-color)}.edgeLabel,.edgeLabel p,.label div .edgeLabel{background-color:var(--md-mermaid-label-bg-color)}.edgeLabel,.edgeLabel p{fill:var(--md-mermaid-label-bg-color);color:var(--md-mermaid-edge-color)}.edgePath .path,.flowchart-link{stroke:var(--md-mermaid-edge-color)}.edgePath .arrowheadPath{fill:var(--md-mermaid-edge-color);stroke:none}.cluster rect{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}.cluster span{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}g #flowchart-circleEnd,g #flowchart-circleStart,g #flowchart-crossEnd,g #flowchart-crossStart,g #flowchart-pointEnd,g #flowchart-pointStart{stroke:none}.classDiagramTitleText{fill:var(--md-mermaid-label-fg-color)}g.classGroup line,g.classGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.classGroup text{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.classLabel .box{fill:var(--md-mermaid-label-bg-color);background-color:var(--md-mermaid-label-bg-color);opacity:1}.classLabel .label{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.node .divider{stroke:var(--md-mermaid-node-fg-color)}.relation{stroke:var(--md-mermaid-edge-color)}.cardinality{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.cardinality text{fill:inherit!important}defs marker.marker.composition.class path,defs marker.marker.dependency.class path,defs marker.marker.extension.class path{fill:var(--md-mermaid-edge-color)!important;stroke:var(--md-mermaid-edge-color)!important}defs marker.marker.aggregation.class path{fill:var(--md-mermaid-label-bg-color)!important;stroke:var(--md-mermaid-edge-color)!important}.statediagramTitleText{fill:var(--md-mermaid-label-fg-color)}g.stateGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.stateGroup .state-title{fill:var(--md-mermaid-label-fg-color)!important;font-family:var(--md-mermaid-font-family)}g.stateGroup .composit{fill:var(--md-mermaid-label-bg-color)}.nodeLabel,.nodeLabel p{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}a .nodeLabel{text-decoration:underline}.node circle.state-end,.node circle.state-start,.start-state{fill:var(--md-mermaid-edge-color);stroke:none}.end-state-inner,.end-state-outer{fill:var(--md-mermaid-edge-color)}.end-state-inner,.node circle.state-end{stroke:var(--md-mermaid-label-bg-color)}.transition{stroke:var(--md-mermaid-edge-color)}[id^=state-fork] rect,[id^=state-join] rect{fill:var(--md-mermaid-edge-color)!important;stroke:none!important}.statediagram-cluster.statediagram-cluster .inner{fill:var(--md-default-bg-color)}.statediagram-cluster rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.statediagram-state rect.divider{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}defs #statediagram-barbEnd{stroke:var(--md-mermaid-edge-color)}[id^=entity] path,[id^=entity] rect{fill:var(--md-default-bg-color)}.relationshipLine{stroke:var(--md-mermaid-edge-color)}defs .marker.oneOrMore.er *,defs .marker.onlyOne.er *,defs .marker.zeroOrMore.er *,defs .marker.zeroOrOne.er *{stroke:var(--md-mermaid-edge-color)!important}text:not([class]):last-child{fill:var(--md-mermaid-label-fg-color)}.actor{fill:var(--md-mermaid-sequence-actor-bg-color);stroke:var(--md-mermaid-sequence-actor-border-color)}text.actor>tspan{fill:var(--md-mermaid-sequence-actor-fg-color);font-family:var(--md-mermaid-font-family)}line{stroke:var(--md-mermaid-sequence-actor-line-color)}.actor-man circle,.actor-man line{fill:var(--md-mermaid-sequence-actorman-bg-color);stroke:var(--md-mermaid-sequence-actorman-line-color)}.messageLine0,.messageLine1{stroke:var(--md-mermaid-sequence-message-line-color)}.note{fill:var(--md-mermaid-sequence-note-bg-color);stroke:var(--md-mermaid-sequence-note-border-color)}.loopText,.loopText>tspan,.messageText,.noteText>tspan{stroke:none;font-family:var(--md-mermaid-font-family)!important}.messageText{fill:var(--md-mermaid-sequence-message-fg-color)}.loopText,.loopText>tspan{fill:var(--md-mermaid-sequence-loop-fg-color)}.noteText>tspan{fill:var(--md-mermaid-sequence-note-fg-color)}#arrowhead path{fill:var(--md-mermaid-sequence-message-line-color);stroke:none}.loopLine{fill:var(--md-mermaid-sequence-loop-bg-color);stroke:var(--md-mermaid-sequence-loop-border-color)}.labelBox{fill:var(--md-mermaid-sequence-label-bg-color);stroke:none}.labelText,.labelText>span{fill:var(--md-mermaid-sequence-label-fg-color);font-family:var(--md-mermaid-font-family)}.sequenceNumber{fill:var(--md-mermaid-sequence-number-fg-color)}rect.rect{fill:var(--md-mermaid-sequence-box-bg-color);stroke:none}rect.rect+text.text{fill:var(--md-mermaid-sequence-box-fg-color)}defs #sequencenumber{fill:var(--md-mermaid-sequence-number-bg-color)!important}";var ko,pp=0;function fp(){return typeof mermaid=="undefined"||mermaid instanceof Element?ar("https://unpkg.com/mermaid@11/dist/mermaid.min.js"):Y(void 0)}function $s(e){return e.classList.remove("mermaid"),ko||(ko=fp().pipe($(()=>mermaid.initialize({startOnLoad:!1,themeCSS:Hs,sequence:{actorFontSize:"16px",messageFontSize:"16px",noteFontSize:"16px"}})),f(()=>{}),se(1))),ko.subscribe(()=>Uo(null,null,function*(){e.classList.add("mermaid");let t=`__mermaid_${pp++}`,r=A("div",{class:"mermaid"}),n=e.textContent,{svg:o,fn:i}=yield mermaid.render(t,n),a=r.attachShadow({mode:"closed"});a.innerHTML=o,e.replaceWith(r),i==null||i(a)})),ko.pipe(f(()=>({ref:e})))}var Ps=A("table");function Is(e){return e.replaceWith(Ps),Ps.replaceWith(gs(e)),Y({ref:e})}function mp(e){let t=e.find(r=>r.checked)||e[0];return R(...e.map(r=>b(r,"change").pipe(f(()=>G(`label[for="${r.id}"]`))))).pipe(J(G(`label[for="${t.id}"]`)),f(r=>({active:r})))}function Rs(e,{viewport$:t,target$:r}){let n=G(".tabbed-labels",e),o=P(":scope > input",e),i=Oo("prev");e.append(i);let a=Oo("next");return e.append(a),j(()=>{let s=new I,c=s.pipe(he(),ye(!0));re([s,Re(e),Et(e)]).pipe(Q(c),Xe(1,je)).subscribe({next([{active:l},u]){let p=wt(l),{width:d}=Ae(l);e.style.setProperty("--md-indicator-x",`${p.x}px`),e.style.setProperty("--md-indicator-width",`${d}px`);let m=ln(n);(p.xm.x+u.width)&&n.scrollTo({left:Math.max(0,p.x-16),behavior:"smooth"})},complete(){e.style.removeProperty("--md-indicator-x"),e.style.removeProperty("--md-indicator-width")}}),re([Ut(n),Re(n)]).pipe(Q(c)).subscribe(([l,u])=>{let p=Mr(n);i.hidden=l.x<16,a.hidden=l.x>p.width-u.width-16}),R(b(i,"click").pipe(f(()=>-1)),b(a,"click").pipe(f(()=>1))).pipe(Q(c)).subscribe(l=>{let{width:u}=Ae(n);n.scrollBy({left:u*l,behavior:"smooth"})}),r.pipe(Q(c),L(l=>o.includes(l))).subscribe(l=>l.click()),n.classList.add("tabbed-labels--linked");for(let l of o){let u=G(`label[for="${l.id}"]`);u.replaceChildren(A("a",{href:`#${u.htmlFor}`,tabIndex:-1},...Array.from(u.childNodes))),b(u.firstElementChild,"click").pipe(Q(c),L(p=>!(p.metaKey||p.ctrlKey)),$(p=>{p.preventDefault(),p.stopPropagation()})).subscribe(()=>{history.replaceState({},"",`#${u.htmlFor}`),u.click()})}return X("content.tabs.link")&&s.pipe(ke(1),pe(t)).subscribe(([{active:l},{offset:u}])=>{let p=l.innerText.trim();if(l.hasAttribute("data-md-switching"))l.removeAttribute("data-md-switching");else{let d=e.offsetTop-u.y;for(let h of P("[data-tabs]"))for(let v of P(":scope > input",h)){let x=G(`label[for="${v.id}"]`);if(x!==l&&x.innerText.trim()===p){x.setAttribute("data-md-switching",""),v.click();break}}window.scrollTo({top:e.offsetTop-d});let m=__md_get("__tabs")||[];__md_set("__tabs",[...new Set([p,...m])])}}),s.pipe(Q(c)).subscribe(()=>{for(let l of P("audio, video",e))l.offsetWidth&&l.autoplay?l.play().catch(()=>{}):l.pause()}),mp(o).pipe($(l=>s.next(l)),V(()=>s.complete()),f(l=>H({ref:e},l)))}).pipe(Ht(ge))}function js(e,t){let{viewport$:r,target$:n,print$:o}=t;return R(...P(".annotate:not(.highlight)",e).map(i=>Es(i,{target$:n,print$:o})),...P("pre:not(.mermaid) > code",e).map(i=>Ls(i,{target$:n,print$:o})),...P("a",e).map(i=>Cs(i,t)),...P("pre.mermaid",e).map(i=>$s(i)),...P("table:not([class])",e).map(i=>Is(i)),...P("details",e).map(i=>Ms(i,{target$:n,print$:o})),...P("[data-tabs]",e).map(i=>Rs(i,{viewport$:r,target$:n})),...P("[title]:not([data-preview])",e).filter(()=>X("content.tooltips")).map(i=>Ge(i,{viewport$:r})),...P(".footnote-ref",e).filter(()=>X("content.footnote.tooltips")).map(i=>Rr(i,{content$:new U(a=>{let s=new URL(i.href).hash.slice(1),c=Array.from(document.getElementById(s).cloneNode(!0).children),l=On(...c);return a.next(l),document.body.append(l),()=>l.remove()}),viewport$:r})))}function dp(e,{alert$:t}){return t.pipe(g(r=>R(Y(!0),Y(!1).pipe(It(2e3))).pipe(f(n=>({message:r,active:n})))))}function Fs(e,t){let r=G(".md-typeset",e);return j(()=>{let n=new I;return n.subscribe(({message:o,active:i})=>{e.classList.toggle("md-dialog--active",i),r.textContent=o}),dp(e,t).pipe($(o=>n.next(o)),V(()=>n.complete()),f(o=>H({ref:e},o)))})}function hp({viewport$:e}){if(!X("header.autohide"))return Y(!1);let t=e.pipe(f(({offset:{y:o}})=>o),Pt(2,1),f(([o,i])=>[oMath.abs(i-o.y)>100),f(([,[o]])=>o),ie()),n=Tn("search");return re([e,n]).pipe(f(([{offset:o},i])=>o.y>400&&!i),ie(),g(o=>o?r:Y(!1)),J(!1))}function Us(e,t){return j(()=>re([Re(e),hp(t)])).pipe(f(([{height:r},n])=>({height:r,hidden:n})),ie((r,n)=>r.height===n.height&&r.hidden===n.hidden),se(1))}function Ns(e,{viewport$:t,header$:r,main$:n}){return j(()=>{let o=new I,i=o.pipe(he(),ye(!0));o.pipe(fe("active"),Ze(r)).subscribe(([{active:s},{hidden:c}])=>{e.classList.toggle("md-header--shadow",s&&!c),e.hidden=c});let a=me(P("[title]",e)).pipe(L(()=>X("content.tooltips")),oe(s=>Ge(s,{viewport$:t})));return n.subscribe(o),r.pipe(Q(i),f(s=>H({ref:e},s)),Rt(a.pipe(Q(i))))})}function vp(e,{viewport$:t,header$:r}){return Sn(e,{viewport$:t,header$:r}).pipe(f(({offset:{y:n}})=>{let{height:o}=Ae(e);return{active:o>0&&n>=o}}),fe("active"))}function Ds(e,t){return j(()=>{let r=new I;r.subscribe({next({active:o}){e.classList.toggle("md-header__title--active",o)},complete(){e.classList.remove("md-header__title--active")}});let n=Le(".md-content h1");return typeof n=="undefined"?y:vp(n,t).pipe($(o=>r.next(o)),V(()=>r.complete()),f(o=>H({ref:e},o)))})}function Ws(e,{viewport$:t,header$:r}){let n=r.pipe(f(({height:i})=>i),ie()),o=n.pipe(g(()=>Re(e).pipe(f(({height:i})=>({top:e.offsetTop,bottom:e.offsetTop+i})),fe("bottom"))));return re([n,o,t]).pipe(f(([i,{top:a,bottom:s},{offset:{y:c},size:{height:l}}])=>(l=Math.max(0,l-Math.max(0,a-c,i)-Math.max(0,l+c-s)),{offset:a-i,height:l,active:a-i<=c})),ie((i,a)=>i.offset===a.offset&&i.height===a.height&&i.active===a.active))}function bp(e){let t=__md_get("__palette")||{index:e.findIndex(n=>matchMedia(n.getAttribute("data-md-color-media")).matches)},r=Math.max(0,Math.min(t.index,e.length-1));return Y(...e).pipe(oe(n=>b(n,"change").pipe(f(()=>n))),J(e[r]),f(n=>({index:e.indexOf(n),color:{media:n.getAttribute("data-md-color-media"),scheme:n.getAttribute("data-md-color-scheme"),primary:n.getAttribute("data-md-color-primary"),accent:n.getAttribute("data-md-color-accent")}})),se(1))}function Vs(e){let t=P("input",e),r=A("meta",{name:"theme-color"});document.head.appendChild(r);let n=A("meta",{name:"color-scheme"});document.head.appendChild(n);let o=Ir("(prefers-color-scheme: light)");return j(()=>{let i=new I;return i.subscribe(a=>{if(document.body.setAttribute("data-md-color-switching",""),a.color.media==="(prefers-color-scheme)"){let s=matchMedia("(prefers-color-scheme: light)"),c=document.querySelector(s.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");a.color.scheme=c.getAttribute("data-md-color-scheme"),a.color.primary=c.getAttribute("data-md-color-primary"),a.color.accent=c.getAttribute("data-md-color-accent")}for(let[s,c]of Object.entries(a.color))document.body.setAttribute(`data-md-color-${s}`,c);for(let s=0;sa.key==="Enter"),pe(i,(a,s)=>s)).subscribe(({index:a})=>{a=(a+1)%t.length,t[a].click(),t[a].focus()}),i.pipe(f(()=>{let a=ht("header"),s=window.getComputedStyle(a);return n.content=s.colorScheme,s.backgroundColor.match(/\d+/g).map(c=>(+c).toString(16).padStart(2,"0")).join("")})).subscribe(a=>r.content=`#${a}`),i.pipe(Ie(ge)).subscribe(()=>{document.body.removeAttribute("data-md-color-switching")}),bp(t).pipe(Q(o.pipe(ke(1))),jt(),$(a=>i.next(a)),V(()=>i.complete()),f(a=>H({ref:e},a)))})}function zs(e,{progress$:t}){return j(()=>{let r=new I;return r.subscribe(({value:n})=>{e.style.setProperty("--md-progress-value",`${n}`)}),t.pipe($(n=>r.next({value:n})),V(()=>r.complete()),f(n=>({ref:e,value:n})))})}var qs='.v u{text-decoration:underline!important;text-decoration-style:wavy!important;text-decoration-thickness:1px!important}.p{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);background-color:rgba(var(--color-backdrop)/var(--alpha-lighter));cursor:pointer;height:100%;pointer-events:auto;position:absolute;transition:opacity .25s;width:100%}.p.m{opacity:0;pointer-events:none;transition:opacity .35s}.r{align-items:center;background-color:initial;border:none;border-radius:var(--space-2);cursor:pointer;display:flex;flex-shrink:0;font-family:var(--font-family);height:36px;justify-content:center;outline:none;padding:0;position:relative;transition:background-color .25s,color .25s;width:36px;z-index:1}.r svg{stroke:rgb(var(--color-foreground));height:18px;opacity:.5;width:18px}.r:before{background-color:rgb(var(--color-background-subtle));border-radius:var(--border-radius-2);content:"";inset:0;opacity:0;position:absolute;transform:scale(.75);transition:transform 125ms,opacity 125ms;z-index:0}.r:hover:before{opacity:1;transform:scale(1)}.r.c{cursor:auto}.r.c:before{display:none}.n{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);background-color:rgba(var(--color-background)/var(--alpha-light));border-radius:var(--space-3);box-shadow:0 0 60px #0000000d;display:flex;height:480px;overflow:hidden;pointer-events:auto;position:absolute;transition:transform .25s cubic-bezier(.16,1,.3,1),opacity .25s;width:640px}.n.l{opacity:0;pointer-events:none;transform:scale(1.1);transition:transform .25s .15s,opacity .15s}@media (max-width:680px){.n{border-radius:0;height:100%;width:100%}}.u{display:flex;flex-basis:min-content;flex-direction:column;flex-grow:1;flex-shrink:0}@keyframes d{0%{transform:scale(0)}50%{transform:scale(1.2)}to{transform:scale(1)}}.y{animation:d .25s ease-in-out;background:var(--color-highlight);border-radius:100%;color:#fff;font-size:8px;font-weight:700;height:12px;padding-top:1px;position:absolute;right:4px;top:4px;width:12px}.i{background-color:rgb(var(--color-background-subtle)/var(--alpha-lighter));flex-shrink:0;overflow:scroll;position:relative;transition:width .35s cubic-bezier(.16,1,.3,1),opacity .25s;width:200px}.i>*{transform:translate(0);transition:transform .25s cubic-bezier(.16,1,.3,1)}.i.l{opacity:0;width:0}.i.l>*{transform:translate(-48px)}@media (max-width:680px){.i{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);background-color:rgba(var(--color-background-subtle)/var(--alpha-light));box-shadow:0 0 60px #00000026;height:100%;position:absolute;right:0;top:0}}.w{border-bottom:1px solid rgb(var(--color-foreground)/var(--alpha-lightest));display:flex;gap:var(--space-1);padding:var(--space-2)}.k{-webkit-overflow-scrolling:touch;overflow:auto;overscroll-behavior:contain}.z{padding:8px 10px}.X{color:rgb(var(--color-foreground)/var(--alpha-light));padding:var(--space-2);position:absolute;width:200px}.X,.j{display:flex;flex-direction:column}.j{gap:2px;list-style:none;padding:0}.F,.j{margin:0}.F{font-size:16px;font-weight:400}.F,.I{padding:8px}.I{font-size:14px;margin:4px 0 0;opacity:.5}.I,.o{font-size:12px}.o{cursor:pointer;display:flex;padding:4px 8px;position:relative}.o:before{background-color:var(--color-highlight-transparent);border-radius:var(--space-1);content:"";inset:0;opacity:0;position:absolute;transform:scale(.75);transition:transform 125ms,opacity 125ms;z-index:0}.o.g:before,.o:hover:before{opacity:1;transform:scale(1)}.o.g,.o:hover{color:var(--color-highlight)}.R{flex-grow:1}.R,.q{position:relative}.q{font-weight:700}.f{flex-grow:1}.f input{background:#0000;border:none;color:rgb(var(--color-foreground));font-family:var(--font-family);font-size:16px;height:100%;letter-spacing:-.25px;outline:none;width:100%}.b{color:rgb(var(--color-foreground)/var(--alpha-light));display:flex;flex-direction:column;gap:2px;line-height:1.3;list-style:none;margin:var(--space-2);margin-top:0;padding:0}.A,.b li{margin:0}.A{color:rgb(var(--color-foreground)/var(--alpha-lighter));font-size:12px;margin-top:var(--space-2);padding:0 18px}.a{border-radius:var(--space-2);color:inherit;cursor:pointer;display:flex;flex-direction:row;flex-grow:1;padding:8px 10px;position:relative;text-decoration:none}.a:before{background-color:rgb(var(--color-background-subtle));border-radius:var(--border-radius-2);content:"";display:block;inset:0;opacity:0;position:absolute;transform:scale(.9);transition:transform 125ms,opacity 125ms;z-index:0}@media (pointer:fine){.a.h:before,.a:hover:before{opacity:1;transform:scale(1)}}.a mark{background:#0000;color:var(--color-highlight)}.a u{background-color:var(--color-highlight-transparent);border-radius:2px;box-shadow:0 0 0 1px var(--color-highlight-transparent);text-decoration:none}.B{flex-grow:1}.s{margin-right:-8px;opacity:0;position:relative;transform:translate(-2px);transition:transform 125ms,opacity 125ms;z-index:0}@media (pointer:fine){.h>.s,:hover>.s{opacity:1;transform:none}}.x{font-size:14px;margin:0;position:relative}.x code{background:rgb(var(--color-background-subtle));border-radius:var(--space-1);font-size:13px;padding:2px 4px}.t{color:rgb(var(--color-foreground)/var(--alpha-lighter));display:inline-flex;flex-wrap:wrap;font-size:12px;gap:var(--space-1);list-style:none;margin:0;padding:0;position:relative}.t li{white-space:nowrap}.t li:after{content:"/";display:inline;margin-left:var(--space-1)}.t li:last-child:after{content:"";display:none}.e{--space-1:4px;--space-2:calc(var(--space-1)*2);--space-3:calc(var(--space-2)*2);--space-4:calc(var(--space-3)*2);--space-5:calc(var(--space-4)*2);--alpha-light:.7;--alpha-lighter:.54;--alpha-lightest:.1;--color-highlight:var(--md-accent-fg-color,#526cfe);--color-highlight-transparent:var(--md-accent-fg-color--transparent,#526cfe1a);--border-radius-1:var(--space-1);--border-radius-2:var(--space-2);--border-radius-3:calc(var(--space-1) + var(--space-2));--font-family:var(--md-text-font-family,Inter,Roboto Flex,system-ui,sans-serif);--font-size:16px;--line-height:1.5;--letter-spacing:-.5px;-webkit-font-smoothing:antialiased;align-items:center;display:flex;font-family:var(--font-family);font-size:var(--font-size);height:100vh;justify-content:center;letter-spacing:var(--letter-spacing);line-height:var(--line-height);pointer-events:none;position:absolute;width:100vw}@media (pointer:coarse){.e{height:-webkit-fill-available}}.e *,.e :after,.e :before{box-sizing:border-box}';function Ks(e,{index$:t}){let r=Ue(),n=document.createElement("div");document.body.appendChild(n),n.style.position="fixed",n.style.height="100%",n.style.top="0",n.style.zIndex="4";let o=n.attachShadow({mode:"open"});o.appendChild(A("style",{},qs.toString()));try{Ya(r.search,{highlight:r.features.includes("search.highlight")}),me(t).subscribe(i=>{for(let a of i.items)a.location=new URL(a.location,r.base).toString();Ja(i,o)}),b(e,"click").subscribe(()=>{go()}),Tn("search").pipe(ke(1)).subscribe(()=>go())}catch(i){e.hidden=!0;let a=G("label[for=__search]");a.hidden=!0}return Ke}var Bs=_r(So());function Ys(e,{index$:t,location$:r}){return re([t,r.pipe(J(Ye()),L(n=>!!n.searchParams.get("h")))]).pipe(f(([n,o])=>_p(n.config)(o.searchParams.get("h"))),f(n=>{var a;let o=new Map,i=document.createNodeIterator(e,NodeFilter.SHOW_TEXT);for(let s=i.nextNode();s;s=i.nextNode())if((a=s.parentElement)!=null&&a.offsetHeight){let c=s.textContent,l=n(c);l.length>c.length&&o.set(s,l)}for(let[s,c]of o){let{childNodes:l}=A("span",null,c);s.replaceWith(...Array.from(l))}return{ref:e,nodes:o}}))}function _p(e){let t=e.separator.split("|").map(o=>o.replace(/(\(\?[!=<][^)]+\))/g,"").length===0?"\uFFFD":o).join("|"),r=new RegExp(t,"img"),n=(o,i,a)=>`${i}${a}`;return o=>{o=o.replace(/\s+/g," ").replace(/&/g,"&").trim();let i=new RegExp(`(^|${e.separator}|)(${o.split(r).map(a=>a.replace(/[|\\{}()[\]^$+*?.-]/g,"\\$&")).filter(a=>a.length>0).join("|")})`,"img");return a=>(0,Bs.default)(a).replace(i,n).replace(/<\/mark>(\s+)]*>/img,"$1")}}function yp(e,{viewport$:t,main$:r}){let n=e.closest(".md-grid"),o=n.offsetTop-n.parentElement.offsetTop;return re([r,t]).pipe(f(([{offset:i,height:a},{offset:{y:s}}])=>(a=a+Math.min(o,Math.max(0,s-i))-o,{height:a,locked:s>=i+o})),ie((i,a)=>i.height===a.height&&i.locked===a.locked))}function Ao(e,n){var o=n,{header$:t}=o,r=gr(o,["header$"]);let i=G(".md-sidebar__scrollwrap",e),{y:a}=wt(i);return j(()=>{let s=new I,c=s.pipe(he(),ye(!0)),l=s.pipe(Xe(0,je));return l.pipe(pe(t)).subscribe({next([{height:u},{height:p}]){i.style.height=`${u-2*a}px`,e.style.top=`${p}px`},complete(){i.style.height="",e.style.top=""}}),l.pipe(Sr()).subscribe(()=>{for(let u of P(".md-nav__link--active[href]",e)){if(!u.clientHeight)continue;let p=u.closest(".md-sidebar__scrollwrap");if(typeof p!="undefined"){let d=u.offsetTop-p.offsetTop,{height:m}=Ae(p);p.scrollTo({top:d-m/2})}}}),me(P("label[tabindex]",e)).pipe(oe(u=>b(u,"click").pipe(Ie(ge),f(()=>u),Q(c)))).subscribe(u=>{let p=G(`[id="${u.htmlFor}"]`);G(`[aria-labelledby="${u.id}"]`).setAttribute("aria-expanded",`${p.checked}`)}),X("content.tooltips")&&me(P("abbr[title]",e)).pipe(oe(u=>Ge(u,{viewport$})),Q(c)).subscribe(),yp(e,r).pipe($(u=>s.next(u)),V(()=>s.complete()),f(u=>H({ref:e},u)))})}function Gs(e,t){if(typeof t!="undefined"){let r=`https://api.github.com/repos/${e}/${t}`;return $t(et(`${r}/releases/latest`).pipe(_e(()=>y),f(n=>({version:n.tag_name})),ot({})),et(r).pipe(_e(()=>y),f(n=>({stars:n.stargazers_count,forks:n.forks_count})),ot({}))).pipe(f(([n,o])=>H(H({},n),o)))}else{let r=`https://api.github.com/users/${e}`;return et(r).pipe(f(n=>({repositories:n.public_repos})),ot({}))}}function Js(e,t){let r=`https://${e}/api/v4/projects/${encodeURIComponent(t)}`;return $t(et(`${r}/releases/permalink/latest`).pipe(_e(()=>y),f(({tag_name:n})=>({version:n})),ot({})),et(r).pipe(_e(()=>y),f(({star_count:n,forks_count:o})=>({stars:n,forks:o})),ot({}))).pipe(f(([n,o])=>H(H({},n),o)))}function Xs(e){let t=e.match(/^.+github\.com\/([^/]+)\/?([^/]+)?/i);if(t){let[,r,n]=t;return Gs(r,n)}if(t=e.match(/^.+?([^/]*gitlab[^/]+)\/(.+?)\/?$/i),t){let[,r,n]=t;return Js(r,n)}return y}var xp;function wp(e){return xp||(xp=j(()=>{let t=__md_get("__source",sessionStorage);if(t)return Y(t);if(Ee("consent").length){let n=__md_get("__consent");if(!(n&&n.github))return y}return Xs(e.href).pipe($(n=>__md_set("__source",n,sessionStorage)))}).pipe(_e(()=>y),L(t=>Object.keys(t).length>0),f(t=>({facts:t})),se(1)))}function Zs(e){let t=G(":scope > :last-child",e);return j(()=>{let r=new I;return r.subscribe(({facts:n})=>{t.appendChild(bs(n)),t.classList.add("md-source__repository--active")}),wp(e).pipe($(n=>r.next(n)),V(()=>r.complete()),f(n=>H({ref:e},n)))})}function Ep(e,{viewport$:t,header$:r}){return Re(document.body).pipe(g(()=>Sn(e,{header$:r,viewport$:t})),f(({offset:{y:n}})=>({hidden:n>=10})),fe("hidden"))}function Qs(e,t){return j(()=>{let r=new I;return r.subscribe({next({hidden:n}){e.hidden=n},complete(){e.hidden=!1}}),(X("navigation.tabs.sticky")?Y({hidden:!1}):Ep(e,t)).pipe($(n=>r.next(n)),V(()=>r.complete()),f(n=>H({ref:e},n)))})}function Tp(e,{viewport$:t,header$:r}){let n=new Map,o=P(".md-nav__link",e);for(let s of o){let c=decodeURIComponent(s.hash.substring(1)),l=Le(`[id="${c}"]`);typeof l!="undefined"&&n.set(s,l)}let i=r.pipe(fe("height"),f(({height:s})=>{let c=ht("main"),l=G(":scope > :first-child",c);return s+.9*(l.offsetTop-c.offsetTop)}),xe());return Re(document.body).pipe(fe("height"),g(s=>j(()=>{let c=[];return Y([...n].reduce((l,[u,p])=>{for(;c.length&&n.get(c[c.length-1]).tagName>=p.tagName;)c.pop();let d=p.offsetTop;for(;!d&&p.parentElement;)p=p.parentElement,d=p.offsetTop;let m=p.offsetParent;for(;m;m=m.offsetParent)d+=m.offsetTop;return l.set([...c=[...c,u]].reverse(),d)},new Map))}).pipe(f(c=>new Map([...c].sort(([,l],[,u])=>l-u))),Ze(i),g(([c,l])=>t.pipe(Or(([u,p],{offset:{y:d},size:m})=>{let h=d+m.height>=Math.floor(s.height);for(;p.length;){let[,v]=p[0];if(v-l=d&&!h)p=[u.pop(),...p];else break}return[u,p]},[[],[...c]]),ie((u,p)=>u[0]===p[0]&&u[1]===p[1])))))).pipe(f(([s,c])=>({prev:s.map(([l])=>l),next:c.map(([l])=>l)})),J({prev:[],next:[]}),Pt(2,1),f(([s,c])=>s.prev.length{let i=new I,a=i.pipe(he(),ye(!0));if(i.subscribe(({prev:s,next:c})=>{for(let[l]of c)l.classList.remove("md-nav__link--passed"),l.classList.remove("md-nav__link--active");for(let[l,[u]]of s.entries())u.classList.add("md-nav__link--passed"),u.classList.toggle("md-nav__link--active",l===s.length-1)}),X("toc.follow")){let s=R(t.pipe(Be(1),f(()=>{})),t.pipe(Be(250),f(()=>"smooth")));i.pipe(L(({prev:c})=>c.length>0),Ze(n.pipe(Ie(ge))),pe(s)).subscribe(([[{prev:c}],l])=>{let[u]=c[c.length-1];if(u.offsetHeight){let p=ki(u);if(typeof p!="undefined"){let d=u.offsetTop-p.offsetTop,{height:m}=Ae(p);p.scrollTo({top:d-m/2,behavior:l})}}})}return X("navigation.tracking")&&t.pipe(Q(a),fe("offset"),Be(250),ke(1),Q(o.pipe(ke(1))),jt({delay:250}),pe(i)).subscribe(([,{prev:s}])=>{let c=Ye(),l=s[s.length-1];if(l&&l.length){let[u]=l,{hash:p}=new URL(u.href);c.hash!==p&&(c.hash=p,history.replaceState({},"",`${c}`))}else c.hash="",history.replaceState({},"",`${c}`)}),Tp(e,{viewport$:t,header$:r}).pipe($(s=>i.next(s)),V(()=>i.complete()),f(s=>H({ref:e},s)))})}function Sp(e,{viewport$:t,main$:r,target$:n}){let o=t.pipe(f(({offset:{y:a}})=>a),Pt(2,1),f(([a,s])=>a>s&&s>0),ie()),i=r.pipe(f(({active:a})=>a));return re([i,o]).pipe(f(([a,s])=>!(a&&s)),ie(),Q(n.pipe(ke(1))),ye(!0),jt({delay:250}),f(a=>({hidden:a})))}function tc(e,{viewport$:t,header$:r,main$:n,target$:o}){let i=new I,a=i.pipe(he(),ye(!0));return i.subscribe({next({hidden:s}){e.hidden=s,s?(e.setAttribute("tabindex","-1"),e.blur()):e.removeAttribute("tabindex")},complete(){e.style.top="",e.hidden=!0,e.removeAttribute("tabindex")}}),r.pipe(Q(a),fe("height")).subscribe(({height:s})=>{e.style.top=`${s+16}px`}),b(e,"click").subscribe(s=>{s.preventDefault(),window.scrollTo({top:0})}),Sp(e,{viewport$:t,main$:n,target$:o}).pipe($(s=>i.next(s)),V(()=>i.complete()),f(s=>H({ref:e},s)))}function rc(e,t){return e.protocol=t.protocol,e.hostname=t.hostname,t.port&&(e.port=t.port),e}function Op(e,t){let r=new Map;for(let n of P("url",e)){let o=G("loc",n),i=[rc(new URL(o.textContent),t)];r.set(`${i[0]}`,i);for(let a of P("[rel=alternate]",n)){let s=a.getAttribute("href");s!=null&&i.push(rc(new URL(s),t))}}return r}function dr(e){return ns(new URL("sitemap.xml",e)).pipe(f(t=>Op(t,new URL(e))),_e(()=>Y(new Map)),xe())}function __ha_langroot(e){let t=new URL(e),r=t.pathname.match(/^\/(zh-hant|en|ja|ru)(?:\/|$)/);return t.pathname=r?`/${r[1]}/`:"/",t.search="",t.hash="",t}function nc({document$:e}){let t=new Map;e.pipe(g(()=>P("link[rel=alternate]")),f(r=>__ha_langroot(r.href)),L(r=>!t.has(r.toString())),oe(r=>dr(r).pipe(f(n=>[r,n]),_e(()=>y)))).subscribe(([r,n])=>{t.set(r.toString().replace(/\/$/,""),n)}),b(document.body,"click").pipe(L(r=>!r.metaKey&&!r.ctrlKey),g(r=>{if(r.target instanceof Element){let n=r.target.closest("a");if(n&&!n.target){let o=[...t].find(([p])=>n.href.startsWith(`${p}/`));if(typeof o=="undefined")return y;let[i,a]=o,s=Ye();if(s.href.startsWith(i))return y;let c=Ue(),l=s.href.replace(c.base,"");l=`${i}/${l}`;let u=a.has(l.split("#")[0])?new URL(l,c.base):new URL(i);return r.preventDefault(),Y(u)}}return y})).subscribe(r=>dt(r,!0))}var Co=_r(Mo());function Lp(e){e.setAttribute("data-md-copying","");let t=e.closest("[data-copy]"),r=t?t.getAttribute("data-copy"):e.innerText;return e.removeAttribute("data-md-copying"),r.trimEnd()}function oc({alert$:e}){Co.default.isSupported()&&new U(t=>{new Co.default("[data-clipboard-target], [data-clipboard-text]",{text:r=>r.getAttribute("data-clipboard-text")||Lp(G(r.getAttribute("data-clipboard-target")))}).on("success",r=>t.next(r))}).pipe($(t=>{t.trigger.focus()}),f(()=>Bt("clipboard.copied"))).subscribe(e)}function ic(e,t){if(!(e.target instanceof Element))return y;let r=e.target.closest("a");if(r===null)return y;if(r.target||e.metaKey||e.ctrlKey)return y;let n=new URL(r.href);return n.search=n.hash="",t.has(`${n}`)?(e.preventDefault(),Y(r)):y}function ac(e){let t=new Map;for(let r of P(":scope > *",e.head))t.set(r.outerHTML,r);return t}function sc(e){for(let t of P("[href], [src]",e))for(let r of["href","src"]){let n=t.getAttribute(r);if(n&&!/^(?:[a-z]+:)?\/\//i.test(n)){t[r]=t[r];break}}return Y(e)}function Mp(e){for(let n of["[data-md-component=announce]","[data-md-component=container]","[data-md-component=header-topic]","[data-md-component=outdated]","[data-md-component=logo]","[data-md-component=skip]",...X("navigation.tabs.sticky")?["[data-md-component=tabs]"]:[]]){let o=Le(n),i=Le(n,e);typeof o!="undefined"&&typeof i!="undefined"&&o.replaceWith(i)}let t=ac(document);for(let[n,o]of ac(e))t.has(n)?t.delete(n):document.head.appendChild(o);for(let n of t.values()){let o=n.getAttribute("name");o!=="theme-color"&&o!=="color-scheme"&&n.remove()}let r=ht("container");return nt(P("script",r)).pipe(g(n=>{let o=e.createElement("script");if(n.src){for(let i of n.getAttributeNames())o.setAttribute(i,n.getAttribute(i));return n.replaceWith(o),new U(i=>{o.onload=()=>i.complete()})}else return o.textContent=n.textContent,n.replaceWith(o),y}),he(),ye(document))}function cc({sitemap$:e,location$:t,viewport$:r,progress$:n}){if(location.protocol==="file:")return Ke;Y(document).subscribe(sc);let o=b(document.body,"click").pipe(Ze(e),g(([s,c])=>ic(s,c)),f(({href:s})=>new URL(s)),xe()),i=b(window,"popstate").pipe(f(Ye),xe());o.pipe(pe(r)).subscribe(([s,{offset:c}])=>{history.replaceState(c,""),history.pushState(null,"",s)}),R(o,i).subscribe(t);let a=t.pipe(fe("pathname"),g(s=>En(s,{progress$:n}).pipe(_e(()=>(dt(s,!0),y)))),g(sc),g(Mp),xe());return R(a.pipe(pe(t,(s,c)=>c)),a.pipe(g(()=>t),fe("hash")),t.pipe(ie((s,c)=>s.pathname===c.pathname&&s.hash===c.hash),g(()=>o),$(()=>history.back()))).subscribe(s=>{var c,l;history.state!==null||!s.hash?window.scrollTo(0,(l=(c=history.state)==null?void 0:c.y)!=null?l:0):(history.scrollRestoration="auto",es(s.hash),history.scrollRestoration="manual")}),t.subscribe(()=>{history.scrollRestoration="manual"}),b(window,"beforeunload").subscribe(()=>{history.scrollRestoration="auto"}),r.pipe(fe("offset"),Be(100)).subscribe(({offset:s})=>{history.replaceState(s,"")}),X("navigation.instant.prefetch")&&R(b(document.body,"mousemove"),b(document.body,"focusin")).pipe(Ze(e),g(([s,c])=>ic(s,c)),Be(25),Yn(({href:s})=>s),cn(s=>{let c=document.createElement("link");return c.rel="prefetch",c.href=s.toString(),document.head.appendChild(c),b(c,"load").pipe(f(()=>c),Me(1))})).subscribe(s=>s.remove()),a}function lc(e){var u;let{selectedVersionSitemap:t,selectedVersionBaseURL:r,currentLocation:n,currentBaseURL:o}=e,i=(u=Ho(o))==null?void 0:u.pathname;if(i===void 0)return;let a=kp(n.pathname,i);if(a===void 0)return;let s=Cp(t.keys());if(!t.has(s))return;let c=Ho(a,s);if(!c||!t.has(c.href))return;let l=Ho(a,r);if(l)return l.hash=n.hash,l.search=n.search,l}function Ho(e,t){try{return new URL(e,t)}catch(r){return}}function kp(e,t){if(e.startsWith(t))return e.slice(t.length)}function Ap(e,t){let r=Math.min(e.length,t.length),n;for(n=0;ny)),n=r.pipe(f(o=>{let[,i]=t.base.match(/([^/]+)\/?$/);return o.find(({version:a,aliases:s})=>a===i||s.includes(i))||o[0]}));r.pipe(f(o=>new Map(o.map(i=>[`${new URL(`../${i.version}/`,t.base)}`,i]))),g(o=>b(document.body,"click").pipe(L(i=>!i.metaKey&&!i.ctrlKey),pe(n),g(([i,a])=>{if(i.target instanceof Element){let s=i.target.closest("a");if(s&&!s.target&&o.has(s.href)){let c=s.href;return!i.target.closest(".md-version")&&o.get(c)===a?y:(i.preventDefault(),Y(new URL(c)))}}return y}),g(i=>dr(i).pipe(f(a=>{var s;return(s=lc({selectedVersionSitemap:a,selectedVersionBaseURL:i,currentLocation:Ye(),currentBaseURL:t.base}))!=null?s:i})))))).subscribe(o=>dt(o,!0)),re([r,n]).subscribe(([o,i])=>{G(".md-header__topic").appendChild(_s(o,i))}),e.pipe(g(()=>n)).subscribe(o=>{var s;let i=new URL(t.base),a=__md_get("__outdated",sessionStorage,i);if(a===null){a=!0;let c=((s=t.version)==null?void 0:s.default)||"latest";Array.isArray(c)||(c=[c]);e:for(let l of c)for(let u of o.aliases.concat(o.version))if(new RegExp(l,"i").test(u)){a=!1;break e}__md_set("__outdated",a,sessionStorage,i)}if(a)for(let c of Ee("outdated"))c.hidden=!1})}function pc({document$:e,viewport$:t}){e.pipe(g(()=>P(".md-ellipsis")),oe(r=>Et(r).pipe(Q(e.pipe(ke(1))),L(n=>n),f(()=>r),Me(1))),L(r=>r.offsetWidth{let n=r.innerText,o=r.closest("a")||r;return o.title=n,X("content.tooltips")?Ge(o,{viewport$:t}).pipe(Q(e.pipe(ke(1))),V(()=>o.removeAttribute("title"))):y})).subscribe(),X("content.tooltips")&&e.pipe(g(()=>P(".md-status")),oe(r=>Ge(r,{viewport$:t}))).subscribe()}function fc({document$:e,tablet$:t}){e.pipe(g(()=>P(".md-toggle--indeterminate")),$(r=>{r.indeterminate=!0,r.checked=!1}),oe(r=>b(r,"change").pipe(Xn(()=>r.classList.contains("md-toggle--indeterminate")),f(()=>r))),pe(t)).subscribe(([r,n])=>{r.classList.remove("md-toggle--indeterminate"),n&&(r.checked=!1)})}function Hp(){return/(iPad|iPhone|iPod)/.test(navigator.userAgent)}function mc({document$:e}){e.pipe(g(()=>P("[data-md-scrollfix]")),$(t=>t.removeAttribute("data-md-scrollfix")),L(Hp),oe(t=>b(t,"touchstart").pipe(f(()=>t)))).subscribe(t=>{let r=t.scrollTop;r===0?t.scrollTop=1:r+t.offsetHeight===t.scrollHeight&&(t.scrollTop=r-1)})}Object.entries||(Object.entries=function(e){let t=[];for(let r of Object.keys(e))t.push([r,e[r]]);return t});Object.values||(Object.values=function(e){let t=[];for(let r of Object.keys(e))t.push(e[r]);return t});typeof Element!="undefined"&&(Element.prototype.scrollTo||(Element.prototype.scrollTo=function(e,t){typeof e=="object"?(this.scrollLeft=e.left,this.scrollTop=e.top):(this.scrollLeft=e,this.scrollTop=t)}),Element.prototype.replaceWith||(Element.prototype.replaceWith=function(...e){let t=this.parentNode;if(t){e.length===0&&t.removeChild(this);for(let r=e.length-1;r>=0;r--){let n=e[r];typeof n=="string"?n=document.createTextNode(n):n.parentNode&&n.parentNode.removeChild(n),r?t.insertBefore(this.previousSibling,n):t.replaceChild(n,this)}}}));function $p(){return location.protocol==="file:"?ar(`${new URL("search.js",Mn.base)}`).pipe(f(()=>__index),_e(()=>Ke),se(1)):et(new URL("search.json",Mn.base))}document.documentElement.classList.remove("no-js");document.documentElement.classList.add("js");var vt=Si(),Ur=Za(),hr=ts(Ur),hc=Xa(),ze=cs(),$o=Ir("(min-width: 60em)"),vc=Ir("(min-width: 76.25em)"),bc=rs(),Mn=Ue(),gc=Le(".md-search")?$p():Ke,Po=new I;oc({alert$:Po});nc({document$:vt});var Io=new I,_c=dr(Mn.base);X("navigation.instant")&&cc({sitemap$:_c,location$:Ur,viewport$:ze,progress$:Io}).subscribe(vt);var dc;((dc=Mn.version)==null?void 0:dc.provider)==="mike"&&uc({document$:vt});R(Ur,hr).pipe(It(125)).subscribe(()=>{Eo("drawer",!1),Eo("search",!1)});hc.pipe(L(({mode:e,meta:t})=>e==="global"&&!t)).subscribe(e=>{switch(e.type){case",":case"p":let t=document.querySelector("link[rel=prev]");t instanceof HTMLLinkElement&&dt(t);break;case".":case"n":let r=document.querySelector("link[rel=next]");r instanceof HTMLLinkElement&&dt(r);break;case"/":let n=document.querySelector("[data-md-component=search] button");n instanceof HTMLButtonElement&&n.click();break;case"Enter":let o=xt();o instanceof HTMLLabelElement&&o.click()}});pc({viewport$:ze,document$:vt});fc({document$:vt,tablet$:$o});mc({document$:vt});var Lt=Us(ht("header"),{viewport$:ze}),Fr=vt.pipe(f(()=>ht("main")),g(e=>Ws(e,{viewport$:ze,header$:Lt})),se(1)),Pp=R(...Ee("consent").map(e=>us(e,{target$:hr})),...Ee("dialog").map(e=>Fs(e,{alert$:Po})),...Ee("palette").map(e=>Vs(e)),...Ee("progress").map(e=>zs(e,{progress$:Io})),...Ee("search").map(e=>Ks(e,{index$:gc})),...Ee("source").map(e=>Zs(e))),Ip=j(()=>R(...Ee("announce").map(e=>ls(e)),...Ee("content").map(e=>js(e,{sitemap$:_c,viewport$:ze,target$:hr,print$:bc})),...Ee("content").map(e=>X("search.highlight")?Ys(e,{index$:gc,location$:Ur}):y),...Ee("header").map(e=>Ns(e,{viewport$:ze,header$:Lt,main$:Fr})),...Ee("header-title").map(e=>Ds(e,{viewport$:ze,header$:Lt})),...Ee("sidebar").map(e=>e.getAttribute("data-md-type")==="navigation"?yo(vc,()=>Ao(e,{viewport$:ze,header$:Lt,main$:Fr})):yo($o,()=>Ao(e,{viewport$:ze,header$:Lt,main$:Fr}))),...Ee("tabs").map(e=>Qs(e,{viewport$:ze,header$:Lt})),...Ee("toc").map(e=>ec(e,{viewport$:ze,header$:Lt,main$:Fr,target$:hr})),...Ee("top").map(e=>tc(e,{viewport$:ze,header$:Lt,main$:Fr,target$:hr})))),yc=vt.pipe(g(()=>Ip),Rt(Pp),se(1));yc.subscribe();window.document$=vt;window.location$=Ur;window.target$=hr;window.keyboard$=hc;window.viewport$=ze;window.tablet$=$o;window.screen$=vc;window.print$=bc;window.alert$=Po;window.progress$=Io;window.component$=yc;})(); -/*! update cache: 20260410225937 */ +/*! update cache: 20260414173625 */ diff --git a/ja/javascripts/animation_player.js b/ja/javascripts/animation_player.js index e03078044..e3cee1071 100644 --- a/ja/javascripts/animation_player.js +++ b/ja/javascripts/animation_player.js @@ -251,4 +251,4 @@ initAutoSlide(); } })(); -/*! update cache: 20260410225937 */ +/*! update cache: 20260414173625 */ diff --git a/ja/javascripts/katex.js b/ja/javascripts/katex.js index 190b7e4e0..a677c9018 100644 --- a/ja/javascripts/katex.js +++ b/ja/javascripts/katex.js @@ -8,4 +8,4 @@ document$.subscribe(({ body }) => { ], }); }); -/*! update cache: 20260410225937 */ +/*! update cache: 20260414173625 */ diff --git a/ja/javascripts/mathjax.js b/ja/javascripts/mathjax.js index a9d80afa9..ad7cfa386 100644 --- a/ja/javascripts/mathjax.js +++ b/ja/javascripts/mathjax.js @@ -15,4 +15,4 @@ window.MathJax = { document$.subscribe(() => { MathJax.typesetPromise(); }); -/*! update cache: 20260410225937 */ +/*! update cache: 20260414173625 */ diff --git a/ja/javascripts/starfield.js b/ja/javascripts/starfield.js index 27631f65a..faba1c208 100644 --- a/ja/javascripts/starfield.js +++ b/ja/javascripts/starfield.js @@ -469,4 +469,4 @@ return Starfield; }); -/*! update cache: 20260410225937 */ +/*! update cache: 20260414173625 */ diff --git a/ja/stylesheets/animation_player.css b/ja/stylesheets/animation_player.css index dd4d473a0..97d79ecd4 100644 --- a/ja/stylesheets/animation_player.css +++ b/ja/stylesheets/animation_player.css @@ -176,4 +176,4 @@ font-size: 0.7rem; } } -/*! update cache: 20260410225937 */ +/*! update cache: 20260414173625 */ diff --git a/ja/stylesheets/extra.css b/ja/stylesheets/extra.css index db2d32232..5de7f3628 100644 --- a/ja/stylesheets/extra.css +++ b/ja/stylesheets/extra.css @@ -806,4 +806,4 @@ a:hover .device-on-hover { margin: 0 0 1em; } } -/*! update cache: 20260410225937 */ +/*! update cache: 20260414173625 */ diff --git a/ja/stylesheets/giscus-dark.css b/ja/stylesheets/giscus-dark.css index d43ea40a6..73c62fa61 100644 --- a/ja/stylesheets/giscus-dark.css +++ b/ja/stylesheets/giscus-dark.css @@ -122,4 +122,4 @@ main .gsc-loading-image { .gsc-reply-content::-webkit-scrollbar-track { background: transparent; } -/*! update cache: 20260410225937 */ +/*! update cache: 20260414173625 */ diff --git a/ja/stylesheets/giscus-light.css b/ja/stylesheets/giscus-light.css index 10e4b63bd..901f02e20 100644 --- a/ja/stylesheets/giscus-light.css +++ b/ja/stylesheets/giscus-light.css @@ -153,4 +153,4 @@ main { .gsc-reply-content::-webkit-scrollbar-track { background: transparent; } -/*! update cache: 20260410225937 */ +/*! update cache: 20260414173625 */ diff --git a/javascripts/animation_player.js b/javascripts/animation_player.js index e0fffe906..e72d4a5c1 100644 --- a/javascripts/animation_player.js +++ b/javascripts/animation_player.js @@ -251,4 +251,4 @@ initAutoSlide(); } })(); -/*! update cache: 20260410225905 */ +/*! update cache: 20260414173552 */ diff --git a/javascripts/katex.js b/javascripts/katex.js index bea24dbd0..b3cf33269 100644 --- a/javascripts/katex.js +++ b/javascripts/katex.js @@ -8,4 +8,4 @@ document$.subscribe(({ body }) => { ], }); }); -/*! update cache: 20260410225905 */ +/*! update cache: 20260414173552 */ diff --git a/javascripts/mathjax.js b/javascripts/mathjax.js index 2c4c67b82..7d52b1a52 100644 --- a/javascripts/mathjax.js +++ b/javascripts/mathjax.js @@ -15,4 +15,4 @@ window.MathJax = { document$.subscribe(() => { MathJax.typesetPromise(); }); -/*! update cache: 20260410225905 */ +/*! update cache: 20260414173552 */ diff --git a/javascripts/starfield.js b/javascripts/starfield.js index ba0edc17a..ff83f699c 100644 --- a/javascripts/starfield.js +++ b/javascripts/starfield.js @@ -469,4 +469,4 @@ return Starfield; }); -/*! update cache: 20260410225905 */ +/*! update cache: 20260414173552 */ diff --git a/ru/assets/javascripts/bundle.c2b142ea.min.js b/ru/assets/javascripts/bundle.c2b142ea.min.js index c3f0970e2..5e61e03ee 100644 --- a/ru/assets/javascripts/bundle.c2b142ea.min.js +++ b/ru/assets/javascripts/bundle.c2b142ea.min.js @@ -1,4 +1,4 @@ "use strict";(()=>{var xc=Object.create;var kn=Object.defineProperty,wc=Object.defineProperties,Ec=Object.getOwnPropertyDescriptor,Tc=Object.getOwnPropertyDescriptors,Sc=Object.getOwnPropertyNames,Dr=Object.getOwnPropertySymbols,Oc=Object.getPrototypeOf,An=Object.prototype.hasOwnProperty,Fo=Object.prototype.propertyIsEnumerable;var jo=(e,t,r)=>t in e?kn(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,H=(e,t)=>{for(var r in t||(t={}))An.call(t,r)&&jo(e,r,t[r]);if(Dr)for(var r of Dr(t))Fo.call(t,r)&&jo(e,r,t[r]);return e},He=(e,t)=>wc(e,Tc(t));var gr=(e,t)=>{var r={};for(var n in e)An.call(e,n)&&t.indexOf(n)<0&&(r[n]=e[n]);if(e!=null&&Dr)for(var n of Dr(e))t.indexOf(n)<0&&Fo.call(e,n)&&(r[n]=e[n]);return r};var Cn=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var Lc=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of Sc(t))!An.call(e,o)&&o!==r&&kn(e,o,{get:()=>t[o],enumerable:!(n=Ec(t,o))||n.enumerable});return e};var _r=(e,t,r)=>(r=e!=null?xc(Oc(e)):{},Lc(t||!e||!e.__esModule?kn(r,"default",{value:e,enumerable:!0}):r,e));var Uo=(e,t,r)=>new Promise((n,o)=>{var i=c=>{try{s(r.next(c))}catch(l){o(l)}},a=c=>{try{s(r.throw(c))}catch(l){o(l)}},s=c=>c.done?n(c.value):Promise.resolve(c.value).then(i,a);s((r=r.apply(e,t)).next())});var Do=Cn((Hn,No)=>{(function(e,t){typeof Hn=="object"&&typeof No!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(Hn,(function(){"use strict";function e(r){var n=!0,o=!1,i=null,a={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function s(_){return!!(_&&_!==document&&_.nodeName!=="HTML"&&_.nodeName!=="BODY"&&"classList"in _&&"contains"in _.classList)}function c(_){var de=_.type,be=_.tagName;return!!(be==="INPUT"&&a[de]&&!_.readOnly||be==="TEXTAREA"&&!_.readOnly||_.isContentEditable)}function l(_){_.classList.contains("focus-visible")||(_.classList.add("focus-visible"),_.setAttribute("data-focus-visible-added",""))}function u(_){_.hasAttribute("data-focus-visible-added")&&(_.classList.remove("focus-visible"),_.removeAttribute("data-focus-visible-added"))}function p(_){_.metaKey||_.altKey||_.ctrlKey||(s(r.activeElement)&&l(r.activeElement),n=!0)}function d(_){n=!1}function m(_){s(_.target)&&(n||c(_.target))&&l(_.target)}function h(_){s(_.target)&&(_.target.classList.contains("focus-visible")||_.target.hasAttribute("data-focus-visible-added"))&&(o=!0,window.clearTimeout(i),i=window.setTimeout(function(){o=!1},100),u(_.target))}function v(_){document.visibilityState==="hidden"&&(o&&(n=!0),x())}function x(){document.addEventListener("mousemove",E),document.addEventListener("mousedown",E),document.addEventListener("mouseup",E),document.addEventListener("pointermove",E),document.addEventListener("pointerdown",E),document.addEventListener("pointerup",E),document.addEventListener("touchmove",E),document.addEventListener("touchstart",E),document.addEventListener("touchend",E)}function w(){document.removeEventListener("mousemove",E),document.removeEventListener("mousedown",E),document.removeEventListener("mouseup",E),document.removeEventListener("pointermove",E),document.removeEventListener("pointerdown",E),document.removeEventListener("pointerup",E),document.removeEventListener("touchmove",E),document.removeEventListener("touchstart",E),document.removeEventListener("touchend",E)}function E(_){_.target.nodeName&&_.target.nodeName.toLowerCase()==="html"||(n=!1,w())}document.addEventListener("keydown",p,!0),document.addEventListener("mousedown",d,!0),document.addEventListener("pointerdown",d,!0),document.addEventListener("touchstart",d,!0),document.addEventListener("visibilitychange",v,!0),x(),r.addEventListener("focus",m,!0),r.addEventListener("blur",h,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)}))});var So=Cn((M0,vs)=>{"use strict";var Gu=/["'&<>]/;vs.exports=Ju;function Ju(e){var t=""+e,r=Gu.exec(t);if(!r)return t;var n,o="",i=0,a=0;for(i=r.index;i{(function(t,r){typeof jr=="object"&&typeof Lo=="object"?Lo.exports=r():typeof define=="function"&&define.amd?define([],r):typeof jr=="object"?jr.ClipboardJS=r():t.ClipboardJS=r()})(jr,function(){return(function(){var e={686:(function(n,o,i){"use strict";i.d(o,{default:function(){return vr}});var a=i(279),s=i.n(a),c=i(370),l=i.n(c),u=i(817),p=i.n(u);function d(B){try{return document.execCommand(B)}catch(C){return!1}}var m=function(C){var k=p()(C);return d("cut"),k},h=m;function v(B){var C=document.documentElement.getAttribute("dir")==="rtl",k=document.createElement("textarea");k.style.fontSize="12pt",k.style.border="0",k.style.padding="0",k.style.margin="0",k.style.position="absolute",k.style[C?"right":"left"]="-9999px";var D=window.pageYOffset||document.documentElement.scrollTop;return k.style.top="".concat(D,"px"),k.setAttribute("readonly",""),k.value=B,k}var x=function(C,k){var D=v(C);k.container.appendChild(D);var W=p()(D);return d("copy"),D.remove(),W},w=function(C){var k=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},D="";return typeof C=="string"?D=x(C,k):C instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(C==null?void 0:C.type)?D=x(C.value,k):(D=p()(C),d("copy")),D},E=w;function _(B){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?_=function(k){return typeof k}:_=function(k){return k&&typeof Symbol=="function"&&k.constructor===Symbol&&k!==Symbol.prototype?"symbol":typeof k},_(B)}var de=function(){var C=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},k=C.action,D=k===void 0?"copy":k,W=C.container,Z=C.target,We=C.text;if(D!=="copy"&&D!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(Z!==void 0)if(Z&&_(Z)==="object"&&Z.nodeType===1){if(D==="copy"&&Z.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(D==="cut"&&(Z.hasAttribute("readonly")||Z.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if(We)return E(We,{container:W});if(Z)return D==="cut"?h(Z):E(Z,{container:W})},be=de;function M(B){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?M=function(k){return typeof k}:M=function(k){return k&&typeof Symbol=="function"&&k.constructor===Symbol&&k!==Symbol.prototype?"symbol":typeof k},M(B)}function O(B,C){if(!(B instanceof C))throw new TypeError("Cannot call a class as a function")}function N(B,C){for(var k=0;k0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof W.action=="function"?W.action:this.defaultAction,this.target=typeof W.target=="function"?W.target:this.defaultTarget,this.text=typeof W.text=="function"?W.text:this.defaultText,this.container=M(W.container)==="object"?W.container:document.body}},{key:"listenClick",value:function(W){var Z=this;this.listener=l()(W,"click",function(We){return Z.onClick(We)})}},{key:"onClick",value:function(W){var Z=W.delegateTarget||W.currentTarget,We=this.action(Z)||"copy",Gt=be({action:We,container:this.container,target:this.target(Z),text:this.text(Z)});this.emit(Gt?"success":"error",{action:We,text:Gt,trigger:Z,clearSelection:function(){Z&&Z.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(W){return Yt("action",W)}},{key:"defaultTarget",value:function(W){var Z=Yt("target",W);if(Z)return document.querySelector(Z)}},{key:"defaultText",value:function(W){return Yt("text",W)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(W){var Z=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return E(W,Z)}},{key:"cut",value:function(W){return h(W)}},{key:"isSupported",value:function(){var W=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],Z=typeof W=="string"?[W]:W,We=!!document.queryCommandSupported;return Z.forEach(function(Gt){We=We&&!!document.queryCommandSupported(Gt)}),We}}]),k})(s()),vr=Mt}),828:(function(n){var o=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function a(s,c){for(;s&&s.nodeType!==o;){if(typeof s.matches=="function"&&s.matches(c))return s;s=s.parentNode}}n.exports=a}),438:(function(n,o,i){var a=i(828);function s(u,p,d,m,h){var v=l.apply(this,arguments);return u.addEventListener(d,v,h),{destroy:function(){u.removeEventListener(d,v,h)}}}function c(u,p,d,m,h){return typeof u.addEventListener=="function"?s.apply(null,arguments):typeof d=="function"?s.bind(null,document).apply(null,arguments):(typeof u=="string"&&(u=document.querySelectorAll(u)),Array.prototype.map.call(u,function(v){return s(v,p,d,m,h)}))}function l(u,p,d,m){return function(h){h.delegateTarget=a(h.target,p),h.delegateTarget&&m.call(u,h)}}n.exports=c}),879:(function(n,o){o.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},o.nodeList=function(i){var a=Object.prototype.toString.call(i);return i!==void 0&&(a==="[object NodeList]"||a==="[object HTMLCollection]")&&"length"in i&&(i.length===0||o.node(i[0]))},o.string=function(i){return typeof i=="string"||i instanceof String},o.fn=function(i){var a=Object.prototype.toString.call(i);return a==="[object Function]"}}),370:(function(n,o,i){var a=i(879),s=i(438);function c(d,m,h){if(!d&&!m&&!h)throw new Error("Missing required arguments");if(!a.string(m))throw new TypeError("Second argument must be a String");if(!a.fn(h))throw new TypeError("Third argument must be a Function");if(a.node(d))return l(d,m,h);if(a.nodeList(d))return u(d,m,h);if(a.string(d))return p(d,m,h);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function l(d,m,h){return d.addEventListener(m,h),{destroy:function(){d.removeEventListener(m,h)}}}function u(d,m,h){return Array.prototype.forEach.call(d,function(v){v.addEventListener(m,h)}),{destroy:function(){Array.prototype.forEach.call(d,function(v){v.removeEventListener(m,h)})}}}function p(d,m,h){return s(document.body,d,m,h)}n.exports=c}),817:(function(n){function o(i){var a;if(i.nodeName==="SELECT")i.focus(),a=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var s=i.hasAttribute("readonly");s||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),s||i.removeAttribute("readonly"),a=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var c=window.getSelection(),l=document.createRange();l.selectNodeContents(i),c.removeAllRanges(),c.addRange(l),a=c.toString()}return a}n.exports=o}),279:(function(n){function o(){}o.prototype={on:function(i,a,s){var c=this.e||(this.e={});return(c[i]||(c[i]=[])).push({fn:a,ctx:s}),this},once:function(i,a,s){var c=this;function l(){c.off(i,l),a.apply(s,arguments)}return l._=a,this.on(i,l,s)},emit:function(i){var a=[].slice.call(arguments,1),s=((this.e||(this.e={}))[i]||[]).slice(),c=0,l=s.length;for(c;c0&&i[i.length-1])&&(l[0]===6||l[0]===2)){r=0;continue}if(l[0]===3&&(!i||l[1]>i[0]&&l[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function te(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var n=r.call(e),o,i=[],a;try{for(;(t===void 0||t-- >0)&&!(o=n.next()).done;)i.push(o.value)}catch(s){a={error:s}}finally{try{o&&!o.done&&(r=n.return)&&r.call(n)}finally{if(a)throw a.error}}return i}function ne(e,t,r){if(r||arguments.length===2)for(var n=0,o=t.length,i;n1||c(m,v)})},h&&(o[m]=h(o[m])))}function c(m,h){try{l(n[m](h))}catch(v){d(i[0][3],v)}}function l(m){m.value instanceof kt?Promise.resolve(m.value.v).then(u,p):d(i[0][2],m)}function u(m){c("next",m)}function p(m){c("throw",m)}function d(m,h){m(h),i.shift(),i.length&&c(i[0][0],i[0][1])}}function zo(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],r;return t?t.call(e):(e=typeof $e=="function"?$e(e):e[Symbol.iterator](),r={},n("next"),n("throw"),n("return"),r[Symbol.asyncIterator]=function(){return this},r);function n(i){r[i]=e[i]&&function(a){return new Promise(function(s,c){a=e[i](a),o(s,c,a.done,a.value)})}}function o(i,a,s,c){Promise.resolve(c).then(function(l){i({value:l,done:s})},a)}}function F(e){return typeof e=="function"}function Jt(e){var t=function(n){Error.call(n),n.stack=new Error().stack},r=e(t);return r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r}var Vr=Jt(function(e){return function(r){e(this),this.message=r?r.length+` errors occurred during unsubscription: `+r.map(function(n,o){return o+1+") "+n.toString()}).join(` `):"",this.name="UnsubscriptionError",this.errors=r}});function ct(e,t){if(e){var r=e.indexOf(t);0<=r&&e.splice(r,1)}}var rt=(function(){function e(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var t,r,n,o,i;if(!this.closed){this.closed=!0;var a=this._parentage;if(a)if(this._parentage=null,Array.isArray(a))try{for(var s=$e(a),c=s.next();!c.done;c=s.next()){var l=c.value;l.remove(this)}}catch(v){t={error:v}}finally{try{c&&!c.done&&(r=s.return)&&r.call(s)}finally{if(t)throw t.error}}else a.remove(this);var u=this.initialTeardown;if(F(u))try{u()}catch(v){i=v instanceof Vr?v.errors:[v]}var p=this._finalizers;if(p){this._finalizers=null;try{for(var d=$e(p),m=d.next();!m.done;m=d.next()){var h=m.value;try{qo(h)}catch(v){i=i!=null?i:[],v instanceof Vr?i=ne(ne([],te(i)),te(v.errors)):i.push(v)}}}catch(v){n={error:v}}finally{try{m&&!m.done&&(o=d.return)&&o.call(d)}finally{if(n)throw n.error}}}if(i)throw new Vr(i)}},e.prototype.add=function(t){var r;if(t&&t!==this)if(this.closed)qo(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(r=this._finalizers)!==null&&r!==void 0?r:[]).push(t)}},e.prototype._hasParent=function(t){var r=this._parentage;return r===t||Array.isArray(r)&&r.includes(t)},e.prototype._addParent=function(t){var r=this._parentage;this._parentage=Array.isArray(r)?(r.push(t),r):r?[r,t]:t},e.prototype._removeParent=function(t){var r=this._parentage;r===t?this._parentage=null:Array.isArray(r)&&ct(r,t)},e.prototype.remove=function(t){var r=this._finalizers;r&&ct(r,t),t instanceof e&&t._removeParent(this)},e.EMPTY=(function(){var t=new e;return t.closed=!0,t})(),e})();var Pn=rt.EMPTY;function zr(e){return e instanceof rt||e&&"closed"in e&&F(e.remove)&&F(e.add)&&F(e.unsubscribe)}function qo(e){F(e)?e():e.unsubscribe()}var Je={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var Xt={setTimeout:function(e,t){for(var r=[],n=2;n0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var n=this,o=this,i=o.hasError,a=o.isStopped,s=o.observers;return i||a?Pn:(this.currentObservers=null,s.push(r),new rt(function(){n.currentObservers=null,ct(s,r)}))},t.prototype._checkFinalizedStatuses=function(r){var n=this,o=n.hasError,i=n.thrownError,a=n.isStopped;o?r.error(i):a&&r.complete()},t.prototype.asObservable=function(){var r=new U;return r.source=this,r},t.create=function(r,n){return new Qo(r,n)},t})(U);var Qo=(function(e){ue(t,e);function t(r,n){var o=e.call(this)||this;return o.destination=r,o.source=n,o}return t.prototype.next=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.next)===null||o===void 0||o.call(n,r)},t.prototype.error=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.error)===null||o===void 0||o.call(n,r)},t.prototype.complete=function(){var r,n;(n=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||n===void 0||n.call(r)},t.prototype._subscribe=function(r){var n,o;return(o=(n=this.source)===null||n===void 0?void 0:n.subscribe(r))!==null&&o!==void 0?o:Pn},t})(I);var Un=(function(e){ue(t,e);function t(r){var n=e.call(this)||this;return n._value=r,n}return Object.defineProperty(t.prototype,"value",{get:function(){return this.getValue()},enumerable:!1,configurable:!0}),t.prototype._subscribe=function(r){var n=e.prototype._subscribe.call(this,r);return!n.closed&&r.next(this._value),n},t.prototype.getValue=function(){var r=this,n=r.hasError,o=r.thrownError,i=r._value;if(n)throw o;return this._throwIfClosed(),i},t.prototype.next=function(r){e.prototype.next.call(this,this._value=r)},t})(I);var xr={now:function(){return(xr.delegate||Date).now()},delegate:void 0};var wr=(function(e){ue(t,e);function t(r,n,o){r===void 0&&(r=1/0),n===void 0&&(n=1/0),o===void 0&&(o=xr);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=n,i._timestampProvider=o,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=n===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,n),i}return t.prototype.next=function(r){var n=this,o=n.isStopped,i=n._buffer,a=n._infiniteTimeWindow,s=n._timestampProvider,c=n._windowTime;o||(i.push(r),!a&&i.push(s.now()+c)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var n=this._innerSubscribe(r),o=this,i=o._infiniteTimeWindow,a=o._buffer,s=a.slice(),c=0;c0?e.prototype.schedule.call(this,r,n):(this.delay=n,this.state=r,this.scheduler.flush(this),this)},t.prototype.execute=function(r,n){return n>0||this.closed?e.prototype.execute.call(this,r,n):this._execute(r,n)},t.prototype.requestAsyncId=function(r,n,o){return o===void 0&&(o=0),o!=null&&o>0||o==null&&this.delay>0?e.prototype.requestAsyncId.call(this,r,n,o):(r.flush(this),0)},t})(tr);var ri=(function(e){ue(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t})(rr);var Wn=new ri(ti);var ni=(function(e){ue(t,e);function t(r,n){var o=e.call(this,r,n)||this;return o.scheduler=r,o.work=n,o}return t.prototype.requestAsyncId=function(r,n,o){return o===void 0&&(o=0),o!==null&&o>0?e.prototype.requestAsyncId.call(this,r,n,o):(r.actions.push(this),r._scheduled||(r._scheduled=er.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,n,o){var i;if(o===void 0&&(o=0),o!=null?o>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,n,o);var a=r.actions;n!=null&&n===r._scheduled&&((i=a[a.length-1])===null||i===void 0?void 0:i.id)!==n&&(er.cancelAnimationFrame(n),r._scheduled=void 0)},t})(tr);var oi=(function(e){ue(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var n;r?n=r.id:(n=this._scheduled,this._scheduled=void 0);var o=this.actions,i;r=r||o.shift();do if(i=r.execute(r.state,r.delay))break;while((r=o[0])&&r.id===n&&o.shift());if(this._active=!1,i){for(;(r=o[0])&&r.id===n&&o.shift();)r.unsubscribe();throw i}},t})(rr);var je=new oi(ni);var y=new U(function(e){return e.complete()});function Br(e){return e&&F(e.schedule)}function Vn(e){return e[e.length-1]}function _t(e){return F(Vn(e))?e.pop():void 0}function qe(e){return Br(Vn(e))?e.pop():void 0}function Yr(e,t){return typeof Vn(e)=="number"?e.pop():t}var nr=(function(e){return e&&typeof e.length=="number"&&typeof e!="function"});function Gr(e){return F(e==null?void 0:e.then)}function Jr(e){return F(e[Qt])}function Xr(e){return Symbol.asyncIterator&&F(e==null?void 0:e[Symbol.asyncIterator])}function Zr(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function Rc(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var Qr=Rc();function en(e){return F(e==null?void 0:e[Qr])}function tn(e){return Vo(this,arguments,function(){var r,n,o,i;return Wr(this,function(a){switch(a.label){case 0:r=e.getReader(),a.label=1;case 1:a.trys.push([1,,9,10]),a.label=2;case 2:return[4,kt(r.read())];case 3:return n=a.sent(),o=n.value,i=n.done,i?[4,kt(void 0)]:[3,5];case 4:return[2,a.sent()];case 5:return[4,kt(o)];case 6:return[4,a.sent()];case 7:return a.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function rn(e){return F(e==null?void 0:e.getReader)}function q(e){if(e instanceof U)return e;if(e!=null){if(Jr(e))return jc(e);if(nr(e))return Fc(e);if(Gr(e))return Uc(e);if(Xr(e))return ii(e);if(en(e))return Nc(e);if(rn(e))return Dc(e)}throw Zr(e)}function jc(e){return new U(function(t){var r=e[Qt]();if(F(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function Fc(e){return new U(function(t){for(var r=0;r=2;return function(n){return n.pipe(e?L(function(o,i){return e(o,i,n)}):Oe,Me(1),r?ot(t):wi(function(){return new on}))}}function Gn(e){return e<=0?function(){return y}:S(function(t,r){var n=[];t.subscribe(T(r,function(o){n.push(o),e=2,!0))}function xe(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new I}:t,n=e.resetOnError,o=n===void 0?!0:n,i=e.resetOnComplete,a=i===void 0?!0:i,s=e.resetOnRefCountZero,c=s===void 0?!0:s;return function(l){var u,p,d,m=0,h=!1,v=!1,x=function(){p==null||p.unsubscribe(),p=void 0},w=function(){x(),u=d=void 0,h=v=!1},E=function(){var _=u;w(),_==null||_.unsubscribe()};return S(function(_,de){m++,!v&&!h&&x();var be=d=d!=null?d:r();de.add(function(){m--,m===0&&!v&&!h&&(p=Jn(E,c))}),be.subscribe(de),!u&&m>0&&(u=new Ct({next:function(M){return be.next(M)},error:function(M){v=!0,x(),p=Jn(w,o,M),be.error(M)},complete:function(){h=!0,x(),p=Jn(w,a),be.complete()}}),q(_).subscribe(u))})(l)}}function Jn(e,t){for(var r=[],n=2;ne.next(document)),e}function P(e,t=document){return Array.from(t.querySelectorAll(e))}function G(e,t=document){let r=Le(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function Le(e,t=document){return t.querySelector(e)||void 0}function xt(){var e,t,r,n;return(n=(r=(t=(e=document.activeElement)==null?void 0:e.shadowRoot)==null?void 0:t.activeElement)!=null?r:document.activeElement)!=null?n:void 0}var il=R(b(document.body,"focusin"),b(document.body,"focusout")).pipe(Be(1),J(void 0),f(()=>xt()||document.body),se(1));function ir(e){return il.pipe(f(t=>e.contains(t)),ie())}function Ft(e,t){let{matches:r}=matchMedia("(hover)");return j(()=>(r?R(b(e,"mouseenter").pipe(f(()=>!0)),b(e,"mouseleave").pipe(f(()=>!1))):R(b(e,"touchstart").pipe(f(()=>!0)),b(e,"touchend").pipe(f(()=>!1)),b(e,"touchcancel").pipe(f(()=>!1)))).pipe(t?Tr(o=>Ve(+!o*t)):Oe,J(!0,e.matches(":hover"))))}function Oi(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)Oi(e,r)}function A(e,t,...r){let n=document.createElement(e);if(t)for(let o of Object.keys(t))typeof t[o]!="undefined"&&(typeof t[o]!="boolean"?n.setAttribute(o,t[o]):n.setAttribute(o,""));for(let o of r)Oi(n,o);return n}function Li(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function ar(e){let t=A("script",{src:e});return j(()=>(document.head.appendChild(t),R(b(t,"load"),b(t,"error").pipe(g(()=>zn(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(f(()=>{}),V(()=>document.head.removeChild(t)),Me(1))))}var Mi=new I,al=j(()=>typeof ResizeObserver=="undefined"?ar("https://unpkg.com/resize-observer-polyfill"):Y(void 0)).pipe(f(()=>new ResizeObserver(e=>e.forEach(t=>Mi.next(t)))),g(e=>R(Ke,Y(e)).pipe(V(()=>e.disconnect()))),se(1));function Ae(e){return{width:e.offsetWidth,height:e.offsetHeight}}function Re(e){let t=e;for(;t.clientWidth===0&&t.parentElement;)t=t.parentElement;return al.pipe($(r=>r.observe(t)),g(r=>Mi.pipe(L(n=>n.target===t),V(()=>r.unobserve(t)))),f(()=>Ae(e)),J(Ae(e)))}function Mr(e){return{width:e.scrollWidth,height:e.scrollHeight}}function ki(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}function Ai(e){let t=[],r=e.parentElement;for(;r;)(e.clientWidth>r.clientWidth||e.clientHeight>r.clientHeight)&&t.push(r),r=(e=r).parentElement;return t.length===0&&t.push(document.documentElement),t}function wt(e){return{x:e.offsetLeft,y:e.offsetTop}}function Ci(e){let t=e.getBoundingClientRect();return{x:t.x+window.scrollX,y:t.y+window.scrollY}}function Hi(e){return R(b(window,"load"),b(window,"resize")).pipe(Xe(0,je),f(()=>wt(e)),J(wt(e)))}function ln(e){return{x:e.scrollLeft,y:e.scrollTop}}function Ut(e){return R(b(e,"scroll"),b(window,"scroll"),b(window,"resize")).pipe(Xe(0,je),f(()=>ln(e)),J(ln(e)))}var $i=new I,sl=j(()=>Y(new IntersectionObserver(e=>{for(let t of e)$i.next(t)},{threshold:0}))).pipe(g(e=>R(Ke,Y(e)).pipe(V(()=>e.disconnect()))),se(1));function Et(e){return sl.pipe($(t=>t.observe(e)),g(t=>$i.pipe(L(({target:r})=>r===e),V(()=>t.unobserve(e)),f(({isIntersecting:r})=>r))))}var cl=Object.create,la=Object.defineProperty,ll=Object.getOwnPropertyDescriptor,ul=Object.getOwnPropertyNames,pl=Object.getPrototypeOf,fl=Object.prototype.hasOwnProperty,ml=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),dl=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of ul(t))!fl.call(e,o)&&o!==r&&la(e,o,{get:()=>t[o],enumerable:!(n=ll(t,o))||n.enumerable});return e},hl=(e,t,r)=>(r=e!=null?cl(pl(e)):{},dl(t||!e||!e.__esModule?la(r,"default",{value:e,enumerable:!0}):r,e)),vl=ml((e,t)=>{var r="Expected a function",n=NaN,o="[object Symbol]",i=/^\s+|\s+$/g,a=/^[-+]0x[0-9a-f]+$/i,s=/^0b[01]+$/i,c=/^0o[0-7]+$/i,l=parseInt,u=typeof global=="object"&&global&&global.Object===Object&&global,p=typeof self=="object"&&self&&self.Object===Object&&self,d=u||p||Function("return this")(),m=Object.prototype,h=m.toString,v=Math.max,x=Math.min,w=function(){return d.Date.now()};function E(O,N,ee){var le,ce,Ne,bt,De,st,tt=0,Yt=!1,Mt=!1,vr=!0;if(typeof O!="function")throw new TypeError(r);N=M(N)||0,_(ee)&&(Yt=!!ee.leading,Mt="maxWait"in ee,Ne=Mt?v(M(ee.maxWait)||0,N):Ne,vr="trailing"in ee?!!ee.trailing:vr);function B(Te){var gt=le,br=ce;return le=ce=void 0,tt=Te,bt=O.apply(br,gt),bt}function C(Te){return tt=Te,De=setTimeout(W,N),Yt?B(Te):bt}function k(Te){var gt=Te-st,br=Te-tt,Ro=N-gt;return Mt?x(Ro,Ne-br):Ro}function D(Te){var gt=Te-st,br=Te-tt;return st===void 0||gt>=N||gt<0||Mt&&br>=Ne}function W(){var Te=w();if(D(Te))return Z(Te);De=setTimeout(W,k(Te))}function Z(Te){return De=void 0,vr&&le?B(Te):(le=ce=void 0,bt)}function We(){De!==void 0&&clearTimeout(De),tt=0,le=st=ce=De=void 0}function Gt(){return De===void 0?bt:Z(w())}function Nr(){var Te=w(),gt=D(Te);if(le=arguments,ce=this,st=Te,gt){if(De===void 0)return C(st);if(Mt)return De=setTimeout(W,N),B(st)}return De===void 0&&(De=setTimeout(W,N)),bt}return Nr.cancel=We,Nr.flush=Gt,Nr}function _(O){var N=typeof O;return!!O&&(N=="object"||N=="function")}function de(O){return!!O&&typeof O=="object"}function be(O){return typeof O=="symbol"||de(O)&&h.call(O)==o}function M(O){if(typeof O=="number")return O;if(be(O))return n;if(_(O)){var N=typeof O.valueOf=="function"?O.valueOf():O;O=_(N)?N+"":N}if(typeof O!="string")return O===0?O:+O;O=O.replace(i,"");var ee=s.test(O);return ee||c.test(O)?l(O.slice(2),ee?2:8):a.test(O)?n:+O}t.exports=E}),yn,K,ua,pa,Nt,Pi,fa,ma,da,lo,to,ro,bl,Ar={},ha=[],gl=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,Pr=Array.isArray;function pt(e,t){for(var r in t)e[r]=t[r];return e}function uo(e){e&&e.parentNode&&e.parentNode.removeChild(e)}function Wt(e,t,r){var n,o,i,a={};for(i in t)i=="key"?n=t[i]:i=="ref"?o=t[i]:a[i]=t[i];if(arguments.length>2&&(a.children=arguments.length>3?yn.call(arguments,2):r),typeof e=="function"&&e.defaultProps!=null)for(i in e.defaultProps)a[i]===void 0&&(a[i]=e.defaultProps[i]);return fn(e,a,n,o,null)}function fn(e,t,r,n,o){var i={type:e,props:t,key:r,ref:n,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:o!=null?o:++ua,__i:-1,__u:0};return o==null&&K.vnode!=null&&K.vnode(i),i}function ft(e){return e.children}function at(e,t){this.props=e,this.context=t}function cr(e,t){if(t==null)return e.__?cr(e.__,e.__i+1):null;for(var r;ts&&Nt.sort(ma),e=Nt.shift(),s=Nt.length,e.__d&&(r=void 0,n=void 0,o=(n=(t=e).__v).__e,i=[],a=[],t.__P&&((r=pt({},n)).__v=n.__v+1,K.vnode&&K.vnode(r),po(t.__P,r,n,t.__n,t.__P.namespaceURI,32&n.__u?[o]:null,i,o!=null?o:cr(n),!!(32&n.__u),a),r.__v=n.__v,r.__.__k[r.__i]=r,_a(i,r,a),n.__e=n.__=null,r.__e!=o&&va(r)));vn.__r=0}function ba(e,t,r,n,o,i,a,s,c,l,u){var p,d,m,h,v,x,w,E=n&&n.__k||ha,_=t.length;for(c=_l(r,t,E,c,_),p=0;p<_;p++)(m=r.__k[p])!=null&&(d=m.__i==-1?Ar:E[m.__i]||Ar,m.__i=p,x=po(e,m,d,o,i,a,s,c,l,u),h=m.__e,m.ref&&d.ref!=m.ref&&(d.ref&&fo(d.ref,null,m),u.push(m.ref,m.__c||h,m)),v==null&&h!=null&&(v=h),(w=!!(4&m.__u))||d.__k===m.__k?c=ga(m,c,e,w):typeof m.type=="function"&&x!==void 0?c=x:h&&(c=h.nextSibling),m.__u&=-7);return r.__e=v,c}function _l(e,t,r,n,o){var i,a,s,c,l,u=r.length,p=u,d=0;for(e.__k=new Array(o),i=0;i0?fn(a.type,a.props,a.key,a.ref?a.ref:null,a.__v):a).__=e,a.__b=e.__b+1,s=null,(l=a.__i=yl(a,r,c,p))!=-1&&(p--,(s=r[l])&&(s.__u|=2)),s==null||s.__v==null?(l==-1&&(o>u?d--:oc?d--:d++,a.__u|=4))):e.__k[i]=null;if(p)for(i=0;i(u?1:0)){for(o=r-1,i=r+1;o>=0||i=0?o--:i++])!=null&&!(2&l.__u)&&s==l.key&&c==l.type)return a}return-1}function Ri(e,t,r){t[0]=="-"?e.setProperty(t,r!=null?r:""):e[t]=r==null?"":typeof r!="number"||gl.test(t)?r:r+"px"}function un(e,t,r,n,o){var i,a;e:if(t=="style")if(typeof r=="string")e.style.cssText=r;else{if(typeof n=="string"&&(e.style.cssText=n=""),n)for(t in n)r&&t in r||Ri(e.style,t,"");if(r)for(t in r)n&&r[t]==n[t]||Ri(e.style,t,r[t])}else if(t[0]=="o"&&t[1]=="n")i=t!=(t=t.replace(da,"$1")),a=t.toLowerCase(),t=a in e||t=="onFocusOut"||t=="onFocusIn"?a.slice(2):t.slice(2),e.l||(e.l={}),e.l[t+i]=r,r?n?r.u=n.u:(r.u=lo,e.addEventListener(t,i?ro:to,i)):e.removeEventListener(t,i?ro:to,i);else{if(o=="http://www.w3.org/2000/svg")t=t.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if(t!="width"&&t!="height"&&t!="href"&&t!="list"&&t!="form"&&t!="tabIndex"&&t!="download"&&t!="rowSpan"&&t!="colSpan"&&t!="role"&&t!="popover"&&t in e)try{e[t]=r!=null?r:"";break e}catch(s){}typeof r=="function"||(r==null||r===!1&&t[4]!="-"?e.removeAttribute(t):e.setAttribute(t,t=="popover"&&r==1?"":r))}}function ji(e){return function(t){if(this.l){var r=this.l[t.type+e];if(t.t==null)t.t=lo++;else if(t.t0?e:Pr(e)?e.map(ya):pt({},e)}function xl(e,t,r,n,o,i,a,s,c){var l,u,p,d,m,h,v,x=r.props,w=t.props,E=t.type;if(E=="svg"?o="http://www.w3.org/2000/svg":E=="math"?o="http://www.w3.org/1998/Math/MathML":o||(o="http://www.w3.org/1999/xhtml"),i!=null){for(l=0;l=r.__.length&&r.__.push({}),r.__[e]}function bn(e){return $r=1,Tl(Ta,e)}function Tl(e,t,r){var n=mo(Hr++,2);if(n.t=e,!n.__c&&(n.__=[r?r(t):Ta(void 0,t),function(s){var c=n.__N?n.__N[0]:n.__[0],l=n.t(c,s);c!==l&&(n.__N=[l,n.__[1]],n.__c.setState({}))}],n.__c=ve,!ve.__f)){var o=function(s,c,l){if(!n.__c.__H)return!0;var u=n.__c.__H.__.filter(function(d){return!!d.__c});if(u.every(function(d){return!d.__N}))return!i||i.call(this,s,c,l);var p=n.__c.props!==s;return u.forEach(function(d){if(d.__N){var m=d.__[0];d.__=d.__N,d.__N=void 0,m!==d.__[0]&&(p=!0)}}),i&&i.call(this,s,c,l)||p};ve.__f=!0;var i=ve.shouldComponentUpdate,a=ve.componentWillUpdate;ve.componentWillUpdate=function(s,c,l){if(this.__e){var u=i;i=void 0,o(s,c,l),i=u}a&&a.call(this,s,c,l)},ve.shouldComponentUpdate=o}return n.__N||n.__}function mt(e,t){var r=mo(Hr++,3);!we.__s&&Ea(r.__H,t)&&(r.__=e,r.u=t,ve.__H.__h.push(r))}function Vt(e){return $r=5,ur(function(){return{current:e}},[])}function ur(e,t){var r=mo(Hr++,7);return Ea(r.__H,t)&&(r.__=e(),r.__H=t,r.__h=e),r.__}function Sl(e,t){return $r=8,ur(function(){return e},t)}function Ol(){for(var e;e=wa.shift();)if(e.__P&&e.__H)try{e.__H.__h.forEach(mn),e.__H.__h.forEach(oo),e.__H.__h=[]}catch(t){e.__H.__h=[],we.__e(t,e.__v)}}we.__b=function(e){ve=null,Ui&&Ui(e)},we.__=function(e,t){e&&t.__k&&t.__k.__m&&(e.__m=t.__k.__m),zi&&zi(e,t)},we.__r=function(e){Ni&&Ni(e),Hr=0;var t=(ve=e.__c).__H;t&&(Zn===ve?(t.__h=[],ve.__h=[],t.__.forEach(function(r){r.__N&&(r.__=r.__N),r.u=r.__N=void 0})):(t.__h.forEach(mn),t.__h.forEach(oo),t.__h=[],Hr=0)),Zn=ve},we.diffed=function(e){Di&&Di(e);var t=e.__c;t&&t.__H&&(t.__H.__h.length&&(wa.push(t)!==1&&Fi===we.requestAnimationFrame||((Fi=we.requestAnimationFrame)||Ll)(Ol)),t.__H.__.forEach(function(r){r.u&&(r.__H=r.u),r.u=void 0})),Zn=ve=null},we.__c=function(e,t){t.some(function(r){try{r.__h.forEach(mn),r.__h=r.__h.filter(function(n){return!n.__||oo(n)})}catch(n){t.some(function(o){o.__h&&(o.__h=[])}),t=[],we.__e(n,r.__v)}}),Wi&&Wi(e,t)},we.unmount=function(e){Vi&&Vi(e);var t,r=e.__c;r&&r.__H&&(r.__H.__.forEach(function(n){try{mn(n)}catch(o){t=o}}),r.__H=void 0,t&&we.__e(t,r.__v))};var qi=typeof requestAnimationFrame=="function";function Ll(e){var t,r=function(){clearTimeout(n),qi&&cancelAnimationFrame(t),setTimeout(e)},n=setTimeout(r,35);qi&&(t=requestAnimationFrame(r))}function mn(e){var t=ve,r=e.__c;typeof r=="function"&&(e.__c=void 0,r()),ve=t}function oo(e){var t=ve;e.__c=e.__(),ve=t}function Ea(e,t){return!e||e.length!==t.length||t.some(function(r,n){return r!==e[n]})}function Ta(e,t){return typeof t=="function"?t(e):t}function Ml(e,t){for(var r in t)e[r]=t[r];return e}function Ki(e,t){for(var r in e)if(r!=="__source"&&!(r in t))return!0;for(var n in t)if(n!=="__source"&&e[n]!==t[n])return!0;return!1}function Bi(e,t){this.props=e,this.context=t}(Bi.prototype=new at).isPureReactComponent=!0,Bi.prototype.shouldComponentUpdate=function(e,t){return Ki(this.props,e)||Ki(this.state,t)};var Yi=K.__b;K.__b=function(e){e.type&&e.type.__f&&e.ref&&(e.props.ref=e.ref,e.ref=null),Yi&&Yi(e)};var Yx=typeof Symbol<"u"&&Symbol.for&&Symbol.for("react.forward_ref")||3911,kl=K.__e;K.__e=function(e,t,r,n){if(e.then){for(var o,i=t;i=i.__;)if((o=i.__c)&&o.__c)return t.__e==null&&(t.__e=r.__e,t.__k=r.__k),o.__c(e,t)}kl(e,t,r,n)};var Gi=K.unmount;function Sa(e,t,r){return e&&(e.__c&&e.__c.__H&&(e.__c.__H.__.forEach(function(n){typeof n.__c=="function"&&n.__c()}),e.__c.__H=null),(e=Ml({},e)).__c!=null&&(e.__c.__P===r&&(e.__c.__P=t),e.__c.__e=!0,e.__c=null),e.__k=e.__k&&e.__k.map(function(n){return Sa(n,t,r)})),e}function Oa(e,t,r){return e&&r&&(e.__v=null,e.__k=e.__k&&e.__k.map(function(n){return Oa(n,t,r)}),e.__c&&e.__c.__P===t&&(e.__e&&r.appendChild(e.__e),e.__c.__e=!0,e.__c.__P=r)),e}function Qn(){this.__u=0,this.o=null,this.__b=null}function La(e){var t=e.__.__c;return t&&t.__a&&t.__a(e)}function pn(){this.i=null,this.l=null}K.unmount=function(e){var t=e.__c;t&&t.__R&&t.__R(),t&&32&e.__u&&(e.type=null),Gi&&Gi(e)},(Qn.prototype=new at).__c=function(e,t){var r=t.__c,n=this;n.o==null&&(n.o=[]),n.o.push(r);var o=La(n.__v),i=!1,a=function(){i||(i=!0,r.__R=null,o?o(s):s())};r.__R=a;var s=function(){if(!--n.__u){if(n.state.__a){var c=n.state.__a;n.__v.__k[0]=Oa(c,c.__c.__P,c.__c.__O)}var l;for(n.setState({__a:n.__b=null});l=n.o.pop();)l.forceUpdate()}};n.__u++||32&t.__u||n.setState({__a:n.__b=n.__v.__k[0]}),e.then(a,a)},Qn.prototype.componentWillUnmount=function(){this.o=[]},Qn.prototype.render=function(e,t){if(this.__b){if(this.__v.__k){var r=document.createElement("div"),n=this.__v.__k[0].__c;this.__v.__k[0]=Sa(this.__b,r,n.__O=n.__P)}this.__b=null}var o=t.__a&&Wt(ft,null,e.fallback);return o&&(o.__u&=-33),[Wt(ft,null,t.__a?null:e.children),o]};var Ji=function(e,t,r){if(++r[1]===r[0]&&e.l.delete(t),e.props.revealOrder&&(e.props.revealOrder[0]!=="t"||!e.l.size))for(r=e.i;r;){for(;r.length>3;)r.pop()();if(r[1]Object.freeze({get current(){return t.current}}),[])}var Nl=typeof globalThis<"u"&&typeof navigator<"u"&&typeof document<"u";function Dl(e,...t){var r;(r=e==null?void 0:e.addEventListener)==null||r.call(e,...t)}function Wl(e,...t){var r;(r=e==null?void 0:e.removeEventListener)==null||r.call(e,...t)}var Vl=(e,t)=>Object.hasOwn(e,t),zl=()=>!0,ql=()=>!1;function Kl(e=!1){let t=Vt(e),r=Sl(()=>t.current,[]);return mt(()=>(t.current=!0,()=>{t.current=!1}),[]),r}function Bl(e,...t){let r=Kl(),n=ka(t[1]),o=ur(()=>function(...i){r()&&(typeof n.current=="function"?n.current.apply(this,i):typeof n.current.handleEvent=="function"&&n.current.handleEvent.apply(this,i))},[]);mt(()=>{let i=Yl(e)?e.current:e;if(!i)return;let a=t.slice(2);return Dl(i,t[0],o,...a),()=>{Wl(i,t[0],o,...a)}},[e,t[0]])}function Yl(e){return e!==null&&typeof e=="object"&&Vl(e,"current")}var Gl=e=>typeof e=="function"?e:typeof e=="string"?t=>t.key===e:e?zl:ql,Jl=Nl?globalThis:null;function Aa(e,t,r=[],n={}){let{event:o="keydown",target:i=Jl,eventOptions:a}=n,s=ka(t),c=ur(()=>{let l=Gl(e);return function(u){l(u)&&s.current.call(this,u)}},r);Bl(i,o,c,a)}function Ca(e){var t,r,n="";if(typeof e=="string"||typeof e=="number")n+=e;else if(typeof e=="object")if(Array.isArray(e)){var o=e.length;for(t=0;t1)St--;else{for(var e,t=!1;kr!==void 0;){var r=kr;for(kr=void 0,io++;r!==void 0;){var n=r.o;if(r.o=void 0,r.f&=-3,!(8&r.f)&&Pa(r))try{r.c()}catch(o){t||(e=o,t=!0)}r=n}}if(io=0,St--,t)throw e}}function Ql(e){if(St>0)return e();St++;try{return e()}finally{xn()}}var ae=void 0;function Ha(e){var t=ae;ae=void 0;try{return e()}finally{ae=t}}var kr=void 0,St=0,io=0,gn=0;function $a(e){if(ae!==void 0){var t=e.n;if(t===void 0||t.t!==ae)return t={i:0,S:e,p:ae.s,n:void 0,t:ae,e:void 0,x:void 0,r:t},ae.s!==void 0&&(ae.s.n=t),ae.s=t,e.n=t,32&ae.f&&e.S(t),t;if(t.i===-1)return t.i=0,t.n!==void 0&&(t.n.p=t.p,t.p!==void 0&&(t.p.n=t.n),t.p=ae.s,t.n=void 0,ae.s.n=t,ae.s=t),t}}function Ce(e,t){this.v=e,this.i=0,this.n=void 0,this.t=void 0,this.W=t==null?void 0:t.watched,this.Z=t==null?void 0:t.unwatched,this.name=t==null?void 0:t.name}Ce.prototype.brand=Zl;Ce.prototype.h=function(){return!0};Ce.prototype.S=function(e){var t=this,r=this.t;r!==e&&e.e===void 0&&(e.x=r,this.t=e,r!==void 0?r.e=e:Ha(function(){var n;(n=t.W)==null||n.call(t)}))};Ce.prototype.U=function(e){var t=this;if(this.t!==void 0){var r=e.e,n=e.x;r!==void 0&&(r.x=n,e.e=void 0),n!==void 0&&(n.e=r,e.x=void 0),e===this.t&&(this.t=n,n===void 0&&Ha(function(){var o;(o=t.Z)==null||o.call(t)}))}};Ce.prototype.subscribe=function(e){var t=this;return qt(function(){var r=t.value,n=ae;ae=void 0;try{e(r)}finally{ae=n}},{name:"sub"})};Ce.prototype.valueOf=function(){return this.value};Ce.prototype.toString=function(){return this.value+""};Ce.prototype.toJSON=function(){return this.value};Ce.prototype.peek=function(){var e=ae;ae=void 0;try{return this.value}finally{ae=e}};Object.defineProperty(Ce.prototype,"value",{get:function(){var e=$a(this);return e!==void 0&&(e.i=this.i),this.v},set:function(e){if(e!==this.v){if(io>100)throw new Error("Cycle detected");this.v=e,this.i++,gn++,St++;try{for(var t=this.t;t!==void 0;t=t.x)t.t.N()}finally{xn()}}}});function Ot(e,t){return new Ce(e,t)}function Pa(e){for(var t=e.s;t!==void 0;t=t.n)if(t.S.i!==t.i||!t.S.h()||t.S.i!==t.i)return!0;return!1}function Ia(e){for(var t=e.s;t!==void 0;t=t.n){var r=t.S.n;if(r!==void 0&&(t.r=r),t.S.n=t,t.i=-1,t.n===void 0){e.s=t;break}}}function Ra(e){for(var t=e.s,r=void 0;t!==void 0;){var n=t.p;t.i===-1?(t.S.U(t),n!==void 0&&(n.n=t.n),t.n!==void 0&&(t.n.p=n)):r=t,t.S.n=t.r,t.r!==void 0&&(t.r=void 0),t=n}e.s=r}function Kt(e,t){Ce.call(this,void 0),this.x=e,this.s=void 0,this.g=gn-1,this.f=4,this.W=t==null?void 0:t.watched,this.Z=t==null?void 0:t.unwatched,this.name=t==null?void 0:t.name}Kt.prototype=new Ce;Kt.prototype.h=function(){if(this.f&=-3,1&this.f)return!1;if((36&this.f)==32||(this.f&=-5,this.g===gn))return!0;if(this.g=gn,this.f|=1,this.i>0&&!Pa(this))return this.f&=-2,!0;var e=ae;try{Ia(this),ae=this;var t=this.x();(16&this.f||this.v!==t||this.i===0)&&(this.v=t,this.f&=-17,this.i++)}catch(r){this.v=r,this.f|=16,this.i++}return ae=e,Ra(this),this.f&=-2,!0};Kt.prototype.S=function(e){if(this.t===void 0){this.f|=36;for(var t=this.s;t!==void 0;t=t.n)t.S.S(t)}Ce.prototype.S.call(this,e)};Kt.prototype.U=function(e){if(this.t!==void 0&&(Ce.prototype.U.call(this,e),this.t===void 0)){this.f&=-33;for(var t=this.s;t!==void 0;t=t.n)t.S.U(t)}};Kt.prototype.N=function(){if(!(2&this.f)){this.f|=6;for(var e=this.t;e!==void 0;e=e.x)e.t.N()}};Object.defineProperty(Kt.prototype,"value",{get:function(){if(1&this.f)throw new Error("Cycle detected");var e=$a(this);if(this.h(),e!==void 0&&(e.i=this.i),16&this.f)throw this.v;return this.v}});function ta(e,t){return new Kt(e,t)}function ja(e){var t=e.u;if(e.u=void 0,typeof t=="function"){St++;var r=ae;ae=void 0;try{t()}catch(n){throw e.f&=-2,e.f|=8,ho(e),n}finally{ae=r,xn()}}}function ho(e){for(var t=e.s;t!==void 0;t=t.n)t.S.U(t);e.x=void 0,e.s=void 0,ja(e)}function eu(e){if(ae!==this)throw new Error("Out-of-order effect");Ra(this),ae=e,this.f&=-2,8&this.f&&ho(this),xn()}function pr(e,t){this.x=e,this.u=void 0,this.s=void 0,this.o=void 0,this.f=32,this.name=t==null?void 0:t.name}pr.prototype.c=function(){var e=this.S();try{if(8&this.f||this.x===void 0)return;var t=this.x();typeof t=="function"&&(this.u=t)}finally{e()}};pr.prototype.S=function(){if(1&this.f)throw new Error("Cycle detected");this.f|=1,this.f&=-9,ja(this),Ia(this),St++;var e=ae;return ae=this,eu.bind(this,e)};pr.prototype.N=function(){2&this.f||(this.f|=2,this.o=kr,kr=this)};pr.prototype.d=function(){this.f|=8,1&this.f||ho(this)};pr.prototype.dispose=function(){this.d()};function qt(e,t){var r=new pr(e,t);try{r.c()}catch(o){throw r.d(),o}var n=r.d.bind(r);return n[Symbol.dispose]=n,n}var Fa,vo,eo,Ua=[];qt(function(){Fa=this.N})();function fr(e,t){K[e]=t.bind(null,K[e]||function(){})}function _n(e){eo&&eo(),eo=e&&e.S()}function Na(e){var t=this,r=e.data,n=ru(r);n.value=r;var o=ur(function(){for(var s=t,c=t.__v;c=c.__;)if(c.__c){c.__c.__$f|=4;break}var l=ta(function(){var m=n.value.value;return m===0?0:m===!0?"":m||""}),u=ta(function(){return!Array.isArray(l.value)&&!pa(l.value)}),p=qt(function(){if(this.N=Da,u.value){var m=l.value;s.__v&&s.__v.__e&&s.__v.__e.nodeType===3&&(s.__v.__e.data=m)}}),d=t.__$u.d;return t.__$u.d=function(){p(),d.call(this)},[u,l]},[]),i=o[0],a=o[1];return i.value?a.peek():a.value}Na.displayName="ReactiveTextNode";Object.defineProperties(Ce.prototype,{constructor:{configurable:!0,value:void 0},type:{configurable:!0,value:Na},props:{configurable:!0,get:function(){return{data:this}}},__b:{configurable:!0,value:1}});fr("__b",function(e,t){if(typeof t.type=="function"&&typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.exitComponent(),typeof t.type=="string"){var r,n=t.props;for(var o in n)if(o!=="children"){var i=n[o];i instanceof Ce&&(r||(t.__np=r={}),r[o]=i,n[o]=i.peek())}}e(t)});fr("__r",function(e,t){if(typeof t.type=="function"&&typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.enterComponent(t),t.type!==ft){_n();var r,n=t.__c;n&&(n.__$f&=-2,(r=n.__$u)===void 0&&(n.__$u=r=(function(o){var i;return qt(function(){i=this}),i.c=function(){n.__$f|=1,n.setState({})},i})())),vo=n,_n(r)}e(t)});fr("__e",function(e,t,r,n){typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.exitComponent(),_n(),vo=void 0,e(t,r,n)});fr("diffed",function(e,t){typeof t.type=="function"&&typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.exitComponent(),_n(),vo=void 0;var r;if(typeof t.type=="string"&&(r=t.__e)){var n=t.__np,o=t.props;if(n){var i=r.U;if(i)for(var a in i){var s=i[a];s!==void 0&&!(a in n)&&(s.d(),i[a]=void 0)}else i={},r.U=i;for(var c in n){var l=i[c],u=n[c];l===void 0?(l=tu(r,c,u,o),i[c]=l):l.o(u,o)}}}e(t)});function tu(e,t,r,n){var o=t in e&&e.ownerSVGElement===void 0,i=Ot(r);return{o:function(a,s){i.value=a,n=s},d:qt(function(){this.N=Da;var a=i.value.value;n[t]!==a&&(n[t]=a,o?e[t]=a:a?e.setAttribute(t,a):e.removeAttribute(t))})}}fr("unmount",function(e,t){if(typeof t.type=="string"){var r=t.__e;if(r){var n=r.U;if(n){r.U=void 0;for(var o in n){var i=n[o];i&&i.d()}}}}else{var a=t.__c;if(a){var s=a.__$u;s&&(a.__$u=void 0,s.d())}}e(t)});fr("__h",function(e,t,r,n){(n<3||n===9)&&(t.__$f|=2),e(t,r,n)});at.prototype.shouldComponentUpdate=function(e,t){var r=this.__$u,n=r&&r.s!==void 0;for(var o in t)return!0;if(this.__f||typeof this.u=="boolean"&&this.u===!0){var i=2&this.__$f;if(!(n||i||4&this.__$f)||1&this.__$f)return!0}else if(!(n||4&this.__$f)||3&this.__$f)return!0;for(var a in e)if(a!=="__source"&&e[a]!==this.props[a])return!0;for(var s in this.props)if(!(s in e))return!0;return!1};function ru(e,t){return bn(function(){return Ot(e,t)})[0]}var nu=function(e){queueMicrotask(function(){queueMicrotask(e)})};function ou(){Ql(function(){for(var e;e=Ua.shift();)Fa.call(e)})}function Da(){Ua.push(this)===1&&(K.requestAnimationFrame||nu)(ou)}var ao=[0];for(let e=0;e<32;e++)ao.push(ao[e]|1<>>5]>>>e&1}set(e){this.data[e>>>5]|=1<<(e&31)}forEach(e){let t=this.size&31;for(let r=0;r{var r;return(r=t.tags)==null?void 0:r.length})&&(matchMedia("(max-width: 768px)").matches||Wa())}function Dt(){Qe.value=He(H({},Qe.value),{hideSearch:!Qe.value.hideSearch})}function Wa(){Qe.value=He(H({},Qe.value),{hideFilters:!Qe.value.hideFilters})}function dn(){return Qe.value.selectedItem}function so(e){Qe.value=He(H({},Qe.value),{selectedItem:e})}function su(){var e,t;return(t=(e=lr.value)==null?void 0:e.items)!=null?t:[]}function wn(){return typeof Se.value.input=="string"?Se.value.input:""}function Va(e){let t=za();e.length&&!t.length?Se.value=He(H({},Se.value),{page:void 0,input:e}):!e.length&&t.length?Se.value=He(H({},Se.value),{page:void 0,input:{type:"operator",data:{operator:"not",operands:[]}}}):Se.value=He(H({},Se.value),{page:void 0,input:e})}function cu(){typeof it.value.pagination.next<"u"&&(Se.value=He(H({},Se.value),{page:it.value.pagination.next}))}function lu(e){let t=Se.value.filter.input;if("type"in t&&t.type==="operator"){for(let r of t.data.operands)if("type"in r&&r.type==="value"&&typeof r.data.value=="string"&&r.data.value===e)return!0}return!1}function za(){let e=Se.value.filter.input,t=[];if("type"in e&&e.type==="operator")for(let r of e.data.operands)"type"in r&&r.type==="value"&&typeof r.data.value=="string"&&t.push(r.data.value);return t}function uu(e){let t=Se.value.filter.input,r=[];if("type"in t&&t.type==="operator")for(let n of t.data.operands)"type"in n&&n.type==="value"&&typeof n.data.value=="string"&&r.push(n.data.value);if(r.includes(e)){let n=r.indexOf(e);n>-1&&r.splice(n,1)}else r.push(e);Se.value=He(H({},Se.value),{page:void 0,filter:He(H({},Se.value.filter),{input:{type:"operator",data:{operator:"and",operands:r.map(n=>({type:"value",data:{field:"tags",value:n}}))}}})}),Va(wn())}function pu(){return it.value.items}function fu(){return it.value.total}function mu(){var e;for(let t of(e=it.value.aggregations)!=null?e:[])if(t.type==="term")return t.data.value;return[]}function sr(){return Qe.value.hideSearch}function du(){return Qe.value.hideFilters}function qa(){var e;return(e=Ka.value.highlight)!=null?e:!1}var Qe=Ot({hideSearch:!0,hideFilters:!0,selectedItem:0}),Ka=Ot({}),lr=Ot(),na=Ot(),Se=Ot({input:"",filter:{input:{type:"operator",data:{operator:"and",operands:[]}},aggregation:{input:[{type:"term",data:{field:"tags"}}]}}}),it=Ot({items:[],query:{select:{documents:new ra(0),terms:new ra(0)},values:[]},pagination:{total:0}});function hu(e,t,r){for(let n=0;tr&&t(0,o,r,r=i);continue;case 62:e.charCodeAt(r+1)===47?t(2,--o,r,r=i+1):hu(e,r,n)?t(3,o,r,r=i+1):t(1,o++,r,r=i+1)}i>r&&t(0,o,r,i)}function bu(e,t=0,r=e.length){let n=++t;e:for(let l=0;n{let i=[],a=[],{onElement:s,onText:c=gu}=typeof r=="function"?{onElement:r}:r,l=0,u=0;return e(t,(p,d,m,h)=>{if(p===0)i[l++]=c(t,m,h),a[u++]={value:null,depth:d};else if(p&1&&(a[u++]={value:bu(t,m,h),depth:d}),p&2)for(let v=0;u>=0;v++){let{value:x,depth:w}=a[--u];if(w>d)continue;let E=i.slice(l-=v,l+v);i[l++]=s(x,E),u++;break}},n,o),i.slice(0,l)}}function yu(e){return e.replace(/[&<>]/g,t=>{switch(t.charCodeAt(0)){case 38:return"&";case 60:return"<";case 62:return">"}})}function hn(e){return e.replace(/&(amp|[lg]t);/g,t=>{switch(t.charCodeAt(1)){case 97:return"&";case 108:return"<";case 103:return">"}})}function xu(e,t){return{start:e.start+t,end:e.end+t,value:e.value}}function wu(e,t,r){return e.slice(t,r)}function Eu(e){let{onHighlight:t,onText:r=wu}=typeof e=="function"?{onHighlight:e}:e;return(n,o,i=0,a=n.length)=>{var l;let s=[],c=(l=o==null?void 0:o.ranges)!=null?l:[];for(let u=0,p=i;ua)break;let m=c[u].end;if(mi&&s.push(r(n,i,d));let{value:h}=c[u];s.push(t(n,{start:d,end:i=m,value:h}))}return i{let o=n.data;switch(o.type){case 1:na.value=!0;break;case 3:typeof o.data.pagination.prev<"u"?it.value=He(H({},it.value),{pagination:o.data.pagination,items:[...it.value.items,...o.data.items]}):(it.value=o.data,so(0));break}},qt(()=>{lr.value&&r.postMessage({type:0,data:lr.value})}),qt(()=>{na.value&&r.postMessage({type:2,data:Se.value})})}var oa={container:"p",hidden:"m"};function ku(e){return z("div",{class:zt(oa.container,{[oa.hidden]:e.hidden}),onClick:()=>Dt()})}var ia={container:"r",disabled:"c"};function co(e){return z("button",{class:zt(ia.container,{[ia.disabled]:!e.onClick}),onClick:e.onClick,children:e.children})}var aa=e=>e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase(),Au=e=>e.replace(/^([A-Z])|[\s-_]+(\w)/g,(t,r,n)=>n?n.toUpperCase():r.toLowerCase()),sa=e=>{let t=Au(e);return t.charAt(0).toUpperCase()+t.slice(1)},Cu=(...e)=>e.filter((t,r,n)=>!!t&&t.trim()!==""&&n.indexOf(t)===r).join(" ").trim(),Hu={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"},$u=c=>{var l=c,{color:e="currentColor",size:t=24,strokeWidth:r=2,absoluteStrokeWidth:n,children:o,iconNode:i,class:a=""}=l,s=gr(l,["color","size","strokeWidth","absoluteStrokeWidth","children","iconNode","class"]);return Wt("svg",H(He(H({},Hu),{width:String(t),height:t,stroke:e,"stroke-width":n?Number(r)*24/Number(t):r,class:["lucide",a].join(" ")}),s),[...i.map(([u,p])=>Wt(u,p)),...Cr(o)])},bo=(e,t)=>{let r=a=>{var s=a,{class:n="",children:o}=s,i=gr(s,["class","children"]);return Wt($u,He(H({},i),{iconNode:t,class:Cu(`lucide-${aa(sa(e))}`,`lucide-${aa(e)}`,n)}),o)};return r.displayName=sa(e),r},Pu=bo("corner-down-left",[["path",{d:"M20 4v7a4 4 0 0 1-4 4H4",key:"6o5b7l"}],["path",{d:"m9 10-5 5 5 5",key:"1kshq7"}]]),Iu=bo("list-filter",[["path",{d:"M2 5h20",key:"1fs1ex"}],["path",{d:"M6 12h12",key:"8npq4p"}],["path",{d:"M9 19h6",key:"456am0"}]]),Ru=bo("search",[["path",{d:"m21 21-4.34-4.34",key:"14j7rj"}],["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}]]),Gx=hl(vl(),1);function ju({threshold:e=0,root:t=null,rootMargin:r="0%",freezeOnceVisible:n=!1,initialIsIntersecting:o=!1,onChange:i}={}){var a;let[s,c]=bn(null),[l,u]=bn(()=>({isIntersecting:o,entry:void 0})),p=Vt();p.current=i;let d=((a=l.entry)==null?void 0:a.isIntersecting)&&n;mt(()=>{if(!s||!("IntersectionObserver"in window)||d)return;let v,x=new IntersectionObserver(w=>{let E=Array.isArray(x.thresholds)?x.thresholds:[x.thresholds];w.forEach(_=>{let de=_.isIntersecting&&E.some(be=>_.intersectionRatio>=be);u({isIntersecting:de,entry:_}),p.current&&p.current(de,_),de&&n&&v&&(v(),v=void 0)})},{threshold:e,root:t,rootMargin:r});return x.observe(s),()=>{x.disconnect()}},[s,JSON.stringify(e),t,r,d,n]);let m=Vt(null);mt(()=>{var v;!s&&(v=l.entry)!=null&&v.target&&!n&&!d&&m.current!==l.entry.target&&(m.current=l.entry.target,u({isIntersecting:o,entry:void 0}))},[s,l.entry,n,d,o]);let h=[c,!!l.isIntersecting,l.entry];return h.ref=h[0],h.isIntersecting=h[1],h.entry=h[2],h}var lt={container:"n",hidden:"l",content:"u",pop:"d",badge:"y",sidebar:"i",controls:"w",results:"k",loadmore:"z"};function Fu(e){let{isIntersecting:t,ref:r}=ju({threshold:0});mt(()=>{t&&cu()},[t]);let n=Vt(null);mt(()=>{n.current&&typeof Se.value.page>"u"&&n.current.scrollTo({top:0,behavior:"smooth"})},[Se.value]);let o=za();return z("div",{class:zt(lt.container,{[lt.hidden]:e.hidden}),children:[z("div",{class:lt.content,children:[z("div",{class:lt.controls,children:[z(co,{onClick:Dt,children:z(Ru,{})}),z(Nu,{focus:!e.hidden}),z(co,{onClick:Wa,children:[z(Iu,{}),o.length>0&&z("span",{class:lt.badge,children:o.length})]})]}),z("div",{class:lt.results,ref:n,children:[z(Du,{keyboard:!e.hidden}),z("div",{class:lt.loadmore,ref:r})]})]}),z("div",{class:zt(lt.sidebar,{[lt.hidden]:du()}),children:z(Uu,{})})]})}var Tt={container:"X",list:"j",heading:"F",title:"I",item:"o",active:"g",value:"R",count:"q"};function Uu(e){let t=mu();return t.sort((r,n)=>n.node.count-r.node.count),z("div",{class:Tt.container,children:[z("h3",{class:Tt.heading,children:"Filters"}),z("h4",{class:Tt.title,children:"Tags"}),z("ol",{class:Tt.list,children:t.map(r=>z("li",{class:zt(Tt.item,{[Tt.active]:lu(r.node.value)}),onClick:()=>uu(r.node.value),children:[z("span",{class:Tt.value,children:r.node.value}),z("span",{class:Tt.count,children:r.node.count})]}))})]})}var ca={container:"f"};function Nu(e){let t=Vt(null);return mt(()=>{var r,n;e.focus?(r=t.current)==null||r.focus():(n=t.current)==null||n.blur()},[e.focus]),z("div",{class:ca.container,children:z("input",{ref:t,type:"text",class:ca.content,value:hn(wn()),onInput:r=>Va(yu(r.currentTarget.value)),autocapitalize:"off",autocomplete:"off",autocorrect:"off",placeholder:"Search",spellcheck:!1,role:"combobox"})})}var ut={container:"b",heading:"A",item:"a",active:"h",wrapper:"B",actions:"s",title:"x",path:"t"};function Ga(){let[e,t]=bn(!1);return mt(()=>{let r=()=>t(!0),n=()=>t(!1);return document.addEventListener("compositionstart",r),document.addEventListener("compositionend",n),()=>{document.removeEventListener("compositionstart",r),document.removeEventListener("compositionend",n)}},[]),e}function Du(e){var s;let t=su(),r=pu(),n=dn(),o=Vt([]),i=Ga();mt(()=>{let c=o.current[n];c&&c.scrollIntoView({block:"center",behavior:"smooth"})},[n]),Aa(e.keyboard,c=>{if(i)return;let l=dn();c.key==="ArrowDown"?(c.preventDefault(),so(Math.min(l+1,r.length-1))):c.key==="ArrowUp"&&(c.preventDefault(),so(Math.max(l-1,0)))},[e.keyboard,i]);let a=(s=fu())!=null?s:0;return z(ft,{children:[r.length>0&&z("h3",{class:ut.heading,children:[z("span",{class:ut.bubble,children:new Intl.NumberFormat("en-US").format(a)})," ","results"]}),z("ol",{class:ut.container,children:r.map((c,l)=>{var m;let u=Ba(t[c.id].title,c.matches.find(({field:h})=>h==="title")),p=Mu((m=t[c.id].path)!=null?m:[],c.matches.find(({field:h})=>h==="path")),d=t[c.id].location;if(qa()){let h=encodeURIComponent(wn()),[v,x]=d.split("#",2);d=`${v}?h=${h.replace(/%20/g,"+")}`,typeof x<"u"&&(d+=`#${x}`)}return z("li",{children:z("a",{ref:h=>{o.current[l]=h},href:d,onClick:()=>Dt(),class:zt(ut.item,{[ut.active]:l===dn()}),children:[z("div",{class:ut.wrapper,children:[z("h2",{class:ut.title,children:u}),z("menu",{class:ut.path,children:p.map(h=>z("li",{children:h}))})]}),z("nav",{class:ut.actions,children:z(co,{children:z(Pu,{})})})]})})})})]})}var Wu={container:"e"};function Vu(e){let t=Ga();return Aa(!0,r=>{var n,o,i,a,s;if(!t)if((r.metaKey||r.ctrlKey)&&r.key==="k")r.preventDefault(),Dt();else if((r.metaKey||r.ctrlKey)&&r.key==="j")document.body.classList.toggle("dark");else if(r.key==="Enter"&&!sr()){r.preventDefault();let c=dn(),l=(o=(n=it.value)==null?void 0:n.items[c])==null?void 0:o.id;if((a=(i=lr.value)==null?void 0:i.items[l])!=null&&a.location){Dt();let u=(s=lr.value)==null?void 0:s.items[l].location;if(qa()){let p=encodeURIComponent(wn()),[d,m]=u.split("#",2);u=`${d}?h=${p.replace(/%20/g,"+")}`,typeof m<"u"&&(u+=`#${m}`)}window.location.href=u}}else r.key==="Escape"&&!sr()&&(r.preventDefault(),Dt())},[t]),z("div",{class:Wu.container,children:[z(ku,{hidden:sr()}),z(Fu,{hidden:sr()})]})}function Ja(e,t){au(e),El(z(Vu,{}),t)}function go(){Dt()}function zu(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function qu(){return R(b(window,"compositionstart").pipe(f(()=>!0)),b(window,"compositionend").pipe(f(()=>!1))).pipe(J(!1))}function Xa(){let e=b(window,"keydown").pipe(f(t=>({mode:sr()?"global":"search",type:t.key,meta:t.ctrlKey||t.metaKey,claim(){t.preventDefault(),t.stopPropagation()}})),L(({mode:t,type:r})=>{if(t==="global"){let n=xt();if(typeof n!="undefined")return!zu(n,r)}return!0}),xe());return qu().pipe(g(t=>t?y:e))}function Ye(){return new URL(location.href)}function dt(e,t=!1){if(X("navigation.instant")&&!t){let r=A("a",{href:e.href});document.body.appendChild(r),r.click(),r.remove()}else location.href=e.href}function Za(){return new I}function Qa(){return location.hash.slice(1)}function es(e){let t=A("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function _o(e){return R(b(window,"hashchange"),e).pipe(f(Qa),J(Qa()),L(t=>t.length>0),se(1))}function ts(e){return _o(e).pipe(f(t=>Le(`[id="${t}"]`)),L(t=>typeof t!="undefined"))}function Ir(e){let t=matchMedia(e);return an(r=>t.addListener(()=>r(t.matches))).pipe(J(t.matches))}function rs(){let e=matchMedia("print");return R(b(window,"beforeprint").pipe(f(()=>!0)),b(window,"afterprint").pipe(f(()=>!1))).pipe(J(e.matches))}function yo(e,t){return e.pipe(g(r=>r?t():y))}function xo(e,t){return new U(r=>{let n=new XMLHttpRequest;return n.open("GET",`${e}`),n.responseType="blob",n.addEventListener("load",()=>{n.status>=200&&n.status<300?(r.next(n.response),r.complete()):r.error(new Error(n.statusText))}),n.addEventListener("error",()=>{r.error(new Error("Network error"))}),n.addEventListener("abort",()=>{r.complete()}),typeof(t==null?void 0:t.progress$)!="undefined"&&(n.addEventListener("progress",o=>{var i;if(o.lengthComputable)t.progress$.next(o.loaded/o.total*100);else{let a=(i=n.getResponseHeader("Content-Length"))!=null?i:0;t.progress$.next(o.loaded/+a*100)}}),t.progress$.next(5)),n.send(),()=>n.abort()})}function et(e,t){return xo(e,t).pipe(g(r=>r.text()),f(r=>JSON.parse(r)),se(1))}function En(e,t){let r=new DOMParser;return xo(e,t).pipe(g(n=>n.text()),f(n=>r.parseFromString(n,"text/html")),se(1))}function ns(e,t){let r=new DOMParser;return xo(e,t).pipe(g(n=>n.text()),f(n=>r.parseFromString(n,"text/xml")),se(1))}var wo={drawer:G("[data-md-toggle=drawer]"),search:G("[data-md-toggle=search]")};function Eo(e,t){wo[e].checked!==t&&wo[e].click()}function Tn(e){let t=wo[e];return b(t,"change").pipe(f(()=>t.checked),J(t.checked))}function os(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function is(){return R(b(window,"scroll",{passive:!0}),b(window,"resize",{passive:!0})).pipe(f(os),J(os()))}function as(){return{width:innerWidth,height:innerHeight}}function ss(){return b(window,"resize",{passive:!0}).pipe(f(as),J(as()))}function cs(){return re([is(),ss()]).pipe(f(([e,t])=>({offset:e,size:t})),se(1))}function Sn(e,{viewport$:t,header$:r}){let n=t.pipe(fe("size")),o=re([n,r]).pipe(f(()=>wt(e)));return re([r,t,o]).pipe(f(([{height:i},{offset:a,size:s},{x:c,y:l}])=>({offset:{x:a.x-c,y:a.y-l+i},size:s})))}var Ku=G("#__config"),mr=JSON.parse(Ku.textContent);mr.base=`${new URL(mr.base,Ye())}`;function Ue(){return mr}function X(e){return mr.features.includes(e)}function Bt(e,t){return typeof t!="undefined"?mr.translations[e].replace("#",t.toString()):mr.translations[e]}function ht(e,t=document){return G(`[data-md-component=${e}]`,t)}function Ee(e,t=document){return P(`[data-md-component=${e}]`,t)}function Bu(e){let t=G(".md-typeset > :first-child",e);return b(t,"click",{once:!0}).pipe(f(()=>G(".md-typeset",e)),f(r=>({hash:__md_hash(r.innerHTML)})))}function ls(e){if(!X("announce.dismiss")||!e.childElementCount)return y;if(!e.hidden){let t=G(".md-typeset",e);__md_hash(t.innerHTML)===__md_get("__announce")&&(e.hidden=!0)}return j(()=>{let t=new I;return t.subscribe(({hash:r})=>{e.hidden=!0,__md_set("__announce",r)}),Bu(e).pipe($(r=>t.next(r)),V(()=>t.complete()),f(r=>H({ref:e},r)))})}function Yu(e,{target$:t}){return t.pipe(f(r=>({hidden:r!==e})))}function us(e,t){let r=new I;return r.subscribe(({hidden:n})=>{e.hidden=n}),Yu(e,t).pipe($(n=>r.next(n)),V(()=>r.complete()),f(n=>H({ref:e},n)))}function To(e,t){return t==="inline"?A("div",{class:"md-tooltip md-tooltip--inline",id:e,role:"tooltip"},A("div",{class:"md-tooltip__inner md-typeset"})):A("div",{class:"md-tooltip",id:e,role:"tooltip"},A("div",{class:"md-tooltip__inner md-typeset"}))}function On(...e){return A("div",{class:"md-tooltip2",role:"dialog"},A("div",{class:"md-tooltip2__inner md-typeset"},e))}function ps(...e){return A("div",{class:"md-tooltip2",role:"tooltip"},A("div",{class:"md-tooltip2__inner md-typeset"},e))}function fs(e,t){if(t=t?`${t}_annotation_${e}`:void 0,t){let r=t?`#${t}`:void 0;return A("aside",{class:"md-annotation",tabIndex:0},To(t),A("a",{href:r,class:"md-annotation__index",tabIndex:-1},A("span",{"data-md-annotation-id":e})))}else return A("aside",{class:"md-annotation",tabIndex:0},To(t),A("span",{class:"md-annotation__index",tabIndex:-1},A("span",{"data-md-annotation-id":e})))}function ms(e){return A("button",{class:"md-code__button",title:Bt("clipboard.copy"),"data-clipboard-target":`#${e} > code`,"data-md-type":"copy"})}function ds(){return A("button",{class:"md-code__button",title:"Toggle line selection","data-md-type":"select"})}function hs(){return A("nav",{class:"md-code__nav"})}var Xu=_r(So());function bs(e){return A("ul",{class:"md-source__facts"},Object.entries(e).map(([t,r])=>A("li",{class:`md-source__fact md-source__fact--${t}`},typeof r=="number"?Li(r):r)))}function Oo(e){let t=`tabbed-control tabbed-control--${e}`;return A("div",{class:t,hidden:!0},A("button",{class:"tabbed-button",tabIndex:-1,"aria-hidden":"true"}))}function gs(e){return A("div",{class:"md-typeset__scrollwrap"},A("div",{class:"md-typeset__table"},e))}function Zu(e){var n;let t=Ue(),r=new URL(`../${e.version}/`,t.base);return A("li",{class:"md-version__item"},A("a",{href:`${r}`,class:"md-version__link"},e.title,((n=t.version)==null?void 0:n.alias)&&e.aliases.length>0&&A("span",{class:"md-version__alias"},e.aliases[0])))}function _s(e,t){var n;let r=Ue();return e=e.filter(o=>{var i;return!((i=o.properties)!=null&&i.hidden)}),A("div",{class:"md-version"},A("button",{class:"md-version__current","aria-label":Bt("select.version")},t.title,((n=r.version)==null?void 0:n.alias)&&t.aliases.length>0&&A("span",{class:"md-version__alias"},t.aliases[0])),A("ul",{class:"md-version__list"},e.map(Zu)))}var Qu=0;function ep(e,t=250){let r=re([ir(e),Ft(e,t)]).pipe(f(([o,i])=>o||i),ie()),n=j(()=>Ai(e)).pipe(oe(Ut),Lr(1),Ze(r),f(()=>Ci(e)));return r.pipe(Sr(o=>o),g(()=>re([r,n])),f(([o,i])=>({active:o,offset:i})),xe())}function Rr(e,t,r=250){let{content$:n,viewport$:o}=t,i=`__tooltip2_${Qu++}`;return j(()=>{let a=new I,s=new Un(!1);a.pipe(he(),ye(!1)).subscribe(s);let c=s.pipe(Tr(u=>Ve(+!u*250,Wn)),ie(),g(u=>u?n:y),$(u=>u.id=i),xe());re([a.pipe(f(({active:u})=>u)),c.pipe(g(u=>Ft(u,250)),J(!1))]).pipe(f(u=>u.some(p=>p))).subscribe(s);let l=s.pipe(L(u=>u),pe(c,o),f(([u,p,{size:d}])=>{let m=e.getBoundingClientRect(),h=m.width/2;if(p.role==="tooltip")return{x:h,y:8+m.height};if(m.y>=d.height/2){let{height:v}=Ae(p);return{x:h,y:-16-v}}else return{x:h,y:16+m.height}}));return re([c,a,l]).subscribe(([u,{offset:p},d])=>{u.style.setProperty("--md-tooltip-host-x",`${p.x}px`),u.style.setProperty("--md-tooltip-host-y",`${p.y}px`),u.style.setProperty("--md-tooltip-x",`${d.x}px`),u.style.setProperty("--md-tooltip-y",`${d.y}px`),u.classList.toggle("md-tooltip2--top",d.y<0),u.classList.toggle("md-tooltip2--bottom",d.y>=0)}),s.pipe(L(u=>u),pe(c,(u,p)=>p),L(u=>u.role==="tooltip")).subscribe(u=>{let p=Ae(G(":scope > *",u));u.style.setProperty("--md-tooltip-width",`${p.width}px`),u.style.setProperty("--md-tooltip-tail","0px")}),s.pipe(ie(),Ie(je),pe(c)).subscribe(([u,p])=>{p.classList.toggle("md-tooltip2--active",u)}),re([s.pipe(L(u=>u)),c]).subscribe(([u,p])=>{p.role==="dialog"?(e.setAttribute("aria-controls",i),e.setAttribute("aria-haspopup","dialog")):e.setAttribute("aria-describedby",i)}),s.pipe(L(u=>!u)).subscribe(()=>{e.removeAttribute("aria-controls"),e.removeAttribute("aria-describedby"),e.removeAttribute("aria-haspopup")}),ep(e,r).pipe($(u=>a.next(u)),V(()=>a.complete()),f(u=>H({ref:e},u)))})}function Ge(e,{viewport$:t},r=document.body){return Rr(e,{content$:new U(n=>{let o=e.title,i=ps(o);return n.next(i),e.removeAttribute("title"),r.append(i),()=>{i.remove(),e.setAttribute("title",o)}}),viewport$:t},0)}function tp(e,t){let r=j(()=>re([Hi(e),Ut(t)])).pipe(f(([{x:n,y:o},i])=>{let{width:a,height:s}=Ae(e);return{x:n-i.x+a/2,y:o-i.y+s/2}}));return ir(e).pipe(g(n=>r.pipe(f(o=>({active:n,offset:o})),Me(+!n||1/0))))}function ys(e,t,{target$:r}){let[n,o]=Array.from(e.children);return j(()=>{let i=new I,a=i.pipe(he(),ye(!0));return i.subscribe({next({offset:s}){e.style.setProperty("--md-tooltip-x",`${s.x}px`),e.style.setProperty("--md-tooltip-y",`${s.y}px`)},complete(){e.style.removeProperty("--md-tooltip-x"),e.style.removeProperty("--md-tooltip-y")}}),Et(e).pipe(Q(a)).subscribe(s=>{e.toggleAttribute("data-md-visible",s)}),R(i.pipe(L(({active:s})=>s)),i.pipe(Be(250),L(({active:s})=>!s))).subscribe({next({active:s}){s?e.prepend(n):n.remove()},complete(){e.prepend(n)}}),i.pipe(Xe(16,je)).subscribe(({active:s})=>{n.classList.toggle("md-tooltip--active",s)}),i.pipe(Lr(125,je),L(()=>!!e.offsetParent),f(()=>e.offsetParent.getBoundingClientRect()),f(({x:s})=>s)).subscribe({next(s){s?e.style.setProperty("--md-tooltip-0",`${-s}px`):e.style.removeProperty("--md-tooltip-0")},complete(){e.style.removeProperty("--md-tooltip-0")}}),b(o,"click").pipe(Q(a),L(s=>!(s.metaKey||s.ctrlKey))).subscribe(s=>{s.stopPropagation(),s.preventDefault()}),b(o,"mousedown").pipe(Q(a),pe(i)).subscribe(([s,{active:c}])=>{var l;if(s.button!==0||s.metaKey||s.ctrlKey)s.preventDefault();else if(c){s.preventDefault();let u=e.parentElement.closest(".md-annotation");u instanceof HTMLElement?u.focus():(l=xt())==null||l.blur()}}),r.pipe(Q(a),L(s=>s===n),It(125)).subscribe(()=>e.focus()),tp(e,t).pipe($(s=>i.next(s)),V(()=>i.complete()),f(s=>H({ref:e},s)))})}function rp(e){let t=Ue();if(e.tagName!=="CODE")return[e];let r=[".c",".c1",".cm"];if(t.annotate){let n=e.closest("[class|=language]");if(n)for(let o of Array.from(n.classList)){if(!o.startsWith("language-"))continue;let[,i]=o.split("-");i in t.annotate&&r.push(...t.annotate[i])}}return P(r.join(", "),e)}function np(e){let t=[];for(let r of rp(e)){let n=[],o=document.createNodeIterator(r,NodeFilter.SHOW_TEXT);for(let i=o.nextNode();i;i=o.nextNode())n.push(i);for(let i of n){let a;for(;a=/(\(\d+\))(!)?/.exec(i.textContent);){let[,s,c]=a;if(typeof c=="undefined"){let l=i.splitText(a.index);i=l.splitText(s.length),t.push(l)}else{i.textContent=s,t.push(i);break}}}}return t}function xs(e,t){t.append(...Array.from(e.childNodes))}function Ln(e,t,{target$:r,print$:n}){let o=t.closest("[id]"),i=o==null?void 0:o.id,a=new Map;for(let s of np(t)){let[,c]=s.textContent.match(/\((\d+)\)/);Le(`:scope > li:nth-child(${c})`,e)&&(a.set(c,fs(c,i)),s.replaceWith(a.get(c)))}return a.size===0?y:j(()=>{let s=new I,c=s.pipe(he(),ye(!0)),l=[];for(let[u,p]of a)l.push([G(".md-typeset",p),G(`:scope > li:nth-child(${u})`,e)]);return n.pipe(Q(c)).subscribe(u=>{e.hidden=!u,e.classList.toggle("md-annotation-list",u);for(let[p,d]of l)u?xs(p,d):xs(d,p)}),R(...[...a].map(([,u])=>ys(u,t,{target$:r}))).pipe(V(()=>s.complete()),xe())})}function ws(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return ws(t)}}function Es(e,t){return j(()=>{let r=ws(e);return typeof r!="undefined"?Ln(r,e,t):y})}var Ss=_r(Mo());var op=0,Ts=R(b(window,"keydown").pipe(f(()=>!0)),R(b(window,"keyup"),b(window,"contextmenu")).pipe(f(()=>!1))).pipe(J(!1),se(1));function Os(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return Os(t)}}function ip(e){return Re(e).pipe(f(({width:t})=>({scrollable:Mr(e).width>t})),fe("scrollable"))}function Ls(e,t){let{matches:r}=matchMedia("(hover)"),n=j(()=>{let o=new I,i=o.pipe(Gn(1));o.subscribe(({scrollable:m})=>{m&&r?e.setAttribute("tabindex","0"):e.removeAttribute("tabindex")});let a=[],s=e.closest("pre"),c=s.closest("[id]"),l=c?c.id:op++;s.id=`__code_${l}`;let u=[],p=e.closest(".highlight");if(p instanceof HTMLElement){let m=Os(p);if(typeof m!="undefined"&&(p.classList.contains("annotate")||X("content.code.annotate"))){let h=Ln(m,e,t);u.push(Re(p).pipe(Q(i),f(({width:v,height:x})=>v&&x),ie(),g(v=>v?h:y)))}}let d=P(":scope > span[id]",e);if(d.length&&(e.classList.add("md-code__content"),e.closest(".select")||X("content.code.select")&&!e.closest(".no-select"))){let m=+d[0].id.split("-").pop(),h=ds();a.push(h),X("content.tooltips")&&u.push(Ge(h,{viewport$}));let v=b(h,"click").pipe(Or(M=>!M,!1),$(()=>h.blur()),xe());v.subscribe(M=>{h.classList.toggle("md-code__button--active",M)});let x=me(d).pipe(oe(M=>Ft(M).pipe(f(O=>[M,O]))));v.pipe(g(M=>M?x:y)).subscribe(([M,O])=>{let N=Le(".hll.select",M);if(N&&!O)N.replaceWith(...Array.from(N.childNodes));else if(!N&&O){let ee=document.createElement("span");ee.className="hll select",ee.append(...Array.from(M.childNodes).slice(1)),M.append(ee)}});let w=me(d).pipe(oe(M=>b(M,"mousedown").pipe($(O=>O.preventDefault()),f(()=>M)))),E=v.pipe(g(M=>M?w:y),pe(Ts),f(([M,O])=>{var ee;let N=d.indexOf(M)+m;if(O===!1)return[N,N];{let le=P(".hll",e).map(ce=>d.indexOf(ce.parentElement)+m);return(ee=window.getSelection())==null||ee.removeAllRanges(),[Math.min(N,...le),Math.max(N,...le)]}})),_=_o(y).pipe(L(M=>M.startsWith(`__codelineno-${l}-`)));_.subscribe(M=>{let[,,O]=M.split("-"),N=O.split(":").map(le=>+le-m+1);N.length===1&&N.push(N[0]);for(let le of P(".hll:not(.select)",e))le.replaceWith(...Array.from(le.childNodes));let ee=d.slice(N[0]-1,N[1]);for(let le of ee){let ce=document.createElement("span");ce.className="hll",ce.append(...Array.from(le.childNodes).slice(1)),le.append(ce)}}),_.pipe(Me(1),Ie(ge)).subscribe(M=>{if(M.includes(":")){let O=document.getElementById(M.split(":")[0]);O&&setTimeout(()=>{let N=O,ee=-64;for(;N!==document.body;)ee+=N.offsetTop,N=N.offsetParent;window.scrollTo({top:ee})},1)}});let be=me(P('a[href^="#__codelineno"]',p)).pipe(oe(M=>b(M,"click").pipe($(O=>O.preventDefault()),f(()=>M)))).pipe(Q(i),pe(Ts),f(([M,O])=>{let ee=+G(`[id="${M.hash.slice(1)}"]`).parentElement.id.split("-").pop();if(O===!1)return[ee,ee];{let le=P(".hll",e).map(ce=>+ce.parentElement.id.split("-").pop());return[Math.min(ee,...le),Math.max(ee,...le)]}}));R(E,be).subscribe(M=>{let O=`#__codelineno-${l}-`;M[0]===M[1]?O+=M[0]:O+=`${M[0]}:${M[1]}`,history.replaceState({},"",O),window.dispatchEvent(new HashChangeEvent("hashchange",{newURL:window.location.origin+window.location.pathname+O,oldURL:window.location.href}))})}if(Ss.default.isSupported()&&(e.closest(".copy")||X("content.code.copy")&&!e.closest(".no-copy"))){let m=ms(s.id);a.push(m),X("content.tooltips")&&u.push(Ge(m,{viewport$}))}if(a.length){let m=hs();m.append(...a),s.insertBefore(m,e)}return ip(e).pipe($(m=>o.next(m)),V(()=>o.complete()),f(m=>H({ref:e},m)),Rt(R(...u).pipe(Q(i))))});return X("content.lazy")?Et(e).pipe(L(o=>o),Me(1),g(()=>n)):n}function ap(e,{target$:t,print$:r}){let n=!0;return R(t.pipe(f(o=>o.closest("details:not([open])")),L(o=>e===o),f(()=>({action:"open",reveal:!0}))),r.pipe(L(o=>o||!n),$(()=>n=e.open),f(o=>({action:o?"open":"close"}))))}function Ms(e,t){return j(()=>{let r=new I;return r.subscribe(({action:n,reveal:o})=>{e.toggleAttribute("open",n==="open"),o&&e.scrollIntoView()}),ap(e,t).pipe($(n=>r.next(n)),V(()=>r.complete()),f(n=>H({ref:e},n)))})}var ks=0,As=new Map;function sp(e){let t=document.createElement("h3");t.innerHTML=e.innerHTML;let r=[t],n=e.nextElementSibling;for(;n&&!(n instanceof HTMLHeadingElement);)r.push(n.cloneNode(!0)),n=n.nextElementSibling;return r}function cp(e,t){for(let r of P("[href], [src]",e))for(let n of["href","src"]){let o=r.getAttribute(n);if(o&&!/^(?:[a-z]+:)?\/\//i.test(o)){r[n]=new URL(r.getAttribute(n),t).toString();break}}for(let r of P("[name^=__], [for]",e))for(let n of["id","for","name"]){let o=r.getAttribute(n);o&&r.setAttribute(n,`${o}$preview_${ks}`)}return ks++,Y(e)}function lp(e){let t=As.get(e.toString());return t?Y(t):En(e).pipe(g(r=>cp(r,e)),f(r=>(As.set(e.toString(),r),r)))}function Cs(e,t){let{sitemap$:r}=t;if(!(e instanceof HTMLAnchorElement))return y;if(!(X("navigation.instant.preview")||e.hasAttribute("data-preview")))return y;e.removeAttribute("title");let n=re([ir(e),Ft(e).pipe(ke(1))]).pipe(f(([i,a])=>i||a),ie(),L(i=>i));return $t([r,n]).pipe(g(([i])=>{let a=new URL(e.href);return a.search=a.hash="",i.has(`${a}`)?Y(a):y}),g(i=>lp(i)),g(i=>{let a=e.hash?`article [id="${decodeURIComponent(e.hash.slice(1))}"]`:"article h1",s=Le(a,i);return typeof s=="undefined"?y:Y(sp(s))})).pipe(g(i=>{let a=new U(s=>{let c=On(...i);return s.next(c),document.body.append(c),()=>c.remove()});return Rr(e,H({content$:a},t))}))}var Hs=".node circle,.node ellipse,.node path,.node polygon,.node rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}marker{fill:var(--md-mermaid-edge-color)!important}.edgeLabel .label rect{fill:#0000}.flowchartTitleText{fill:var(--md-mermaid-label-fg-color)}.label{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.label foreignObject{line-height:normal;overflow:visible}.label div .edgeLabel{color:var(--md-mermaid-label-fg-color)}.edgeLabel,.edgeLabel p,.label div .edgeLabel{background-color:var(--md-mermaid-label-bg-color)}.edgeLabel,.edgeLabel p{fill:var(--md-mermaid-label-bg-color);color:var(--md-mermaid-edge-color)}.edgePath .path,.flowchart-link{stroke:var(--md-mermaid-edge-color)}.edgePath .arrowheadPath{fill:var(--md-mermaid-edge-color);stroke:none}.cluster rect{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}.cluster span{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}g #flowchart-circleEnd,g #flowchart-circleStart,g #flowchart-crossEnd,g #flowchart-crossStart,g #flowchart-pointEnd,g #flowchart-pointStart{stroke:none}.classDiagramTitleText{fill:var(--md-mermaid-label-fg-color)}g.classGroup line,g.classGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.classGroup text{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.classLabel .box{fill:var(--md-mermaid-label-bg-color);background-color:var(--md-mermaid-label-bg-color);opacity:1}.classLabel .label{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.node .divider{stroke:var(--md-mermaid-node-fg-color)}.relation{stroke:var(--md-mermaid-edge-color)}.cardinality{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.cardinality text{fill:inherit!important}defs marker.marker.composition.class path,defs marker.marker.dependency.class path,defs marker.marker.extension.class path{fill:var(--md-mermaid-edge-color)!important;stroke:var(--md-mermaid-edge-color)!important}defs marker.marker.aggregation.class path{fill:var(--md-mermaid-label-bg-color)!important;stroke:var(--md-mermaid-edge-color)!important}.statediagramTitleText{fill:var(--md-mermaid-label-fg-color)}g.stateGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.stateGroup .state-title{fill:var(--md-mermaid-label-fg-color)!important;font-family:var(--md-mermaid-font-family)}g.stateGroup .composit{fill:var(--md-mermaid-label-bg-color)}.nodeLabel,.nodeLabel p{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}a .nodeLabel{text-decoration:underline}.node circle.state-end,.node circle.state-start,.start-state{fill:var(--md-mermaid-edge-color);stroke:none}.end-state-inner,.end-state-outer{fill:var(--md-mermaid-edge-color)}.end-state-inner,.node circle.state-end{stroke:var(--md-mermaid-label-bg-color)}.transition{stroke:var(--md-mermaid-edge-color)}[id^=state-fork] rect,[id^=state-join] rect{fill:var(--md-mermaid-edge-color)!important;stroke:none!important}.statediagram-cluster.statediagram-cluster .inner{fill:var(--md-default-bg-color)}.statediagram-cluster rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.statediagram-state rect.divider{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}defs #statediagram-barbEnd{stroke:var(--md-mermaid-edge-color)}[id^=entity] path,[id^=entity] rect{fill:var(--md-default-bg-color)}.relationshipLine{stroke:var(--md-mermaid-edge-color)}defs .marker.oneOrMore.er *,defs .marker.onlyOne.er *,defs .marker.zeroOrMore.er *,defs .marker.zeroOrOne.er *{stroke:var(--md-mermaid-edge-color)!important}text:not([class]):last-child{fill:var(--md-mermaid-label-fg-color)}.actor{fill:var(--md-mermaid-sequence-actor-bg-color);stroke:var(--md-mermaid-sequence-actor-border-color)}text.actor>tspan{fill:var(--md-mermaid-sequence-actor-fg-color);font-family:var(--md-mermaid-font-family)}line{stroke:var(--md-mermaid-sequence-actor-line-color)}.actor-man circle,.actor-man line{fill:var(--md-mermaid-sequence-actorman-bg-color);stroke:var(--md-mermaid-sequence-actorman-line-color)}.messageLine0,.messageLine1{stroke:var(--md-mermaid-sequence-message-line-color)}.note{fill:var(--md-mermaid-sequence-note-bg-color);stroke:var(--md-mermaid-sequence-note-border-color)}.loopText,.loopText>tspan,.messageText,.noteText>tspan{stroke:none;font-family:var(--md-mermaid-font-family)!important}.messageText{fill:var(--md-mermaid-sequence-message-fg-color)}.loopText,.loopText>tspan{fill:var(--md-mermaid-sequence-loop-fg-color)}.noteText>tspan{fill:var(--md-mermaid-sequence-note-fg-color)}#arrowhead path{fill:var(--md-mermaid-sequence-message-line-color);stroke:none}.loopLine{fill:var(--md-mermaid-sequence-loop-bg-color);stroke:var(--md-mermaid-sequence-loop-border-color)}.labelBox{fill:var(--md-mermaid-sequence-label-bg-color);stroke:none}.labelText,.labelText>span{fill:var(--md-mermaid-sequence-label-fg-color);font-family:var(--md-mermaid-font-family)}.sequenceNumber{fill:var(--md-mermaid-sequence-number-fg-color)}rect.rect{fill:var(--md-mermaid-sequence-box-bg-color);stroke:none}rect.rect+text.text{fill:var(--md-mermaid-sequence-box-fg-color)}defs #sequencenumber{fill:var(--md-mermaid-sequence-number-bg-color)!important}";var ko,pp=0;function fp(){return typeof mermaid=="undefined"||mermaid instanceof Element?ar("https://unpkg.com/mermaid@11/dist/mermaid.min.js"):Y(void 0)}function $s(e){return e.classList.remove("mermaid"),ko||(ko=fp().pipe($(()=>mermaid.initialize({startOnLoad:!1,themeCSS:Hs,sequence:{actorFontSize:"16px",messageFontSize:"16px",noteFontSize:"16px"}})),f(()=>{}),se(1))),ko.subscribe(()=>Uo(null,null,function*(){e.classList.add("mermaid");let t=`__mermaid_${pp++}`,r=A("div",{class:"mermaid"}),n=e.textContent,{svg:o,fn:i}=yield mermaid.render(t,n),a=r.attachShadow({mode:"closed"});a.innerHTML=o,e.replaceWith(r),i==null||i(a)})),ko.pipe(f(()=>({ref:e})))}var Ps=A("table");function Is(e){return e.replaceWith(Ps),Ps.replaceWith(gs(e)),Y({ref:e})}function mp(e){let t=e.find(r=>r.checked)||e[0];return R(...e.map(r=>b(r,"change").pipe(f(()=>G(`label[for="${r.id}"]`))))).pipe(J(G(`label[for="${t.id}"]`)),f(r=>({active:r})))}function Rs(e,{viewport$:t,target$:r}){let n=G(".tabbed-labels",e),o=P(":scope > input",e),i=Oo("prev");e.append(i);let a=Oo("next");return e.append(a),j(()=>{let s=new I,c=s.pipe(he(),ye(!0));re([s,Re(e),Et(e)]).pipe(Q(c),Xe(1,je)).subscribe({next([{active:l},u]){let p=wt(l),{width:d}=Ae(l);e.style.setProperty("--md-indicator-x",`${p.x}px`),e.style.setProperty("--md-indicator-width",`${d}px`);let m=ln(n);(p.xm.x+u.width)&&n.scrollTo({left:Math.max(0,p.x-16),behavior:"smooth"})},complete(){e.style.removeProperty("--md-indicator-x"),e.style.removeProperty("--md-indicator-width")}}),re([Ut(n),Re(n)]).pipe(Q(c)).subscribe(([l,u])=>{let p=Mr(n);i.hidden=l.x<16,a.hidden=l.x>p.width-u.width-16}),R(b(i,"click").pipe(f(()=>-1)),b(a,"click").pipe(f(()=>1))).pipe(Q(c)).subscribe(l=>{let{width:u}=Ae(n);n.scrollBy({left:u*l,behavior:"smooth"})}),r.pipe(Q(c),L(l=>o.includes(l))).subscribe(l=>l.click()),n.classList.add("tabbed-labels--linked");for(let l of o){let u=G(`label[for="${l.id}"]`);u.replaceChildren(A("a",{href:`#${u.htmlFor}`,tabIndex:-1},...Array.from(u.childNodes))),b(u.firstElementChild,"click").pipe(Q(c),L(p=>!(p.metaKey||p.ctrlKey)),$(p=>{p.preventDefault(),p.stopPropagation()})).subscribe(()=>{history.replaceState({},"",`#${u.htmlFor}`),u.click()})}return X("content.tabs.link")&&s.pipe(ke(1),pe(t)).subscribe(([{active:l},{offset:u}])=>{let p=l.innerText.trim();if(l.hasAttribute("data-md-switching"))l.removeAttribute("data-md-switching");else{let d=e.offsetTop-u.y;for(let h of P("[data-tabs]"))for(let v of P(":scope > input",h)){let x=G(`label[for="${v.id}"]`);if(x!==l&&x.innerText.trim()===p){x.setAttribute("data-md-switching",""),v.click();break}}window.scrollTo({top:e.offsetTop-d});let m=__md_get("__tabs")||[];__md_set("__tabs",[...new Set([p,...m])])}}),s.pipe(Q(c)).subscribe(()=>{for(let l of P("audio, video",e))l.offsetWidth&&l.autoplay?l.play().catch(()=>{}):l.pause()}),mp(o).pipe($(l=>s.next(l)),V(()=>s.complete()),f(l=>H({ref:e},l)))}).pipe(Ht(ge))}function js(e,t){let{viewport$:r,target$:n,print$:o}=t;return R(...P(".annotate:not(.highlight)",e).map(i=>Es(i,{target$:n,print$:o})),...P("pre:not(.mermaid) > code",e).map(i=>Ls(i,{target$:n,print$:o})),...P("a",e).map(i=>Cs(i,t)),...P("pre.mermaid",e).map(i=>$s(i)),...P("table:not([class])",e).map(i=>Is(i)),...P("details",e).map(i=>Ms(i,{target$:n,print$:o})),...P("[data-tabs]",e).map(i=>Rs(i,{viewport$:r,target$:n})),...P("[title]:not([data-preview])",e).filter(()=>X("content.tooltips")).map(i=>Ge(i,{viewport$:r})),...P(".footnote-ref",e).filter(()=>X("content.footnote.tooltips")).map(i=>Rr(i,{content$:new U(a=>{let s=new URL(i.href).hash.slice(1),c=Array.from(document.getElementById(s).cloneNode(!0).children),l=On(...c);return a.next(l),document.body.append(l),()=>l.remove()}),viewport$:r})))}function dp(e,{alert$:t}){return t.pipe(g(r=>R(Y(!0),Y(!1).pipe(It(2e3))).pipe(f(n=>({message:r,active:n})))))}function Fs(e,t){let r=G(".md-typeset",e);return j(()=>{let n=new I;return n.subscribe(({message:o,active:i})=>{e.classList.toggle("md-dialog--active",i),r.textContent=o}),dp(e,t).pipe($(o=>n.next(o)),V(()=>n.complete()),f(o=>H({ref:e},o)))})}function hp({viewport$:e}){if(!X("header.autohide"))return Y(!1);let t=e.pipe(f(({offset:{y:o}})=>o),Pt(2,1),f(([o,i])=>[oMath.abs(i-o.y)>100),f(([,[o]])=>o),ie()),n=Tn("search");return re([e,n]).pipe(f(([{offset:o},i])=>o.y>400&&!i),ie(),g(o=>o?r:Y(!1)),J(!1))}function Us(e,t){return j(()=>re([Re(e),hp(t)])).pipe(f(([{height:r},n])=>({height:r,hidden:n})),ie((r,n)=>r.height===n.height&&r.hidden===n.hidden),se(1))}function Ns(e,{viewport$:t,header$:r,main$:n}){return j(()=>{let o=new I,i=o.pipe(he(),ye(!0));o.pipe(fe("active"),Ze(r)).subscribe(([{active:s},{hidden:c}])=>{e.classList.toggle("md-header--shadow",s&&!c),e.hidden=c});let a=me(P("[title]",e)).pipe(L(()=>X("content.tooltips")),oe(s=>Ge(s,{viewport$:t})));return n.subscribe(o),r.pipe(Q(i),f(s=>H({ref:e},s)),Rt(a.pipe(Q(i))))})}function vp(e,{viewport$:t,header$:r}){return Sn(e,{viewport$:t,header$:r}).pipe(f(({offset:{y:n}})=>{let{height:o}=Ae(e);return{active:o>0&&n>=o}}),fe("active"))}function Ds(e,t){return j(()=>{let r=new I;r.subscribe({next({active:o}){e.classList.toggle("md-header__title--active",o)},complete(){e.classList.remove("md-header__title--active")}});let n=Le(".md-content h1");return typeof n=="undefined"?y:vp(n,t).pipe($(o=>r.next(o)),V(()=>r.complete()),f(o=>H({ref:e},o)))})}function Ws(e,{viewport$:t,header$:r}){let n=r.pipe(f(({height:i})=>i),ie()),o=n.pipe(g(()=>Re(e).pipe(f(({height:i})=>({top:e.offsetTop,bottom:e.offsetTop+i})),fe("bottom"))));return re([n,o,t]).pipe(f(([i,{top:a,bottom:s},{offset:{y:c},size:{height:l}}])=>(l=Math.max(0,l-Math.max(0,a-c,i)-Math.max(0,l+c-s)),{offset:a-i,height:l,active:a-i<=c})),ie((i,a)=>i.offset===a.offset&&i.height===a.height&&i.active===a.active))}function bp(e){let t=__md_get("__palette")||{index:e.findIndex(n=>matchMedia(n.getAttribute("data-md-color-media")).matches)},r=Math.max(0,Math.min(t.index,e.length-1));return Y(...e).pipe(oe(n=>b(n,"change").pipe(f(()=>n))),J(e[r]),f(n=>({index:e.indexOf(n),color:{media:n.getAttribute("data-md-color-media"),scheme:n.getAttribute("data-md-color-scheme"),primary:n.getAttribute("data-md-color-primary"),accent:n.getAttribute("data-md-color-accent")}})),se(1))}function Vs(e){let t=P("input",e),r=A("meta",{name:"theme-color"});document.head.appendChild(r);let n=A("meta",{name:"color-scheme"});document.head.appendChild(n);let o=Ir("(prefers-color-scheme: light)");return j(()=>{let i=new I;return i.subscribe(a=>{if(document.body.setAttribute("data-md-color-switching",""),a.color.media==="(prefers-color-scheme)"){let s=matchMedia("(prefers-color-scheme: light)"),c=document.querySelector(s.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");a.color.scheme=c.getAttribute("data-md-color-scheme"),a.color.primary=c.getAttribute("data-md-color-primary"),a.color.accent=c.getAttribute("data-md-color-accent")}for(let[s,c]of Object.entries(a.color))document.body.setAttribute(`data-md-color-${s}`,c);for(let s=0;sa.key==="Enter"),pe(i,(a,s)=>s)).subscribe(({index:a})=>{a=(a+1)%t.length,t[a].click(),t[a].focus()}),i.pipe(f(()=>{let a=ht("header"),s=window.getComputedStyle(a);return n.content=s.colorScheme,s.backgroundColor.match(/\d+/g).map(c=>(+c).toString(16).padStart(2,"0")).join("")})).subscribe(a=>r.content=`#${a}`),i.pipe(Ie(ge)).subscribe(()=>{document.body.removeAttribute("data-md-color-switching")}),bp(t).pipe(Q(o.pipe(ke(1))),jt(),$(a=>i.next(a)),V(()=>i.complete()),f(a=>H({ref:e},a)))})}function zs(e,{progress$:t}){return j(()=>{let r=new I;return r.subscribe(({value:n})=>{e.style.setProperty("--md-progress-value",`${n}`)}),t.pipe($(n=>r.next({value:n})),V(()=>r.complete()),f(n=>({ref:e,value:n})))})}var qs='.v u{text-decoration:underline!important;text-decoration-style:wavy!important;text-decoration-thickness:1px!important}.p{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);background-color:rgba(var(--color-backdrop)/var(--alpha-lighter));cursor:pointer;height:100%;pointer-events:auto;position:absolute;transition:opacity .25s;width:100%}.p.m{opacity:0;pointer-events:none;transition:opacity .35s}.r{align-items:center;background-color:initial;border:none;border-radius:var(--space-2);cursor:pointer;display:flex;flex-shrink:0;font-family:var(--font-family);height:36px;justify-content:center;outline:none;padding:0;position:relative;transition:background-color .25s,color .25s;width:36px;z-index:1}.r svg{stroke:rgb(var(--color-foreground));height:18px;opacity:.5;width:18px}.r:before{background-color:rgb(var(--color-background-subtle));border-radius:var(--border-radius-2);content:"";inset:0;opacity:0;position:absolute;transform:scale(.75);transition:transform 125ms,opacity 125ms;z-index:0}.r:hover:before{opacity:1;transform:scale(1)}.r.c{cursor:auto}.r.c:before{display:none}.n{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);background-color:rgba(var(--color-background)/var(--alpha-light));border-radius:var(--space-3);box-shadow:0 0 60px #0000000d;display:flex;height:480px;overflow:hidden;pointer-events:auto;position:absolute;transition:transform .25s cubic-bezier(.16,1,.3,1),opacity .25s;width:640px}.n.l{opacity:0;pointer-events:none;transform:scale(1.1);transition:transform .25s .15s,opacity .15s}@media (max-width:680px){.n{border-radius:0;height:100%;width:100%}}.u{display:flex;flex-basis:min-content;flex-direction:column;flex-grow:1;flex-shrink:0}@keyframes d{0%{transform:scale(0)}50%{transform:scale(1.2)}to{transform:scale(1)}}.y{animation:d .25s ease-in-out;background:var(--color-highlight);border-radius:100%;color:#fff;font-size:8px;font-weight:700;height:12px;padding-top:1px;position:absolute;right:4px;top:4px;width:12px}.i{background-color:rgb(var(--color-background-subtle)/var(--alpha-lighter));flex-shrink:0;overflow:scroll;position:relative;transition:width .35s cubic-bezier(.16,1,.3,1),opacity .25s;width:200px}.i>*{transform:translate(0);transition:transform .25s cubic-bezier(.16,1,.3,1)}.i.l{opacity:0;width:0}.i.l>*{transform:translate(-48px)}@media (max-width:680px){.i{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);background-color:rgba(var(--color-background-subtle)/var(--alpha-light));box-shadow:0 0 60px #00000026;height:100%;position:absolute;right:0;top:0}}.w{border-bottom:1px solid rgb(var(--color-foreground)/var(--alpha-lightest));display:flex;gap:var(--space-1);padding:var(--space-2)}.k{-webkit-overflow-scrolling:touch;overflow:auto;overscroll-behavior:contain}.z{padding:8px 10px}.X{color:rgb(var(--color-foreground)/var(--alpha-light));padding:var(--space-2);position:absolute;width:200px}.X,.j{display:flex;flex-direction:column}.j{gap:2px;list-style:none;padding:0}.F,.j{margin:0}.F{font-size:16px;font-weight:400}.F,.I{padding:8px}.I{font-size:14px;margin:4px 0 0;opacity:.5}.I,.o{font-size:12px}.o{cursor:pointer;display:flex;padding:4px 8px;position:relative}.o:before{background-color:var(--color-highlight-transparent);border-radius:var(--space-1);content:"";inset:0;opacity:0;position:absolute;transform:scale(.75);transition:transform 125ms,opacity 125ms;z-index:0}.o.g:before,.o:hover:before{opacity:1;transform:scale(1)}.o.g,.o:hover{color:var(--color-highlight)}.R{flex-grow:1}.R,.q{position:relative}.q{font-weight:700}.f{flex-grow:1}.f input{background:#0000;border:none;color:rgb(var(--color-foreground));font-family:var(--font-family);font-size:16px;height:100%;letter-spacing:-.25px;outline:none;width:100%}.b{color:rgb(var(--color-foreground)/var(--alpha-light));display:flex;flex-direction:column;gap:2px;line-height:1.3;list-style:none;margin:var(--space-2);margin-top:0;padding:0}.A,.b li{margin:0}.A{color:rgb(var(--color-foreground)/var(--alpha-lighter));font-size:12px;margin-top:var(--space-2);padding:0 18px}.a{border-radius:var(--space-2);color:inherit;cursor:pointer;display:flex;flex-direction:row;flex-grow:1;padding:8px 10px;position:relative;text-decoration:none}.a:before{background-color:rgb(var(--color-background-subtle));border-radius:var(--border-radius-2);content:"";display:block;inset:0;opacity:0;position:absolute;transform:scale(.9);transition:transform 125ms,opacity 125ms;z-index:0}@media (pointer:fine){.a.h:before,.a:hover:before{opacity:1;transform:scale(1)}}.a mark{background:#0000;color:var(--color-highlight)}.a u{background-color:var(--color-highlight-transparent);border-radius:2px;box-shadow:0 0 0 1px var(--color-highlight-transparent);text-decoration:none}.B{flex-grow:1}.s{margin-right:-8px;opacity:0;position:relative;transform:translate(-2px);transition:transform 125ms,opacity 125ms;z-index:0}@media (pointer:fine){.h>.s,:hover>.s{opacity:1;transform:none}}.x{font-size:14px;margin:0;position:relative}.x code{background:rgb(var(--color-background-subtle));border-radius:var(--space-1);font-size:13px;padding:2px 4px}.t{color:rgb(var(--color-foreground)/var(--alpha-lighter));display:inline-flex;flex-wrap:wrap;font-size:12px;gap:var(--space-1);list-style:none;margin:0;padding:0;position:relative}.t li{white-space:nowrap}.t li:after{content:"/";display:inline;margin-left:var(--space-1)}.t li:last-child:after{content:"";display:none}.e{--space-1:4px;--space-2:calc(var(--space-1)*2);--space-3:calc(var(--space-2)*2);--space-4:calc(var(--space-3)*2);--space-5:calc(var(--space-4)*2);--alpha-light:.7;--alpha-lighter:.54;--alpha-lightest:.1;--color-highlight:var(--md-accent-fg-color,#526cfe);--color-highlight-transparent:var(--md-accent-fg-color--transparent,#526cfe1a);--border-radius-1:var(--space-1);--border-radius-2:var(--space-2);--border-radius-3:calc(var(--space-1) + var(--space-2));--font-family:var(--md-text-font-family,Inter,Roboto Flex,system-ui,sans-serif);--font-size:16px;--line-height:1.5;--letter-spacing:-.5px;-webkit-font-smoothing:antialiased;align-items:center;display:flex;font-family:var(--font-family);font-size:var(--font-size);height:100vh;justify-content:center;letter-spacing:var(--letter-spacing);line-height:var(--line-height);pointer-events:none;position:absolute;width:100vw}@media (pointer:coarse){.e{height:-webkit-fill-available}}.e *,.e :after,.e :before{box-sizing:border-box}';function Ks(e,{index$:t}){let r=Ue(),n=document.createElement("div");document.body.appendChild(n),n.style.position="fixed",n.style.height="100%",n.style.top="0",n.style.zIndex="4";let o=n.attachShadow({mode:"open"});o.appendChild(A("style",{},qs.toString()));try{Ya(r.search,{highlight:r.features.includes("search.highlight")}),me(t).subscribe(i=>{for(let a of i.items)a.location=new URL(a.location,r.base).toString();Ja(i,o)}),b(e,"click").subscribe(()=>{go()}),Tn("search").pipe(ke(1)).subscribe(()=>go())}catch(i){e.hidden=!0;let a=G("label[for=__search]");a.hidden=!0}return Ke}var Bs=_r(So());function Ys(e,{index$:t,location$:r}){return re([t,r.pipe(J(Ye()),L(n=>!!n.searchParams.get("h")))]).pipe(f(([n,o])=>_p(n.config)(o.searchParams.get("h"))),f(n=>{var a;let o=new Map,i=document.createNodeIterator(e,NodeFilter.SHOW_TEXT);for(let s=i.nextNode();s;s=i.nextNode())if((a=s.parentElement)!=null&&a.offsetHeight){let c=s.textContent,l=n(c);l.length>c.length&&o.set(s,l)}for(let[s,c]of o){let{childNodes:l}=A("span",null,c);s.replaceWith(...Array.from(l))}return{ref:e,nodes:o}}))}function _p(e){let t=e.separator.split("|").map(o=>o.replace(/(\(\?[!=<][^)]+\))/g,"").length===0?"\uFFFD":o).join("|"),r=new RegExp(t,"img"),n=(o,i,a)=>`${i}${a}`;return o=>{o=o.replace(/\s+/g," ").replace(/&/g,"&").trim();let i=new RegExp(`(^|${e.separator}|)(${o.split(r).map(a=>a.replace(/[|\\{}()[\]^$+*?.-]/g,"\\$&")).filter(a=>a.length>0).join("|")})`,"img");return a=>(0,Bs.default)(a).replace(i,n).replace(/<\/mark>(\s+)]*>/img,"$1")}}function yp(e,{viewport$:t,main$:r}){let n=e.closest(".md-grid"),o=n.offsetTop-n.parentElement.offsetTop;return re([r,t]).pipe(f(([{offset:i,height:a},{offset:{y:s}}])=>(a=a+Math.min(o,Math.max(0,s-i))-o,{height:a,locked:s>=i+o})),ie((i,a)=>i.height===a.height&&i.locked===a.locked))}function Ao(e,n){var o=n,{header$:t}=o,r=gr(o,["header$"]);let i=G(".md-sidebar__scrollwrap",e),{y:a}=wt(i);return j(()=>{let s=new I,c=s.pipe(he(),ye(!0)),l=s.pipe(Xe(0,je));return l.pipe(pe(t)).subscribe({next([{height:u},{height:p}]){i.style.height=`${u-2*a}px`,e.style.top=`${p}px`},complete(){i.style.height="",e.style.top=""}}),l.pipe(Sr()).subscribe(()=>{for(let u of P(".md-nav__link--active[href]",e)){if(!u.clientHeight)continue;let p=u.closest(".md-sidebar__scrollwrap");if(typeof p!="undefined"){let d=u.offsetTop-p.offsetTop,{height:m}=Ae(p);p.scrollTo({top:d-m/2})}}}),me(P("label[tabindex]",e)).pipe(oe(u=>b(u,"click").pipe(Ie(ge),f(()=>u),Q(c)))).subscribe(u=>{let p=G(`[id="${u.htmlFor}"]`);G(`[aria-labelledby="${u.id}"]`).setAttribute("aria-expanded",`${p.checked}`)}),X("content.tooltips")&&me(P("abbr[title]",e)).pipe(oe(u=>Ge(u,{viewport$})),Q(c)).subscribe(),yp(e,r).pipe($(u=>s.next(u)),V(()=>s.complete()),f(u=>H({ref:e},u)))})}function Gs(e,t){if(typeof t!="undefined"){let r=`https://api.github.com/repos/${e}/${t}`;return $t(et(`${r}/releases/latest`).pipe(_e(()=>y),f(n=>({version:n.tag_name})),ot({})),et(r).pipe(_e(()=>y),f(n=>({stars:n.stargazers_count,forks:n.forks_count})),ot({}))).pipe(f(([n,o])=>H(H({},n),o)))}else{let r=`https://api.github.com/users/${e}`;return et(r).pipe(f(n=>({repositories:n.public_repos})),ot({}))}}function Js(e,t){let r=`https://${e}/api/v4/projects/${encodeURIComponent(t)}`;return $t(et(`${r}/releases/permalink/latest`).pipe(_e(()=>y),f(({tag_name:n})=>({version:n})),ot({})),et(r).pipe(_e(()=>y),f(({star_count:n,forks_count:o})=>({stars:n,forks:o})),ot({}))).pipe(f(([n,o])=>H(H({},n),o)))}function Xs(e){let t=e.match(/^.+github\.com\/([^/]+)\/?([^/]+)?/i);if(t){let[,r,n]=t;return Gs(r,n)}if(t=e.match(/^.+?([^/]*gitlab[^/]+)\/(.+?)\/?$/i),t){let[,r,n]=t;return Js(r,n)}return y}var xp;function wp(e){return xp||(xp=j(()=>{let t=__md_get("__source",sessionStorage);if(t)return Y(t);if(Ee("consent").length){let n=__md_get("__consent");if(!(n&&n.github))return y}return Xs(e.href).pipe($(n=>__md_set("__source",n,sessionStorage)))}).pipe(_e(()=>y),L(t=>Object.keys(t).length>0),f(t=>({facts:t})),se(1)))}function Zs(e){let t=G(":scope > :last-child",e);return j(()=>{let r=new I;return r.subscribe(({facts:n})=>{t.appendChild(bs(n)),t.classList.add("md-source__repository--active")}),wp(e).pipe($(n=>r.next(n)),V(()=>r.complete()),f(n=>H({ref:e},n)))})}function Ep(e,{viewport$:t,header$:r}){return Re(document.body).pipe(g(()=>Sn(e,{header$:r,viewport$:t})),f(({offset:{y:n}})=>({hidden:n>=10})),fe("hidden"))}function Qs(e,t){return j(()=>{let r=new I;return r.subscribe({next({hidden:n}){e.hidden=n},complete(){e.hidden=!1}}),(X("navigation.tabs.sticky")?Y({hidden:!1}):Ep(e,t)).pipe($(n=>r.next(n)),V(()=>r.complete()),f(n=>H({ref:e},n)))})}function Tp(e,{viewport$:t,header$:r}){let n=new Map,o=P(".md-nav__link",e);for(let s of o){let c=decodeURIComponent(s.hash.substring(1)),l=Le(`[id="${c}"]`);typeof l!="undefined"&&n.set(s,l)}let i=r.pipe(fe("height"),f(({height:s})=>{let c=ht("main"),l=G(":scope > :first-child",c);return s+.9*(l.offsetTop-c.offsetTop)}),xe());return Re(document.body).pipe(fe("height"),g(s=>j(()=>{let c=[];return Y([...n].reduce((l,[u,p])=>{for(;c.length&&n.get(c[c.length-1]).tagName>=p.tagName;)c.pop();let d=p.offsetTop;for(;!d&&p.parentElement;)p=p.parentElement,d=p.offsetTop;let m=p.offsetParent;for(;m;m=m.offsetParent)d+=m.offsetTop;return l.set([...c=[...c,u]].reverse(),d)},new Map))}).pipe(f(c=>new Map([...c].sort(([,l],[,u])=>l-u))),Ze(i),g(([c,l])=>t.pipe(Or(([u,p],{offset:{y:d},size:m})=>{let h=d+m.height>=Math.floor(s.height);for(;p.length;){let[,v]=p[0];if(v-l=d&&!h)p=[u.pop(),...p];else break}return[u,p]},[[],[...c]]),ie((u,p)=>u[0]===p[0]&&u[1]===p[1])))))).pipe(f(([s,c])=>({prev:s.map(([l])=>l),next:c.map(([l])=>l)})),J({prev:[],next:[]}),Pt(2,1),f(([s,c])=>s.prev.length{let i=new I,a=i.pipe(he(),ye(!0));if(i.subscribe(({prev:s,next:c})=>{for(let[l]of c)l.classList.remove("md-nav__link--passed"),l.classList.remove("md-nav__link--active");for(let[l,[u]]of s.entries())u.classList.add("md-nav__link--passed"),u.classList.toggle("md-nav__link--active",l===s.length-1)}),X("toc.follow")){let s=R(t.pipe(Be(1),f(()=>{})),t.pipe(Be(250),f(()=>"smooth")));i.pipe(L(({prev:c})=>c.length>0),Ze(n.pipe(Ie(ge))),pe(s)).subscribe(([[{prev:c}],l])=>{let[u]=c[c.length-1];if(u.offsetHeight){let p=ki(u);if(typeof p!="undefined"){let d=u.offsetTop-p.offsetTop,{height:m}=Ae(p);p.scrollTo({top:d-m/2,behavior:l})}}})}return X("navigation.tracking")&&t.pipe(Q(a),fe("offset"),Be(250),ke(1),Q(o.pipe(ke(1))),jt({delay:250}),pe(i)).subscribe(([,{prev:s}])=>{let c=Ye(),l=s[s.length-1];if(l&&l.length){let[u]=l,{hash:p}=new URL(u.href);c.hash!==p&&(c.hash=p,history.replaceState({},"",`${c}`))}else c.hash="",history.replaceState({},"",`${c}`)}),Tp(e,{viewport$:t,header$:r}).pipe($(s=>i.next(s)),V(()=>i.complete()),f(s=>H({ref:e},s)))})}function Sp(e,{viewport$:t,main$:r,target$:n}){let o=t.pipe(f(({offset:{y:a}})=>a),Pt(2,1),f(([a,s])=>a>s&&s>0),ie()),i=r.pipe(f(({active:a})=>a));return re([i,o]).pipe(f(([a,s])=>!(a&&s)),ie(),Q(n.pipe(ke(1))),ye(!0),jt({delay:250}),f(a=>({hidden:a})))}function tc(e,{viewport$:t,header$:r,main$:n,target$:o}){let i=new I,a=i.pipe(he(),ye(!0));return i.subscribe({next({hidden:s}){e.hidden=s,s?(e.setAttribute("tabindex","-1"),e.blur()):e.removeAttribute("tabindex")},complete(){e.style.top="",e.hidden=!0,e.removeAttribute("tabindex")}}),r.pipe(Q(a),fe("height")).subscribe(({height:s})=>{e.style.top=`${s+16}px`}),b(e,"click").subscribe(s=>{s.preventDefault(),window.scrollTo({top:0})}),Sp(e,{viewport$:t,main$:n,target$:o}).pipe($(s=>i.next(s)),V(()=>i.complete()),f(s=>H({ref:e},s)))}function rc(e,t){return e.protocol=t.protocol,e.hostname=t.hostname,t.port&&(e.port=t.port),e}function Op(e,t){let r=new Map;for(let n of P("url",e)){let o=G("loc",n),i=[rc(new URL(o.textContent),t)];r.set(`${i[0]}`,i);for(let a of P("[rel=alternate]",n)){let s=a.getAttribute("href");s!=null&&i.push(rc(new URL(s),t))}}return r}function dr(e){return ns(new URL("sitemap.xml",e)).pipe(f(t=>Op(t,new URL(e))),_e(()=>Y(new Map)),xe())}function __ha_langroot(e){let t=new URL(e),r=t.pathname.match(/^\/(zh-hant|en|ja|ru)(?:\/|$)/);return t.pathname=r?`/${r[1]}/`:"/",t.search="",t.hash="",t}function nc({document$:e}){let t=new Map;e.pipe(g(()=>P("link[rel=alternate]")),f(r=>__ha_langroot(r.href)),L(r=>!t.has(r.toString())),oe(r=>dr(r).pipe(f(n=>[r,n]),_e(()=>y)))).subscribe(([r,n])=>{t.set(r.toString().replace(/\/$/,""),n)}),b(document.body,"click").pipe(L(r=>!r.metaKey&&!r.ctrlKey),g(r=>{if(r.target instanceof Element){let n=r.target.closest("a");if(n&&!n.target){let o=[...t].find(([p])=>n.href.startsWith(`${p}/`));if(typeof o=="undefined")return y;let[i,a]=o,s=Ye();if(s.href.startsWith(i))return y;let c=Ue(),l=s.href.replace(c.base,"");l=`${i}/${l}`;let u=a.has(l.split("#")[0])?new URL(l,c.base):new URL(i);return r.preventDefault(),Y(u)}}return y})).subscribe(r=>dt(r,!0))}var Co=_r(Mo());function Lp(e){e.setAttribute("data-md-copying","");let t=e.closest("[data-copy]"),r=t?t.getAttribute("data-copy"):e.innerText;return e.removeAttribute("data-md-copying"),r.trimEnd()}function oc({alert$:e}){Co.default.isSupported()&&new U(t=>{new Co.default("[data-clipboard-target], [data-clipboard-text]",{text:r=>r.getAttribute("data-clipboard-text")||Lp(G(r.getAttribute("data-clipboard-target")))}).on("success",r=>t.next(r))}).pipe($(t=>{t.trigger.focus()}),f(()=>Bt("clipboard.copied"))).subscribe(e)}function ic(e,t){if(!(e.target instanceof Element))return y;let r=e.target.closest("a");if(r===null)return y;if(r.target||e.metaKey||e.ctrlKey)return y;let n=new URL(r.href);return n.search=n.hash="",t.has(`${n}`)?(e.preventDefault(),Y(r)):y}function ac(e){let t=new Map;for(let r of P(":scope > *",e.head))t.set(r.outerHTML,r);return t}function sc(e){for(let t of P("[href], [src]",e))for(let r of["href","src"]){let n=t.getAttribute(r);if(n&&!/^(?:[a-z]+:)?\/\//i.test(n)){t[r]=t[r];break}}return Y(e)}function Mp(e){for(let n of["[data-md-component=announce]","[data-md-component=container]","[data-md-component=header-topic]","[data-md-component=outdated]","[data-md-component=logo]","[data-md-component=skip]",...X("navigation.tabs.sticky")?["[data-md-component=tabs]"]:[]]){let o=Le(n),i=Le(n,e);typeof o!="undefined"&&typeof i!="undefined"&&o.replaceWith(i)}let t=ac(document);for(let[n,o]of ac(e))t.has(n)?t.delete(n):document.head.appendChild(o);for(let n of t.values()){let o=n.getAttribute("name");o!=="theme-color"&&o!=="color-scheme"&&n.remove()}let r=ht("container");return nt(P("script",r)).pipe(g(n=>{let o=e.createElement("script");if(n.src){for(let i of n.getAttributeNames())o.setAttribute(i,n.getAttribute(i));return n.replaceWith(o),new U(i=>{o.onload=()=>i.complete()})}else return o.textContent=n.textContent,n.replaceWith(o),y}),he(),ye(document))}function cc({sitemap$:e,location$:t,viewport$:r,progress$:n}){if(location.protocol==="file:")return Ke;Y(document).subscribe(sc);let o=b(document.body,"click").pipe(Ze(e),g(([s,c])=>ic(s,c)),f(({href:s})=>new URL(s)),xe()),i=b(window,"popstate").pipe(f(Ye),xe());o.pipe(pe(r)).subscribe(([s,{offset:c}])=>{history.replaceState(c,""),history.pushState(null,"",s)}),R(o,i).subscribe(t);let a=t.pipe(fe("pathname"),g(s=>En(s,{progress$:n}).pipe(_e(()=>(dt(s,!0),y)))),g(sc),g(Mp),xe());return R(a.pipe(pe(t,(s,c)=>c)),a.pipe(g(()=>t),fe("hash")),t.pipe(ie((s,c)=>s.pathname===c.pathname&&s.hash===c.hash),g(()=>o),$(()=>history.back()))).subscribe(s=>{var c,l;history.state!==null||!s.hash?window.scrollTo(0,(l=(c=history.state)==null?void 0:c.y)!=null?l:0):(history.scrollRestoration="auto",es(s.hash),history.scrollRestoration="manual")}),t.subscribe(()=>{history.scrollRestoration="manual"}),b(window,"beforeunload").subscribe(()=>{history.scrollRestoration="auto"}),r.pipe(fe("offset"),Be(100)).subscribe(({offset:s})=>{history.replaceState(s,"")}),X("navigation.instant.prefetch")&&R(b(document.body,"mousemove"),b(document.body,"focusin")).pipe(Ze(e),g(([s,c])=>ic(s,c)),Be(25),Yn(({href:s})=>s),cn(s=>{let c=document.createElement("link");return c.rel="prefetch",c.href=s.toString(),document.head.appendChild(c),b(c,"load").pipe(f(()=>c),Me(1))})).subscribe(s=>s.remove()),a}function lc(e){var u;let{selectedVersionSitemap:t,selectedVersionBaseURL:r,currentLocation:n,currentBaseURL:o}=e,i=(u=Ho(o))==null?void 0:u.pathname;if(i===void 0)return;let a=kp(n.pathname,i);if(a===void 0)return;let s=Cp(t.keys());if(!t.has(s))return;let c=Ho(a,s);if(!c||!t.has(c.href))return;let l=Ho(a,r);if(l)return l.hash=n.hash,l.search=n.search,l}function Ho(e,t){try{return new URL(e,t)}catch(r){return}}function kp(e,t){if(e.startsWith(t))return e.slice(t.length)}function Ap(e,t){let r=Math.min(e.length,t.length),n;for(n=0;ny)),n=r.pipe(f(o=>{let[,i]=t.base.match(/([^/]+)\/?$/);return o.find(({version:a,aliases:s})=>a===i||s.includes(i))||o[0]}));r.pipe(f(o=>new Map(o.map(i=>[`${new URL(`../${i.version}/`,t.base)}`,i]))),g(o=>b(document.body,"click").pipe(L(i=>!i.metaKey&&!i.ctrlKey),pe(n),g(([i,a])=>{if(i.target instanceof Element){let s=i.target.closest("a");if(s&&!s.target&&o.has(s.href)){let c=s.href;return!i.target.closest(".md-version")&&o.get(c)===a?y:(i.preventDefault(),Y(new URL(c)))}}return y}),g(i=>dr(i).pipe(f(a=>{var s;return(s=lc({selectedVersionSitemap:a,selectedVersionBaseURL:i,currentLocation:Ye(),currentBaseURL:t.base}))!=null?s:i})))))).subscribe(o=>dt(o,!0)),re([r,n]).subscribe(([o,i])=>{G(".md-header__topic").appendChild(_s(o,i))}),e.pipe(g(()=>n)).subscribe(o=>{var s;let i=new URL(t.base),a=__md_get("__outdated",sessionStorage,i);if(a===null){a=!0;let c=((s=t.version)==null?void 0:s.default)||"latest";Array.isArray(c)||(c=[c]);e:for(let l of c)for(let u of o.aliases.concat(o.version))if(new RegExp(l,"i").test(u)){a=!1;break e}__md_set("__outdated",a,sessionStorage,i)}if(a)for(let c of Ee("outdated"))c.hidden=!1})}function pc({document$:e,viewport$:t}){e.pipe(g(()=>P(".md-ellipsis")),oe(r=>Et(r).pipe(Q(e.pipe(ke(1))),L(n=>n),f(()=>r),Me(1))),L(r=>r.offsetWidth{let n=r.innerText,o=r.closest("a")||r;return o.title=n,X("content.tooltips")?Ge(o,{viewport$:t}).pipe(Q(e.pipe(ke(1))),V(()=>o.removeAttribute("title"))):y})).subscribe(),X("content.tooltips")&&e.pipe(g(()=>P(".md-status")),oe(r=>Ge(r,{viewport$:t}))).subscribe()}function fc({document$:e,tablet$:t}){e.pipe(g(()=>P(".md-toggle--indeterminate")),$(r=>{r.indeterminate=!0,r.checked=!1}),oe(r=>b(r,"change").pipe(Xn(()=>r.classList.contains("md-toggle--indeterminate")),f(()=>r))),pe(t)).subscribe(([r,n])=>{r.classList.remove("md-toggle--indeterminate"),n&&(r.checked=!1)})}function Hp(){return/(iPad|iPhone|iPod)/.test(navigator.userAgent)}function mc({document$:e}){e.pipe(g(()=>P("[data-md-scrollfix]")),$(t=>t.removeAttribute("data-md-scrollfix")),L(Hp),oe(t=>b(t,"touchstart").pipe(f(()=>t)))).subscribe(t=>{let r=t.scrollTop;r===0?t.scrollTop=1:r+t.offsetHeight===t.scrollHeight&&(t.scrollTop=r-1)})}Object.entries||(Object.entries=function(e){let t=[];for(let r of Object.keys(e))t.push([r,e[r]]);return t});Object.values||(Object.values=function(e){let t=[];for(let r of Object.keys(e))t.push(e[r]);return t});typeof Element!="undefined"&&(Element.prototype.scrollTo||(Element.prototype.scrollTo=function(e,t){typeof e=="object"?(this.scrollLeft=e.left,this.scrollTop=e.top):(this.scrollLeft=e,this.scrollTop=t)}),Element.prototype.replaceWith||(Element.prototype.replaceWith=function(...e){let t=this.parentNode;if(t){e.length===0&&t.removeChild(this);for(let r=e.length-1;r>=0;r--){let n=e[r];typeof n=="string"?n=document.createTextNode(n):n.parentNode&&n.parentNode.removeChild(n),r?t.insertBefore(this.previousSibling,n):t.replaceChild(n,this)}}}));function $p(){return location.protocol==="file:"?ar(`${new URL("search.js",Mn.base)}`).pipe(f(()=>__index),_e(()=>Ke),se(1)):et(new URL("search.json",Mn.base))}document.documentElement.classList.remove("no-js");document.documentElement.classList.add("js");var vt=Si(),Ur=Za(),hr=ts(Ur),hc=Xa(),ze=cs(),$o=Ir("(min-width: 60em)"),vc=Ir("(min-width: 76.25em)"),bc=rs(),Mn=Ue(),gc=Le(".md-search")?$p():Ke,Po=new I;oc({alert$:Po});nc({document$:vt});var Io=new I,_c=dr(Mn.base);X("navigation.instant")&&cc({sitemap$:_c,location$:Ur,viewport$:ze,progress$:Io}).subscribe(vt);var dc;((dc=Mn.version)==null?void 0:dc.provider)==="mike"&&uc({document$:vt});R(Ur,hr).pipe(It(125)).subscribe(()=>{Eo("drawer",!1),Eo("search",!1)});hc.pipe(L(({mode:e,meta:t})=>e==="global"&&!t)).subscribe(e=>{switch(e.type){case",":case"p":let t=document.querySelector("link[rel=prev]");t instanceof HTMLLinkElement&&dt(t);break;case".":case"n":let r=document.querySelector("link[rel=next]");r instanceof HTMLLinkElement&&dt(r);break;case"/":let n=document.querySelector("[data-md-component=search] button");n instanceof HTMLButtonElement&&n.click();break;case"Enter":let o=xt();o instanceof HTMLLabelElement&&o.click()}});pc({viewport$:ze,document$:vt});fc({document$:vt,tablet$:$o});mc({document$:vt});var Lt=Us(ht("header"),{viewport$:ze}),Fr=vt.pipe(f(()=>ht("main")),g(e=>Ws(e,{viewport$:ze,header$:Lt})),se(1)),Pp=R(...Ee("consent").map(e=>us(e,{target$:hr})),...Ee("dialog").map(e=>Fs(e,{alert$:Po})),...Ee("palette").map(e=>Vs(e)),...Ee("progress").map(e=>zs(e,{progress$:Io})),...Ee("search").map(e=>Ks(e,{index$:gc})),...Ee("source").map(e=>Zs(e))),Ip=j(()=>R(...Ee("announce").map(e=>ls(e)),...Ee("content").map(e=>js(e,{sitemap$:_c,viewport$:ze,target$:hr,print$:bc})),...Ee("content").map(e=>X("search.highlight")?Ys(e,{index$:gc,location$:Ur}):y),...Ee("header").map(e=>Ns(e,{viewport$:ze,header$:Lt,main$:Fr})),...Ee("header-title").map(e=>Ds(e,{viewport$:ze,header$:Lt})),...Ee("sidebar").map(e=>e.getAttribute("data-md-type")==="navigation"?yo(vc,()=>Ao(e,{viewport$:ze,header$:Lt,main$:Fr})):yo($o,()=>Ao(e,{viewport$:ze,header$:Lt,main$:Fr}))),...Ee("tabs").map(e=>Qs(e,{viewport$:ze,header$:Lt})),...Ee("toc").map(e=>ec(e,{viewport$:ze,header$:Lt,main$:Fr,target$:hr})),...Ee("top").map(e=>tc(e,{viewport$:ze,header$:Lt,main$:Fr,target$:hr})))),yc=vt.pipe(g(()=>Ip),Rt(Pp),se(1));yc.subscribe();window.document$=vt;window.location$=Ur;window.target$=hr;window.keyboard$=hc;window.viewport$=ze;window.tablet$=$o;window.screen$=vc;window.print$=bc;window.alert$=Po;window.progress$=Io;window.component$=yc;})(); -/*! update cache: 20260410225948 */ +/*! update cache: 20260414173637 */ diff --git a/ru/chapter_appendix/contribution/index.html b/ru/chapter_appendix/contribution/index.html index 73ecad75d..318743e0e 100644 --- a/ru/chapter_appendix/contribution/index.html +++ b/ru/chapter_appendix/contribution/index.html @@ -4387,11 +4387,11 @@

В этой же открытой книге цикл обновления содержания сокращается до нескольких дней, а иногда даже до нескольких часов.

1.   Небольшие правки содержания

-

Как показано на рисунке 16-3, в правом верхнем углу каждой страницы есть "значок редактирования". Текст или код можно изменить следующим образом.

+

Как показано на рисунке 16-3, в правом верхнем углу каждой страницы есть «значок редактирования». Текст или код можно изменить следующим образом.

    -
  1. Нажмите на "значок редактирования". Если появится сообщение "You need to fork this repository", согласитесь с этим действием.
  2. +
  3. Нажмите на «значок редактирования». Если появится сообщение «You need to fork this repository», согласитесь с этим действием.
  4. Измените содержимое исходного Markdown-файла, проверьте корректность правок и постарайтесь сохранить единый стиль оформления.
  5. -
  6. Внизу страницы заполните описание изменений, затем нажмите кнопку "Propose file change". После перехода на следующую страницу нажмите кнопку "Create pull request", чтобы отправить pull request.
  7. +
  8. Внизу страницы заполните описание изменений, затем нажмите кнопку «Propose file change». После перехода на следующую страницу нажмите кнопку «Create pull request», чтобы отправить pull request.

Кнопка редактирования страницы

Рисунок 16-3   Кнопка редактирования страницы

@@ -4404,7 +4404,7 @@
  • Перейдите на страницу своего Fork-репозитория и с помощью команды git clone клонируйте репозиторий локально.
  • Создавайте и редактируйте содержание локально, затем проведите полное тестирование и проверьте корректность кода.
  • Зафиксируйте локальные изменения, после чего выполните Push в удаленный репозиторий.
  • -
  • Обновите страницу репозитория и нажмите кнопку "Create pull request", чтобы инициировать pull request.
  • +
  • Обновите страницу репозитория и нажмите кнопку «Create pull request», чтобы инициировать pull request.
  • 3.   Развертывание Docker

    В корневом каталоге hello-algo выполните следующий Docker-скрипт, после чего проект станет доступен по адресу http://localhost:8000:

    diff --git a/ru/chapter_appendix/installation/index.html b/ru/chapter_appendix/installation/index.html index 5a02877e2..65355b48c 100644 --- a/ru/chapter_appendix/installation/index.html +++ b/ru/chapter_appendix/installation/index.html @@ -4594,7 +4594,7 @@

    Загрузка VS Code с официального сайта

    Рисунок 16-1   Загрузка VS Code с официального сайта

    -

    VS Code обладает мощной экосистемой расширений и поддерживает выполнение и отладку большинства языков программирования. Например, после установки расширения "Python Extension Pack" можно отлаживать код на Python. Процесс установки показан на рисунке 16-2.

    +

    VS Code обладает мощной экосистемой расширений и поддерживает выполнение и отладку большинства языков программирования. Например, после установки расширения «Python Extension Pack» можно отлаживать код на Python. Процесс установки показан на рисунке 16-2.

    Установка расширений VS Code

    Рисунок 16-2   Установка расширений VS Code

    @@ -4607,7 +4607,7 @@

    2.   Среда C/C++

      -
    1. В Windows требуется установить MinGW (руководство по настройке); в macOS компилятор Clang уже установлен по умолчанию.
    2. +
    3. В Windows требуется установить MinGW (руководство по настройке). В macOS компилятор Clang уже установлен по умолчанию.
    4. В магазине расширений VS Code найдите c++ и установите C/C++ Extension Pack.
    5. (Необязательно) Откройте страницу Settings, найдите параметр форматирования Clang_format_fallback Style и задайте значение { BasedOnStyle: Microsoft, BreakBeforeBraces: Attach }.
    diff --git a/ru/chapter_array_and_linkedlist/array/index.html b/ru/chapter_array_and_linkedlist/array/index.html index 87d21c4d3..999496b83 100644 --- a/ru/chapter_array_and_linkedlist/array/index.html +++ b/ru/chapter_array_and_linkedlist/array/index.html @@ -4651,11 +4651,11 @@

    https://pythontutor.com/render.html#code=%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D0%BC%D0%B0%D1%81%D1%81%D0%B8%D0%B2%0Aarr%20%3D%20%5B0%5D%20%2A%205%20%20%23%20%5B%200%2C%200%2C%200%2C%200%2C%200%20%5D%0Anums%20%3D%20%5B1%2C%203%2C%202%2C%205%2C%204%5D&cumulative=false&curInstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

    2.   Доступ к элементам

    -

    Элементы массива хранятся в непрерывной области памяти, что упрощает вычисление их адресов. Зная адрес массива в памяти (то есть адрес первого элемента) и индекс некоторого элемента, мы можем по формуле с рисунка ниже вычислить адрес этого элемента и напрямую обратиться к нему.

    +

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

    Вычисление адреса элемента массива

    Рисунок 4-2   Вычисление адреса элемента массива

    -

    Если посмотреть на рисунок 4-2, можно заметить, что индекс первого элемента массива равен \(0\) , и это кажется не слишком интуитивным, ведь естественнее было бы начинать счет с \(1\) . Однако с точки зрения формулы адресации индекс по сути является смещением относительно адреса памяти. Смещение первого элемента равно \(0\) , поэтому индекс \(0\) полностью логичен.

    +

    Как видно на рисунке 4-2, индекс первого элемента массива равен \(0\) , и это кажется не слишком интуитивным, ведь естественнее было бы начинать счет с \(1\) . Однако с точки зрения формулы адресации индекс по сути является смещением относительно адреса памяти. Смещение первого элемента равно \(0\) , поэтому индекс \(0\) полностью логичен.

    Доступ к элементам массива очень эффективен: любой элемент массива можно получить за \(O(1)\) времени.

    @@ -4814,7 +4814,7 @@

    Пример вставки элемента в массив

    Рисунок 4-3   Пример вставки элемента в массив

    -

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

    +

    Стоит отметить, что длина массива фиксирована, поэтому вставка нового элемента неизбежно приведет к потере элемента на конце массива. Решение этой проблемы мы оставим для обсуждения в разделе о «списках».

    @@ -5127,7 +5127,7 @@
    • Высокая временная сложность: средняя временная сложность и вставки, и удаления равна \(O(n)\) , где \(n\) - длина массива.
    • Потеря элементов: поскольку длина массива неизменяема, после вставки элементы, выходящие за пределы длины массива, будут потеряны.
    • -
    • Потери памяти: можно заранее инициализировать более длинный массив и использовать только его переднюю часть; тогда теряемые при вставке элементы на конце не будут нести смысла, но такой подход приводит к лишнему расходу памяти.
    • +
    • Потери памяти: можно заранее инициализировать более длинный массив и использовать только его переднюю часть. Тогда теряемые при вставке элементы на конце не будут нести смысла, но такой подход приводит к лишнему расходу памяти.

    5.   Обход массива

    В большинстве языков программирования массив можно обходить как по индексу, так и напрямую перебирая каждый элемент:

    @@ -5347,7 +5347,7 @@

    6.   Поиск элемента

    -

    Чтобы найти заданный элемент в массиве, нужно пройти по массиву и на каждой итерации проверять, совпадает ли значение; если совпадает, вернуть соответствующий индекс.

    +

    Чтобы найти заданный элемент в массиве, нужно пройти по массиву и на каждой итерации проверять, совпадает ли значение. Если совпадает, вернуть соответствующий индекс.

    Поскольку массив - это линейная структура данных, такая операция поиска называется линейным поиском.

    @@ -5718,7 +5718,7 @@

    Непрерывное хранение данных - это палка о двух концах, и у него есть следующие ограничения.

    • Низкая эффективность вставки и удаления: когда элементов в массиве много, вставка и удаление требуют сдвига большого количества элементов.
    • -
    • Неизменяемая длина: после инициализации длина массива фиксирована; расширение массива требует копирования всех данных в новый массив, что стоит дорого.
    • +
    • Неизменяемая длина: после инициализации длина массива фиксирована. Расширение массива требует копирования всех данных в новый массив, что стоит дорого.
    • Потери памяти: если выделенный массив больше, чем реально необходимо, лишнее пространство пропадает впустую.

    4.1.3   Типичные применения массива

    diff --git a/ru/chapter_array_and_linkedlist/linked_list/index.html b/ru/chapter_array_and_linkedlist/linked_list/index.html index 95ea676f3..012de6f43 100644 --- a/ru/chapter_array_and_linkedlist/linked_list/index.html +++ b/ru/chapter_array_and_linkedlist/linked_list/index.html @@ -4890,9 +4890,9 @@ Визуализация выполнения

    https://pythontutor.com/render.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%D1%81%D0%B2%D1%8F%D0%B7%D0%BD%D1%8B%D0%B9%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%D1%83%D0%B7%D0%B5%D0%BB%D0%BA%D0%BB%D0%B0%D1%81%D1%81%22%22%22%0A%20%20%20%20def%20__init__%28self%2C%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%D0%97%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%D1%83%D0%B7%D0%BB%D0%B0%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%D0%A1%D1%81%D1%8B%D0%BB%D0%BA%D0%B0%20%D0%BD%D0%B0%20%D1%81%D0%BB%D0%B5%D0%B4%D1%83%D1%8E%D1%89%D0%B8%D0%B9%20%D1%83%D0%B7%D0%B5%D0%BB%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%81%D0%B2%D1%8F%D0%B7%D0%BD%D1%8B%D0%B9%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%201%20-%3E%203%20-%3E%202%20-%3E%205%20-%3E%204%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D0%BA%D0%B0%D0%B6%D0%B4%D1%8B%D0%B9%20%D1%83%D0%B7%D0%B5%D0%BB%0A%20%20%20%20n0%20%3D%20ListNode%281%29%0A%20%20%20%20n1%20%3D%20ListNode%283%29%0A%20%20%20%20n2%20%3D%20ListNode%282%29%0A%20%20%20%20n3%20%3D%20ListNode%285%29%0A%20%20%20%20n4%20%3D%20ListNode%284%29%0A%20%20%20%20%23%20%D0%9F%D0%BE%D1%81%D1%82%D1%80%D0%BE%D0%B8%D1%82%D1%8C%20%D1%81%D1%81%D1%8B%D0%BB%D0%BA%D0%B8%20%D0%BC%D0%B5%D0%B6%D0%B4%D1%83%20%D1%83%D0%B7%D0%BB%D0%B0%D0%BC%D0%B8%0A%20%20%20%20n0.next%20%3D%20n1%0A%20%20%20%20n1.next%20%3D%20n2%0A%20%20%20%20n2.next%20%3D%20n3%0A%20%20%20%20n3.next%20%3D%20n4&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

    -

    Массив в целом - это одна переменная: например, массив nums содержит элементы nums[0] , nums[1] и т.д. Связный список же состоит из множества независимых объектов-узлов. Обычно в качестве обозначения всего связного списка используют головной узел; например, в приведенном выше коде связный список можно обозначить как n0 .

    +

    Массив в целом - это одна переменная: например, массив nums содержит элементы nums[0] , nums[1] и т.д. Связный список же состоит из множества независимых объектов-узлов. Обычно в качестве обозначения всего связного списка используют головной узел. Например, в приведенном выше коде связный список можно обозначить как n0 .

    2.   Вставка узла

    -

    Вставить узел в связный список очень легко. Как показано на рисунке 4-6, предположим, что мы хотим вставить новый узел P между двумя соседними узлами n0 и n1 ; для этого нужно изменить всего две ссылки (указателя), а временная сложность будет равна \(O(1)\) .

    +

    Вставить узел в связный список очень легко. Как показано на рисунке 4-6, предположим, что мы хотим вставить новый узел P между двумя соседними узлами n0 и n1. Для этого нужно изменить всего две ссылки (указателя), а временная сложность будет равна \(O(1)\) .

    Для сравнения: временная сложность вставки элемента в массив составляет \(O(n)\) , и при большом объеме данных это менее эффективно.

    Пример вставки узла в связный список

    Рисунок 4-6   Пример вставки узла в связный список

    @@ -5819,14 +5819,14 @@

    4.2.4   Типичные применения связных списков

    Односвязные списки обычно используются для реализации стеков, очередей, хеш-таблиц и графов.

      -
    • Стеки и очереди: если операции вставки и удаления выполняются на одном конце связного списка, он проявляет свойства LIFO, соответствующие стеку; если вставка происходит на одном конце, а удаление на другом, он проявляет свойства FIFO, соответствующие очереди.
    • +
    • Стеки и очереди: если операции вставки и удаления выполняются на одном конце связного списка, он проявляет свойства LIFO, соответствующие стеку. Если вставка происходит на одном конце, а удаление на другом, он проявляет свойства FIFO, соответствующие очереди.
    • Хеш-таблицы: метод цепочек - один из основных способов разрешения коллизий в хеш-таблицах. В этом подходе все конфликтующие элементы помещаются в связный список.
    • Графы: список смежности - это распространенный способ представления графа, при котором каждой вершине графа соответствует связный список, а каждый элемент этого списка представляет другую вершину, соединенную с данной.

    Двусвязные списки обычно используются там, где нужен быстрый доступ как к предыдущему, так и к следующему элементу.

      -
    • Продвинутые структуры данных: например, в красно-черных деревьях и B-деревьях нам нужен доступ к родительскому узлу; этого можно добиться, сохранив в узле ссылку на родителя, по аналогии с двусвязным списком.
    • -
    • История браузера: когда пользователь в браузере нажимает кнопки "вперед" или "назад", браузеру нужно знать предыдущую и следующую посещенные страницы. Свойства двусвязного списка делают такую операцию простой.
    • +
    • Продвинутые структуры данных: например, в красно-черных деревьях и B-деревьях нам нужен доступ к родительскому узлу. Этого можно добиться, сохранив в узле ссылку на родителя, по аналогии с двусвязным списком.
    • +
    • История браузера: когда пользователь в браузере нажимает кнопки «вперед» или «назад», браузеру нужно знать предыдущую и следующую посещенные страницы. Свойства двусвязного списка делают такую операцию простой.
    • Алгоритм LRU: в алгоритмах вытеснения из кэша (LRU) нужно быстро находить наименее недавно использованные данные, а также быстро добавлять и удалять узлы. Для этого двусвязный список подходит очень хорошо.

    Циклические списки часто применяются в сценариях, требующих циклических операций, например при планировании ресурсов в операционной системе.

    diff --git a/ru/chapter_array_and_linkedlist/list/index.html b/ru/chapter_array_and_linkedlist/list/index.html index d5f1bc0c0..a9b22d868 100644 --- a/ru/chapter_array_and_linkedlist/list/index.html +++ b/ru/chapter_array_and_linkedlist/list/index.html @@ -4506,9 +4506,9 @@
  • Связный список естественным образом можно рассматривать как список: он поддерживает операции добавления, удаления, поиска и изменения элементов и может гибко расширяться динамически.
  • Массив тоже поддерживает операции добавления, удаления, поиска и изменения элементов, но из-за неизменяемости длины его можно считать лишь списком с ограниченной длиной.
  • -

    Когда список реализуется с помощью массива, неизменяемость длины снижает его практическую полезность. Причина в том, что мы обычно не можем заранее точно знать, сколько данных нужно хранить, а значит, трудно выбрать подходящую длину списка. Если длина слишком мала, она может не покрыть реальные потребности; если слишком велика, будет зря расходоваться память.

    +

    Когда список реализуется с помощью массива, неизменяемость длины снижает его практическую полезность. Причина в том, что мы обычно не можем заранее точно знать, сколько данных нужно хранить, а значит, трудно выбрать подходящую длину списка. Если длина слишком мала, она может не покрыть реальные потребности. Если слишком велика, будет зря расходоваться память.

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

    -

    На практике списки из стандартных библиотек многих языков программирования реализованы именно на основе динамических массивов, например list в Python, ArrayList в Java, vector в C++ и List в C#. В дальнейшем обсуждении мы будем считать понятия "список" и "динамический массив" эквивалентными.

    +

    На практике списки из стандартных библиотек многих языков программирования реализованы именно на основе динамических массивов, например list в Python, ArrayList в Java, vector в C++ и List в C#. В дальнейшем обсуждении мы будем считать понятия «список» и «динамический массив» эквивалентными.

    4.3.1   Основные операции со списком

    1.   Инициализация списка

    Обычно используются два способа инициализации: без начальных значений и с начальными значениями:

    @@ -5226,7 +5226,7 @@

    https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%202%2C%205%2C%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%9E%D0%B1%D1%8A%D0%B5%D0%B4%D0%B8%D0%BD%D0%B8%D1%82%D1%8C%20%D0%B4%D0%B2%D0%B0%20%D1%81%D0%BF%D0%B8%D1%81%D0%BA%D0%B0%0A%20%20%20%20nums1%20%3D%20%5B6%2C%208%2C%207%2C%2010%2C%209%5D%0A%20%20%20%20nums%20%2B%3D%20nums1%20%20%23%20%D0%9F%D1%80%D0%B8%D1%81%D0%BE%D0%B5%D0%B4%D0%B8%D0%BD%D0%B8%D1%82%D1%8C%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%20nums1%20%D0%BA%20nums&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

    6.   Сортировка списка

    -

    После сортировки списка мы сможем применять алгоритмы "двоичный поиск" и "два указателя", которые очень часто встречаются в задачах по массивам.

    +

    После сортировки списка мы сможем применять алгоритмы «двоичный поиск» и «два указателя», которые очень часто встречаются в задачах по массивам.

    diff --git a/ru/chapter_array_and_linkedlist/ram_and_cache/index.html b/ru/chapter_array_and_linkedlist/ram_and_cache/index.html index 0c85f7cd1..a8e936723 100644 --- a/ru/chapter_array_and_linkedlist/ram_and_cache/index.html +++ b/ru/chapter_array_and_linkedlist/ram_and_cache/index.html @@ -4452,7 +4452,7 @@

    С другой стороны, во время выполнения программы при многократном выделении и освобождении памяти фрагментация свободной памяти становится все более серьезной, что снижает эффективность ее использования. Массивы из-за непрерывного хранения относительно менее подвержены фрагментации. Напротив, элементы связного списка распределены по памяти, и частые операции вставки и удаления легче приводят к фрагментации.

    4.4.3   Эффективность использования кэша структурами данных

    Хотя по объему кэш намного меньше оперативной памяти, он значительно быстрее и играет критически важную роль в скорости выполнения программ. Поскольку объем кэша ограничен и в нем можно хранить только небольшую долю часто используемых данных, когда CPU пытается обратиться к данным, которых в кэше нет, происходит промах кэша (cache miss) , и CPU вынужден загружать нужные данные из более медленной памяти.

    -

    Очевидно, что чем меньше промахов кэша, тем выше эффективность чтения и записи данных CPU, а значит, тем лучше производительность программы. Долю обращений, при которых CPU успешно получает данные из кэша, называют коэффициентом попадания в кэш (cache hit rate) ; этот показатель обычно используют для оценки эффективности кэша.

    +

    Очевидно, что чем меньше промахов кэша, тем выше эффективность чтения и записи данных CPU, а значит, тем лучше производительность программы. Долю обращений, при которых CPU успешно получает данные из кэша, называют коэффициентом попадания в кэш (cache hit rate). Этот показатель обычно используют для оценки эффективности кэша.

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

    • Строки кэша: кэш хранит и загружает данные не по одному байту, а строками кэша. По сравнению с передачей по байтам это гораздо эффективнее.
    • @@ -4468,7 +4468,7 @@
    • Пространственная локальность: массив хранится в компактной области памяти, поэтому данные рядом с уже загруженными с большей вероятностью скоро будут использованы.

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

    -

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

    +

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

    • При решении алгоритмических задач мы обычно предпочитаем стек на основе массива, потому что он дает более высокую эффективность операций и поддерживает произвольный доступ, а цена за это - необходимость заранее выделить некоторый объем памяти под массив.
    • Если объем данных очень велик, структура сильно динамична, а ожидаемый размер стека трудно оценить заранее, то более уместен стек на основе связного списка. Список позволяет распределить большой объем данных по разным участкам памяти и избегает накладных расходов, связанных с расширением массива.
    • diff --git a/ru/chapter_array_and_linkedlist/summary/index.html b/ru/chapter_array_and_linkedlist/summary/index.html index d3deb4c78..43a3df808 100644 --- a/ru/chapter_array_and_linkedlist/summary/index.html +++ b/ru/chapter_array_and_linkedlist/summary/index.html @@ -4360,8 +4360,8 @@

      1.   Ключевые выводы

      • Массивы и связные списки - это две базовые структуры данных, представляющие два способа хранения данных в памяти компьютера: хранение в непрерывном пространстве и хранение в разрозненном пространстве. Их свойства во многом взаимно дополняют друг друга.
      • -
      • Массив поддерживает произвольный доступ и занимает меньше памяти; однако вставка и удаление элементов в нем неэффективны, а длина после инициализации фиксирована.
      • -
      • Связный список позволяет эффективно вставлять и удалять узлы путем изменения ссылок (указателей), а также гибко менять длину; однако доступ к узлам менее эффективен, а памяти он занимает больше. Распространенные типы списков включают односвязные, циклические и двусвязные списки.
      • +
      • Массив поддерживает произвольный доступ и занимает меньше памяти. Однако вставка и удаление элементов в нем неэффективны, а длина после инициализации фиксирована.
      • +
      • Связный список позволяет эффективно вставлять и удалять узлы путем изменения ссылок (указателей), а также гибко менять длину. Однако доступ к узлам менее эффективен, а памяти он занимает больше. Распространенные типы списков включают односвязные, циклические и двусвязные списки.
      • Список - это упорядоченная коллекция элементов, поддерживающая добавление, удаление, поиск и изменение, и обычно реализуемая на основе динамического массива. Он сохраняет преимущества массива и при этом может гибко менять длину.
      • Появление списка значительно повысило практическую ценность массива, хотя это и может приводить к потере части памяти.
      • Во время работы программы данные в основном хранятся в оперативной памяти. Массив обеспечивает более высокую эффективность использования пространства памяти, а связный список дает большую гибкость в использовании памяти.
      • @@ -4372,13 +4372,13 @@

        Q: Влияет ли хранение массива в стеке или в куче на временную и пространственную эффективность?

        Массивы, расположенные и в стеке, и в куче, все равно хранятся в непрерывной области памяти, поэтому эффективность операций с данными у них в целом одинакова. Однако у стека и кучи есть собственные особенности, из-за которых возникают следующие различия.

          -
        1. Эффективность выделения и освобождения: стек представляет собой относительно небольшой участок памяти, а выделение в нем обычно выполняется автоматически компилятором; куча же обычно больше, может выделяться динамически из кода и легче фрагментируется. Поэтому выделение и освобождение памяти в куче обычно медленнее, чем в стеке.
        2. +
        3. Эффективность выделения и освобождения: стек представляет собой относительно небольшой участок памяти, а выделение в нем обычно выполняется автоматически компилятором. Куча же обычно больше, может выделяться динамически из кода и легче фрагментируется. Поэтому выделение и освобождение памяти в куче обычно медленнее, чем в стеке.
        4. Ограничение размера: объем стека относительно невелик, а размер кучи обычно ограничивается доступной памятью. Поэтому куча лучше подходит для хранения больших массивов.
        5. Гибкость: размер массива в стеке должен быть известен во время компиляции, а размер массива в куче может определяться динамически во время выполнения.

        Q: Почему для массива требуется, чтобы все элементы были одного типа, а для связного списка это не подчеркивается?

        Связный список состоит из узлов, а узлы соединяются между собой через ссылки (указатели), поэтому каждый узел в принципе может хранить данные разного типа, например int , double , string , object и т.д.

        -

        Напротив, элементы массива должны быть одного типа, иначе нельзя будет вычислять адрес элемента через смещение. Например, если массив одновременно содержит int и long , один элемент занимает 4 байта, а другой - 8 байт ; в этом случае формула ниже уже не позволит вычислить смещение, потому что в массиве будут присутствовать элементы разной длины.

        +

        Напротив, элементы массива должны быть одного типа, иначе нельзя будет вычислять адрес элемента через смещение. Например, если массив одновременно содержит int и long , один элемент занимает 4 байта, а другой - 8 байт. В этом случае формула ниже уже не позволит вычислить смещение, потому что в массиве будут присутствовать элементы разной длины.

        # Адрес элемента в памяти = адрес массива в памяти (адрес первого элемента) + длина элемента * индекс элемента
         

        Q: После удаления узла P нужно ли присваивать P.next = None ?

        @@ -4386,16 +4386,16 @@

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

        Q: Временная сложность вставки и удаления в связном списке равна \(O(1)\) . Но до вставки или удаления обычно еще нужно потратить \(O(n)\) на поиск элемента. Почему тогда общая сложность не \(O(n)\) ?

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

        -

        Q: На рисунке "Определение связного списка и способ хранения" светло-голубой блок с указателем узла - это отдельный адрес памяти? Или он делит память пополам со значением узла?

        -

        Этот рисунок дает только качественное представление; количественно все зависит от конкретных условий.

        +

        Q: На рисунке «Определение связного списка и способ хранения» светло-голубой блок с указателем узла - это отдельный адрес памяти? Или он делит память пополам со значением узла?

        +

        Этот рисунок дает только качественное представление. Количественно все зависит от конкретных условий.

        • Значения узлов разных типов занимают разный объем памяти, например int , long , double и объекты-экземпляры.
        • Размер памяти, занимаемой переменной-указателем, зависит от операционной системы и среды компиляции и обычно составляет 8 байт или 4 байта.

        Q: Всегда ли добавление элемента в конец списка имеет сложность \(O(1)\) ?

        Если при добавлении элемента длина списка превышается, то сначала приходится расширять список, а уже затем добавлять новый элемент. Система выделяет новый участок памяти и переносит туда все элементы исходного списка, и в этот момент временная сложность становится \(O(n)\) .

        -

        Q: В утверждении "появление списка сильно повысило практическую полезность массива, но может приводить к потере части памяти" под потерями памяти имеется в виду дополнительная память под такие переменные, как емкость, длина и коэффициент расширения?

        -

        Потери памяти здесь в основном имеют два значения: во-первых, список обычно имеет некоторую начальную емкость, которая может быть нам не нужна целиком; во-вторых, чтобы избежать слишком частых расширений, емкость при расширении обычно умножается на некоторый коэффициент, например \(\times 1.5\) . Из-за этого появляется много пустых слотов, которые обычно нельзя полностью заполнить.

        +

        Q: В утверждении «появление списка сильно повысило практическую полезность массива, но может приводить к потере части памяти» под потерями памяти имеется в виду дополнительная память под такие переменные, как емкость, длина и коэффициент расширения?

        +

        Потери памяти здесь в основном имеют два значения: во-первых, список обычно имеет некоторую начальную емкость, которая может быть нам не нужна целиком. Во-вторых, чтобы избежать слишком частых расширений, емкость при расширении обычно умножается на некоторый коэффициент, например \(\times 1.5\) . Из-за этого появляется много пустых слотов, которые обычно нельзя полностью заполнить.

        Q: В Python после инициализации n = [1, 2, 3] адреса этих трех элементов выглядят непрерывными, но после m = [2, 1, 3] можно заметить, что id элементов не идут подряд, а совпадают с одинаковыми числами из n . Если адреса элементов не непрерывны, остается ли m массивом?

        Предположим, что элементами списка являются узлы n = [n1, n2, n3, n4, n5] . Обычно эти 5 объектов-узлов тоже будут храниться в разных местах памяти. Однако, имея индекс списка, мы по-прежнему можем за \(O(1)\) получить адрес памяти соответствующего узла и обратиться к нему. Это связано с тем, что в массиве хранятся ссылки на узлы, а не сами узлы.

        В отличие от многих других языков, в Python даже числа обернуты в объекты, и в списке хранятся не сами числа, а ссылки на них. Поэтому мы и наблюдаем, что одинаковые числа в двух массивах имеют один и тот же id , а адреса этих чисел не обязаны быть непрерывными.

        @@ -4411,8 +4411,8 @@

        Если нужно, чтобы каждый [0] был независимым, можно использовать res = [[0] for _ in range(n)] . В этом варианте создаются \(n\) независимых объектов-списков [0] .

        Q: Операция res = [0] * n создает список. Каждый целочисленный 0 в нем независим?

        В этом списке все целые числа 0 являются ссылками на один и тот же объект. Это связано с тем, что Python использует механизм кэш-пула для маленьких целых чисел (обычно от -5 до 256), чтобы максимально переиспользовать объекты и повысить производительность.

        -

        Хотя все элементы указывают на один и тот же объект, мы все равно можем независимо изменять элементы списка, потому что целые числа в Python - это "неизменяемые объекты". Когда мы изменяем некоторый элемент, на самом деле происходит переключение ссылки на другой объект, а не изменение исходного объекта.

        -

        Однако если элементами списка являются "изменяемые объекты" (например списки, словари или экземпляры классов), то изменение одного элемента прямо меняет сам объект, и все элементы, ссылающиеся на него, увидят одно и то же изменение.

        +

        Хотя все элементы указывают на один и тот же объект, мы все равно можем независимо изменять элементы списка, потому что целые числа в Python - это «неизменяемые объекты». Когда мы изменяем некоторый элемент, на самом деле происходит переключение ссылки на другой объект, а не изменение исходного объекта.

        +

        Однако если элементами списка являются «изменяемые объекты» (например списки, словари или экземпляры классов), то изменение одного элемента прямо меняет сам объект, и все элементы, ссылающиеся на него, увидят одно и то же изменение.

        diff --git a/ru/chapter_backtracking/backtracking_algorithm/index.html b/ru/chapter_backtracking/backtracking_algorithm/index.html index 471af24e3..de103f342 100644 --- a/ru/chapter_backtracking/backtracking_algorithm/index.html +++ b/ru/chapter_backtracking/backtracking_algorithm/index.html @@ -4446,12 +4446,12 @@

        13.1   Алгоритм поиска с возвратом

        Алгоритм поиска с возвратом (backtracking algorithm) - это метод решения задач путем полного перебора. Его основная идея состоит в том, чтобы, начиная с некоторого исходного состояния, грубо перебрать все возможные решения, записывать корректные решения и продолжать поиск до тех пор, пока решение не будет найдено или пока не будут исчерпаны все возможные варианты.

        -

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

        +

        Обычно алгоритмы поиска с возвратом используют обход в глубину для обхода пространства решений. В главе «Бинарные деревья» мы уже упоминали, что прямой, симметричный и обратный обходы относятся к обходу в глубину. Теперь мы на основе прямого обхода построим задачу поиска с возвратом и постепенно разберем принцип работы этого алгоритма.

        Пример 1

        -

        Дано двоичное дерево. Найдите и запишите все узлы со значением \(7\) ; верните список этих узлов.

        +

        Дано двоичное дерево. Найдите и запишите все узлы со значением \(7\). Верните список этих узлов.

        -

        Для этой задачи мы выполняем прямой обход дерева и проверяем, равно ли значение текущего узла \(7\) ; если да, то добавляем значение этого узла в список результатов res . Соответствующий процесс показан на рисунке 13-1 и в коде:

        +

        Для этой задачи мы выполняем прямой обход дерева и проверяем, равно ли значение текущего узла \(7\). Если да, то добавляем значение этого узла в список результатов res . Соответствующий процесс показан на рисунке 13-1 и в коде:

        @@ -4657,8 +4657,8 @@

        Рисунок 13-1   Поиск узлов при прямом обходе

        13.1.1   Попытка и откат

        -

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

        -

        Для примера 1 посещение каждого узла представляет собой "попытку", а прохождение листового узла или возврат к родителю через return означает "откат".

        +

        Алгоритм называется поиском с возвратом, потому что при поиске в пространстве решений он использует стратегию «попытка» и «откат». Когда в процессе поиска алгоритм приходит в состояние, из которого нельзя двигаться дальше или нельзя получить удовлетворяющее условиям решение, он отменяет предыдущий выбор, возвращается к более раннему состоянию и пробует другие возможные варианты.

        +

        Для примера 1 посещение каждого узла представляет собой «попытку», а прохождение листового узла или возврат к родителю через return означает «откат».

        Важно понимать, что откат не сводится только к возврату из функции. Чтобы показать это, слегка расширим пример 1.

        Пример 2

        @@ -4936,8 +4936,8 @@

        -

        В каждой "попытке" мы добавляем текущий узел в path , чтобы записать путь; а перед "откатом" нам нужно удалить этот узел из path , чтобы восстановить состояние, существовавшее до текущей попытки.

        -

        Если посмотреть на процесс, изображенный на рисунке 13-2, то попытку и откат можно понимать как "движение вперед" и "отмену": это два взаимно противоположных действия.

        +

        В каждой «попытке» мы добавляем текущий узел в path , чтобы записать путь. А перед «откатом» нам нужно удалить этот узел из path , чтобы восстановить состояние, существовавшее до текущей попытки.

        +

        Если посмотреть на процесс, изображенный на рисунке 13-2, то попытку и откат можно понимать как «движение вперед» и «отмену»: это два взаимно противоположных действия.

        @@ -4978,7 +4978,7 @@

        Рисунок 13-2   Попытка и откат

        13.1.2   Обрезка

        -

        Сложные задачи поиска с возвратом обычно содержат одно или несколько ограничений, которые часто можно использовать для "обрезки".

        +

        Сложные задачи поиска с возвратом обычно содержат одно или несколько ограничений, которые часто можно использовать для «обрезки».

        Пример 3

        Найдите в двоичном дереве все узлы со значением \(7\) , верните пути от корня до этих узлов, причем путь не должен содержать узлы со значением \(3\).

        @@ -5267,12 +5267,12 @@

        -

        Термин "обрезка" очень нагляден. Как показано на рисунке 13-3, во время поиска мы отсекаем ветви, не удовлетворяющие ограничениям , тем самым избегая множества бессмысленных попыток и повышая эффективность поиска.

        +

        Термин «обрезка» очень нагляден. Как показано на рисунке 13-3, во время поиска мы отсекаем ветви, не удовлетворяющие ограничениям , тем самым избегая множества бессмысленных попыток и повышая эффективность поиска.

        Обрезка по условиям задачи

        Рисунок 13-3   Обрезка по условиям задачи

        13.1.3   Каркас кода

        -

        Теперь попробуем извлечь общий каркас из действий "попытка", "откат" и "обрезка", чтобы сделать код более универсальным.

        +

        Теперь попробуем извлечь общий каркас из действий «попытка», «откат» и «обрезка», чтобы сделать код более универсальным.

        В следующем каркасе кода state обозначает текущее состояние задачи, а choices - список выборов, доступных в текущем состоянии:

        @@ -6265,7 +6265,7 @@ Решение (solution) -Решение - это ответ, удовлетворяющий условиям задачи; решений может быть одно или несколько +Решение - это ответ, удовлетворяющий условиям задачи. Решений может быть одно или несколько Все пути от корня до узла \(7\) , удовлетворяющие ограничениям @@ -6298,7 +6298,7 @@

        Tip

        -

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

        +

        Такие понятия, как задача, решение и состояние, являются общими и встречаются не только в поиске с возвратом, но и в «разделяй и властвуй», динамическом программировании, жадных алгоритмах и других темах.

        13.1.5   Преимущества и ограничения

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

        @@ -6317,25 +6317,25 @@

        Поисковые задачи: целью таких задач является поиск решений, удовлетворяющих определенным условиям.

        • Задача о перестановках: дано множество, требуется найти все возможные перестановки его элементов.
        • -
        • Задача о сумме подмножеств: даны множество и целевая сумма; нужно найти все подмножества, сумма элементов которых равна целевой.
        • -
        • Задача о Ханойской башне: даны три стержня и набор дисков разного размера; требуется перенести все диски с одного стержня на другой, перемещая за раз только один диск и не помещая больший диск на меньший.
        • +
        • Задача о сумме подмножеств: даны множество и целевая сумма. Нужно найти все подмножества, сумма элементов которых равна целевой.
        • +
        • Задача о Ханойской башне: даны три стержня и набор дисков разного размера. Требуется перенести все диски с одного стержня на другой, перемещая за раз только один диск и не помещая больший диск на меньший.

        Задачи удовлетворения ограничений: целью таких задач является поиск решений, удовлетворяющих всем ограничениям.

        • Задача о \(n\) ферзях: разместить \(n\) ферзей на шахматной доске размера \(n \times n\) так, чтобы они не атаковали друг друга.
        • Судоку: заполнить сетку \(9 \times 9\) числами от \(1\) до \(9\) так, чтобы в каждой строке, каждом столбце и каждом блоке \(3 \times 3\) числа не повторялись.
        • -
        • Задача раскраски графа: дан неориентированный граф; требуется раскрасить его вершины минимальным числом цветов так, чтобы соседние вершины имели разные цвета.
        • +
        • Задача раскраски графа: дан неориентированный граф. Требуется раскрасить его вершины минимальным числом цветов так, чтобы соседние вершины имели разные цвета.

        Задачи комбинаторной оптимизации: целью таких задач является поиск оптимального решения в некотором комбинаторном пространстве при заданных ограничениях.

          -
        • Задача о рюкзаке 0-1: даны набор предметов и рюкзак; у каждого предмета есть ценность и вес, и нужно выбрать предметы так, чтобы при ограниченной вместимости рюкзака суммарная ценность была максимальной.
        • +
        • Задача о рюкзаке 0-1: даны набор предметов и рюкзак. У каждого предмета есть ценность и вес, и нужно выбрать предметы так, чтобы при ограниченной вместимости рюкзака суммарная ценность была максимальной.
        • Задача коммивояжера: начиная из некоторой вершины графа, требуется посетить все остальные вершины ровно по одному разу и вернуться в исходную вершину, найдя при этом кратчайший путь.
        • -
        • Задача о максимальной клике: дан неориентированный граф; требуется найти в нем максимальный полный подграф, то есть подграф, в котором любая пара вершин соединена ребром.
        • +
        • Задача о максимальной клике: дан неориентированный граф. Требуется найти в нем максимальный полный подграф, то есть подграф, в котором любая пара вершин соединена ребром.

        Стоит отметить: для многих задач комбинаторной оптимизации поиск с возвратом не является оптимальным способом решения.

        • Задача о рюкзаке 0-1 обычно решается с помощью динамического программирования, что дает более высокую временную эффективность.
        • -
        • Задача коммивояжера является известной NP-Hard задачей; для ее решения часто используют генетические алгоритмы, муравьиные алгоритмы и другие методы.
        • +
        • Задача коммивояжера является известной NP-Hard задачей. Для ее решения часто используют генетические алгоритмы, муравьиные алгоритмы и другие методы.
        • Задача о максимальной клике является классической задачей теории графов и может решаться жадными и другими эвристическими алгоритмами.
        diff --git a/ru/chapter_backtracking/n_queens_problem/index.html b/ru/chapter_backtracking/n_queens_problem/index.html index 048e5a125..0e1e79910 100644 --- a/ru/chapter_backtracking/n_queens_problem/index.html +++ b/ru/chapter_backtracking/n_queens_problem/index.html @@ -4381,7 +4381,7 @@

        13.4   Задача о n ферзях

        Question

        -

        Согласно правилам шахмат ферзь может атаковать фигуры, находящиеся с ним на одной строке, в одном столбце или на одной диагонали. Даны \(n\) ферзей и шахматная доска размера \(n \times n\) ; требуется найти такие расстановки, при которых ни одна пара ферзей не может атаковать друг друга.

        +

        Согласно правилам шахмат ферзь может атаковать фигуры, находящиеся с ним на одной строке, в одном столбце или на одной диагонали. Даны \(n\) ферзей и шахматная доска размера \(n \times n\). Требуется найти такие расстановки, при которых ни одна пара ферзей не может атаковать друг друга.

        Как показано на рисунке 13-15, при \(n = 4\) существует два решения. С точки зрения поиска с возвратом доска размера \(n \times n\) содержит \(n^2\) клеток, которые образуют все возможные выборы choices . По мере поочередного размещения ферзей состояние доски непрерывно меняется, и текущее содержимое доски образует состояние state .

        Решения задачи о 4 ферзях

        @@ -4394,7 +4394,7 @@

        1.   Построчная стратегия размещения

        Число ферзей и число строк доски одинаково и равно \(n\) , поэтому легко получить следующий вывод: в каждой строке доски разрешено и нужно разместить ровно одного ферзя.

        Иначе говоря, можно использовать построчную стратегию: начиная с первой строки, размещать по одному ферзю в каждой строке, пока не будет достигнута последняя.

        -

        На рисунке 13-17 показан процесс построчного размещения для задачи о 4 ферзях. Из-за ограничений размера изображения на нем раскрыта только одна ветвь поиска для первой строки, а все варианты, не удовлетворяющие ограничениям по столбцам и диагоналям, были отсечены.

        +

        На рисунке 13-17 показан процесс построчного размещения для задачи о 4 ферзях. Из-за ограничений размера изображения на рисунке 13-17 показана лишь одна ветвь поиска для первой строки, а все варианты, не удовлетворяющие ограничениям по столбцам и диагоналям, были отсечены.

        Построчная стратегия размещения

        Рисунок 13-17   Построчная стратегия размещения

        diff --git a/ru/chapter_backtracking/permutations_problem/index.html b/ru/chapter_backtracking/permutations_problem/index.html index 84bf67205..78bb832ec 100644 --- a/ru/chapter_backtracking/permutations_problem/index.html +++ b/ru/chapter_backtracking/permutations_problem/index.html @@ -4524,7 +4524,7 @@

        Question

        Дан массив целых чисел, в котором нет повторяющихся элементов. Верните все возможные перестановки.

        -

        С точки зрения поиска с возвратом процесс построения перестановок можно представить как результат последовательности выборов. Пусть входной массив равен \([1, 2, 3]\) ; если мы сначала выберем \(1\) , затем \(3\) , а потом \(2\) , то получим перестановку \([1, 3, 2]\) . Откат здесь означает отмену одного из выборов с последующей попыткой других вариантов.

        +

        С точки зрения поиска с возвратом процесс построения перестановок можно представить как результат последовательности выборов. Пусть входной массив равен \([1, 2, 3]\). Если мы сначала выберем \(1\) , затем \(3\) , а потом \(2\) , то получим перестановку \([1, 3, 2]\) . Откат здесь означает отмену одного из выборов с последующей попыткой других вариантов.

        С точки зрения кода поиска с возвратом множество кандидатов choices состоит из всех элементов входного массива, а состояние state - из элементов, уже выбранных к текущему моменту. Поскольку каждый элемент разрешено выбирать только один раз, все элементы в state должны быть уникальны.

        Как показано на рисунке 13-5, процесс поиска можно развернуть в дерево рекурсии, где каждый узел представляет текущее состояние state . Начиная от корня, после трех раундов выбора мы попадаем в листья, и каждый лист соответствует одной перестановке.

        Дерево рекурсии для перестановок

        @@ -4540,9 +4540,9 @@

        Пример обрезки в задаче о перестановках

        Рисунок 13-6   Пример обрезки в задаче о перестановках

        -

        Из рисунка видно, что такая обрезка уменьшает размер пространства поиска с \(O(n^n)\) до \(O(n!)\) .

        +

        Как видно на рисунке 13-6, такая обрезка уменьшает размер пространства поиска с \(O(n^n)\) до \(O(n!)\) .

        2.   Реализация кода

        -

        После прояснения всей логики можно просто "заполнить пропуски" в шаблоне поиска с возвратом. Чтобы сократить общий объем кода, мы не будем отдельно реализовывать каждую функцию из каркаса, а раскроем их прямо внутри backtrack() :

        +

        После прояснения всей логики можно просто «заполнить пропуски» в шаблоне поиска с возвратом. Чтобы сократить общий объем кода, мы не будем отдельно реализовывать каждую функцию из каркаса, а раскроем их прямо внутри backtrack() :

        @@ -5021,7 +5021,7 @@

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

        1.   Обрезка равных элементов

        -

        Посмотрите на рисунок 13-8: в первом раунде выбрать \(1\) или выбрать \(\hat{1}\) - это одно и то же, а значит, все перестановки, полученные из этих двух выборов, будут дублироваться. Поэтому ветвь \(\hat{1}\) нужно отсечь.

        +

        Как видно на рисунке 13-8, в первом раунде выбрать \(1\) или выбрать \(\hat{1}\) - это одно и то же, а значит, все перестановки, полученные из этих двух выборов, будут дублироваться. Поэтому ветвь \(\hat{1}\) нужно отсечь.

        Точно так же, если в первом раунде выбрать \(2\) , то во втором раунде выборы \(1\) и \(\hat{1}\) снова создадут дублирующиеся ветви, поэтому и в этом случае ветвь \(\hat{1}\) нужно отсечь.

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

        Обрезка повторяющихся перестановок

        @@ -5522,7 +5522,7 @@

        -

        Если предположить, что все элементы попарно различны, то из \(n\) элементов можно получить \(n!\) перестановок; при записи результата требуется копировать список длины \(n\) , что занимает \(O(n)\) времени. Следовательно, временная сложность равна \(O(n!n)\) .

        +

        Если предположить, что все элементы попарно различны, то из \(n\) элементов можно получить \(n!\) перестановок. При записи результата требуется копировать список длины \(n\) , что занимает \(O(n)\) времени. Следовательно, временная сложность равна \(O(n!n)\) .

        Максимальная глубина рекурсии равна \(n\) , что требует \(O(n)\) стековой памяти. Массив selected занимает \(O(n)\) пространства. Одновременно может существовать до \(n\) хеш-множеств duplicated , что дает \(O(n^2)\) памяти. Следовательно, пространственная сложность равна \(O(n^2)\) .

        3.   Сравнение двух видов обрезки

        Обратите внимание: хотя и selected , и duplicated используются для обрезки, их цели различаются.

        diff --git a/ru/chapter_backtracking/subset_sum_problem/index.html b/ru/chapter_backtracking/subset_sum_problem/index.html index e2b76944e..a3668096a 100644 --- a/ru/chapter_backtracking/subset_sum_problem/index.html +++ b/ru/chapter_backtracking/subset_sum_problem/index.html @@ -4494,7 +4494,7 @@

        13.3.1   Случай без повторяющихся элементов

        Question

        -

        Дан массив положительных целых чисел nums и целое положительное значение target . Найдите все возможные комбинации, сумма элементов которых равна target . Во входном массиве нет повторяющихся элементов, и каждый элемент можно выбирать неограниченное число раз. Верните эти комбинации в виде списка; в результате не должно быть повторяющихся комбинаций.

        +

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

        Например, для входного множества \(\{3, 4, 5\}\) и целевого значения \(9\) решениями будут \(\{3, 3, 3\}\) и \(\{4, 5\}\) . При этом важно учитывать два обстоятельства.

          @@ -4502,7 +4502,7 @@
        • Подмножество не различает порядок элементов, поэтому \(\{4, 5\}\) и \(\{5, 4\}\) считаются одним и тем же подмножеством.

        1.   Отталкиваемся от решения задачи о перестановках

        -

        Как и в задаче о перестановках, можно представлять построение подмножеств как результат последовательности выборов и во время выбора динамически обновлять "сумму элементов"; когда эта сумма становится равной target , соответствующее подмножество записывается в список результатов.

        +

        Как и в задаче о перестановках, можно представлять построение подмножеств как результат последовательности выборов и во время выбора динамически обновлять «сумму элементов». Когда эта сумма становится равной target , соответствующее подмножество записывается в список результатов.

        Однако в отличие от задачи о перестановках в этой задаче элементы множества можно выбирать неограниченное число раз, поэтому нам не нужен булев список selected для записи того, был ли выбран элемент. Можно слегка изменить код для перестановок и получить первоначальную версию решения:

        @@ -4999,7 +4999,7 @@
      • Сравнение подмножеств (то есть массивов) само по себе довольно затратно: сначала приходится сортировать массивы, а затем поэлементно сравнивать их.

      2.   Обрезка повторяющихся подмножеств

      -

      Поэтому стоит выполнять устранение дубликатов прямо во время поиска, с помощью обрезки. Посмотрите на рисунок 13-11: повторяющиеся подмножества возникают тогда, когда элементы массива выбираются в разном порядке, например так.

      +

      Поэтому стоит выполнять устранение дубликатов прямо во время поиска, с помощью обрезки. Как видно на рисунке 13-11, повторяющиеся подмножества возникают тогда, когда элементы массива выбираются в разном порядке, например так.

      1. Если в первом и втором раундах выбрать соответственно \(3\) и \(4\) , то будут сгенерированы все подмножества, содержащие эти два элемента, и их можно обозначить как \([3, 4, \dots]\) .
      2. После этого, если в первом раунде выбрать \(4\) , то во втором раунде нужно пропустить \(3\) , потому что подмножества \([4, 3, \dots]\) полностью дублируют подмножества, уже построенные на шаге 1. .
      3. @@ -5013,13 +5013,13 @@

        Повторяющиеся подмножества из-за разного порядка выбора

        Рисунок 13-11   Повторяющиеся подмножества из-за разного порядка выбора

        -

        В общем виде, если входной массив имеет вид \([x_1, x_2, \dots, x_n]\) , а последовательность выборов в ходе поиска равна \([x_{i_1}, x_{i_2}, \dots, x_{i_m}]\) , то она должна удовлетворять условию \(i_1 \leq i_2 \leq \dots \leq i_m\) ; все последовательности выборов, не удовлетворяющие этому условию, приводят к дубликатам и должны отсекаться.

        +

        В общем виде, если входной массив имеет вид \([x_1, x_2, \dots, x_n]\) , а последовательность выборов в ходе поиска равна \([x_{i_1}, x_{i_2}, \dots, x_{i_m}]\) , то она должна удовлетворять условию \(i_1 \leq i_2 \leq \dots \leq i_m\). Все последовательности выборов, не удовлетворяющие этому условию, приводят к дубликатам и должны отсекаться.

        3.   Реализация кода

        Чтобы реализовать такую обрезку, инициализируем переменную start , которая будет указывать начальную точку обхода. После выбора элемента \(x_i\) следующий раунд начинается с индекса \(i\). Благодаря этому последовательность выборов всегда удовлетворяет условию \(i_1 \leq i_2 \leq \dots \leq i_m\) , а значит, каждое подмножество создается только один раз.

        Помимо этого, мы внесем в код еще два улучшения.

        • Перед началом поиска отсортируем массив nums . Тогда при обходе всех вариантов можно сразу прервать цикл, как только сумма подмножества превысит target , потому что все последующие элементы будут еще больше и их сумма тоже превысит target .
        • -
        • Откажемся от отдельной переменной суммы total и будем учитывать сумму через вычитание из target ; когда target станет равным \(0\) , решение фиксируется.
        • +
        • Откажемся от отдельной переменной суммы total и будем учитывать сумму через вычитание из target. Когда target станет равным \(0\) , решение фиксируется.
        @@ -5526,10 +5526,10 @@

        13.3.2   Учет повторяющихся элементов

        Question

        -

        Дан массив положительных целых чисел nums и целое положительное значение target . Найдите все возможные комбинации, сумма элементов которых равна target . Во входном массиве могут присутствовать повторяющиеся элементы, и каждый элемент разрешено выбирать только один раз. Верните эти комбинации в виде списка; в результате не должно быть повторяющихся комбинаций.

        +

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

        По сравнению с предыдущей задачей во входном массиве теперь могут присутствовать повторяющиеся элементы, и это создает новую проблему. Например, если дан массив \([4, \hat{4}, 5]\) и целевое значение \(9\) , то существующий код вернет результат \([4, 5], [\hat{4}, 5]\) , то есть с повторяющимся подмножеством.

        -

        Причина появления дублей в том, что равные элементы выбираются несколько раз в одном и том же раунде. На рисунке 13-13 в первом раунде существует три варианта выбора, и два из них равны \(4\) ; из-за этого появляются две дублирующиеся ветви поиска и, соответственно, повторяющиеся подмножества. Точно так же два элемента \(4\) во втором раунде тоже порождают дубликаты.

        +

        Причина появления дублей в том, что равные элементы выбираются несколько раз в одном и том же раунде. На рисунке 13-13 в первом раунде существует три варианта выбора, и два из них равны \(4\). Из-за этого появляются две дублирующиеся ветви поиска и, соответственно, повторяющиеся подмножества. Точно так же два элемента \(4\) во втором раунде тоже порождают дубликаты.

        Повторяющиеся подмножества из-за равных элементов

        Рисунок 13-13   Повторяющиеся подмножества из-за равных элементов

        diff --git a/ru/chapter_backtracking/summary/index.html b/ru/chapter_backtracking/summary/index.html index 9b414c9da..f0556b47f 100644 --- a/ru/chapter_backtracking/summary/index.html +++ b/ru/chapter_backtracking/summary/index.html @@ -4360,11 +4360,11 @@

        1.   Ключевые выводы

        • Алгоритм поиска с возвратом по своей сути является методом полного перебора: он ищет решения путем обхода пространства решений в глубину. Во время поиска он фиксирует решения, удовлетворяющие условиям, пока не найдет все такие решения или пока обход не завершится.
        • -
        • Процесс поиска с возвратом состоит из двух частей: попытки и отката. Он с помощью поиска в глубину пробует разные варианты выбора; когда встречается состояние, не удовлетворяющее ограничениям, алгоритм отменяет предыдущий выбор, возвращается к прошлому состоянию и продолжает пробовать другие варианты. Попытка и откат являются двумя противоположными по направлению действиями.
        • +
        • Процесс поиска с возвратом состоит из двух частей: попытки и отката. Он с помощью поиска в глубину пробует разные варианты выбора. Когда встречается состояние, не удовлетворяющее ограничениям, алгоритм отменяет предыдущий выбор, возвращается к прошлому состоянию и продолжает пробовать другие варианты. Попытка и откат являются двумя противоположными по направлению действиями.
        • Задачи поиска с возвратом обычно содержат несколько ограничений, которые можно использовать для обрезки. Обрезка позволяет заранее завершать ненужные ветви поиска и тем самым значительно повышать эффективность.
        • Алгоритм поиска с возвратом в первую очередь применяется для решения поисковых задач и задач с ограничениями. Задачи комбинаторной оптимизации тоже можно решать с его помощью, но для них часто существуют более эффективные или более подходящие методы.
        • Задача о перестановках нацелена на поиск всех возможных перестановок элементов данного множества. Мы используем массив для записи того, был ли выбран каждый элемент, и отсекаем ветви, где один и тот же элемент выбирается повторно, чтобы гарантировать однократный выбор каждого элемента.
        • -
        • В задаче о перестановках, если во множестве присутствуют повторяющиеся элементы, в итоговом результате возникнут повторяющиеся перестановки. Поэтому нужно ограничить выбор равных элементов так, чтобы в каждом раунде каждый из них выбирался только один раз; обычно это реализуется с помощью хеш-множества.
        • +
        • В задаче о перестановках, если во множестве присутствуют повторяющиеся элементы, в итоговом результате возникнут повторяющиеся перестановки. Поэтому нужно ограничить выбор равных элементов так, чтобы в каждом раунде каждый из них выбирался только один раз. Обычно это реализуется с помощью хеш-множества.
        • Цель задачи о сумме подмножеств - найти все подмножества данного множества, сумма которых равна целевому значению. В множестве порядок элементов не важен, однако процесс поиска порождает результаты во всех возможных порядках, из-за чего появляются повторяющиеся подмножества. Поэтому перед запуском поиска с возвратом мы сортируем данные и вводим переменную, указывающую начальную точку обхода в каждом раунде, чтобы отсечь ветви, создающие дубликаты.
        • В задаче о сумме подмножеств равные элементы массива также порождают повторяющиеся множества. При наличии предварительной сортировки их можно отсекать, проверяя равенство соседних элементов, и тем самым гарантировать, что в каждом раунде равные элементы будут выбираться только один раз.
        • Задача о \(n\) ферзях состоит в поиске способов разместить \(n\) ферзей на доске размера \(n \times n\) так, чтобы никакие два ферзя не атаковали друг друга. Ограничения этой задачи включают строки, столбцы, главные диагонали и побочные диагонали. Чтобы выполнить ограничение по строкам, используется построчная стратегия размещения, гарантирующая по одному ферзю в каждой строке.
        • @@ -4372,10 +4372,10 @@

        2.   Вопросы и ответы

        Q: Как понять связь между поиском с возвратом и рекурсией?

        -

        В целом поиск с возвратом - это скорее "алгоритмическая стратегия", а рекурсия больше похожа на "инструмент".

        +

        В целом поиск с возвратом - это скорее «алгоритмическая стратегия», а рекурсия больше похожа на «инструмент».

        • Алгоритмы поиска с возвратом обычно реализуются на основе рекурсии. Однако поиск с возвратом - это лишь один из вариантов применения рекурсии, а именно ее использование в поисковых задачах.
        • -
        • Структура рекурсии отражает парадигму разбиения на подзадачи и часто применяется для решения задач "разделяй и властвуй", поиска с возвратом, динамического программирования (мемоизированной рекурсии) и других подобных задач.
        • +
        • Структура рекурсии отражает парадигму разбиения на подзадачи и часто применяется для решения задач «разделяй и властвуй», поиска с возвратом, динамического программирования (мемоизированной рекурсии) и других подобных задач.
        diff --git a/ru/chapter_computational_complexity/iteration_and_recursion/index.html b/ru/chapter_computational_complexity/iteration_and_recursion/index.html index 5e98f4705..582bca4f5 100644 --- a/ru/chapter_computational_complexity/iteration_and_recursion/index.html +++ b/ru/chapter_computational_complexity/iteration_and_recursion/index.html @@ -4706,7 +4706,7 @@

        -

        Ниже представлена блок-схема этой функции суммирования.

        +

        На рисунке 2-1 представлена блок-схема этой функции суммирования.

        Блок-схема функции суммирования

        Рисунок 2-1   Блок-схема функции суммирования

        @@ -5353,7 +5353,7 @@

        -

        Ниже приведена блок-схема такого вложенного цикла.

        +

        На рисунке 2-2 приведена блок-схема такого вложенного цикла.

        Блок-схема вложенного цикла

        Рисунок 2-2   Блок-схема вложенного цикла

        @@ -5548,7 +5548,7 @@

        -

        Ниже представлен рекурсивный процесс этой функции.

        +

        На рисунке 2-3 представлен рекурсивный процесс этой функции.

        Рекурсивный процесс функции суммирования

        Рисунок 2-3   Рекурсивный процесс функции суммирования

        @@ -5744,10 +5744,10 @@

        Обратите внимание: многие компиляторы и интерпретаторы не поддерживают оптимизацию хвостовой рекурсии. Например, Python по умолчанию такую оптимизацию не выполняет, поэтому даже функция в хвостово-рекурсивной форме все равно может привести к переполнению стека.

        3.   Дерево рекурсии

        -

        При решении задач, связанных с алгоритмами типа "разделяй и властвуй", рекурсия часто оказывается более интуитивной и читабельной, чем итерация. Рассмотрим в качестве примера последовательность Фибоначчи.

        +

        При решении задач, связанных с алгоритмами типа «разделяй и властвуй», рекурсия часто оказывается более интуитивной и читабельной, чем итерация. Рассмотрим в качестве примера последовательность Фибоначчи.

        Question

        -

        Дана последовательность Фибоначчи \(0, 1, 1, 2, 3, 5, 8, 13, \dots\) ; найди \(n\)-й элемент этой последовательности.

        +

        Дана последовательность Фибоначчи \(0, 1, 1, 2, 3, 5, 8, 13, \dots\). Найди \(n\)-й элемент этой последовательности.

        Обозначив \(n\)-й член последовательности Фибоначчи как \(f(n)\) , можно сформулировать два утверждения.

          @@ -5935,10 +5935,10 @@

          Дерево рекурсии последовательности Фибоначчи

          Рисунок 2-6   Дерево рекурсии последовательности Фибоначчи

          -

          По своей сути рекурсия отражает парадигму мышления "разбиение задачи на более мелкие подзадачи", что делает стратегию "разделяй и властвуй" крайне важной.

          +

          По своей сути рекурсия отражает парадигму мышления «разбиение задачи на более мелкие подзадачи», что делает стратегию «разделяй и властвуй» крайне важной.

            -
          • С точки зрения алгоритмов многие важные алгоритмические стратегии, такие как поиск, сортировка, возврат, "разделяй и властвуй" и динамическое программирование, прямо или косвенно используют этот подход.
          • -
          • С точки зрения структур данных рекурсия естественно подходит для решения задач, связанных со списками, деревьями и графами, поскольку они очень хорошо поддаются анализу с использованием идеи "разделяй и властвуй".
          • +
          • С точки зрения алгоритмов многие важные алгоритмические стратегии, такие как поиск, сортировка, возврат, «разделяй и властвуй» и динамическое программирование, прямо или косвенно используют этот подход.
          • +
          • С точки зрения структур данных рекурсия естественно подходит для решения задач, связанных со списками, деревьями и графами, поскольку они очень хорошо поддаются анализу с использованием идеи «разделяй и властвуй».

          2.2.3   Сравнение

          Подводя итог, можно сказать, что итерация и рекурсия различаются по реализации, производительности и применимости, как показано в таблице 2-1.

          @@ -5972,20 +5972,20 @@ Сфера использования Подходит для простых циклических задач, код интуитивно понятен и хорошо читаем -Подходит для разбиения на подзадачи, для структур деревья и графы, алгоритмов "разделяй и властвуй", возврата и т. д.; структура кода проста и ясна +Подходит для разбиения на подзадачи, для структур деревья и графы, алгоритмов «разделяй и властвуй», возврата и т. д.. Структура кода проста и ясна

        Tip

        -

        Если дальнейшее содержание кажется сложным, можно вернуться к нему после чтения главы о "стеке".

        +

        Если дальнейшее содержание кажется сложным, можно вернуться к нему после чтения главы о «стеке».

        -

        Какова же внутренняя связь между итерацией и рекурсией? В рассмотренном примере рекурсивной функции операция сложения выполняется на этапе возврата рекурсии. Это означает, что функция, вызванная первой, фактически завершает операцию сложения последней, что соответствует принципу стека "первым пришел - последним вышел".

        -

        На самом деле такие термины рекурсии, как "стек вызовов" и "пространство стекового кадра", уже намекают на тесную связь между рекурсией и стеком.

        +

        Какова же внутренняя связь между итерацией и рекурсией? В рассмотренном примере рекурсивной функции операция сложения выполняется на этапе возврата рекурсии. Это означает, что функция, вызванная первой, фактически завершает операцию сложения последней, что соответствует принципу стека «первым пришел - последним вышел».

        +

        На самом деле такие термины рекурсии, как «стек вызовов» и «пространство стекового кадра», уже намекают на тесную связь между рекурсией и стеком.

          -
        1. Вызов: когда вызывается функция, система выделяет для нее новый стековый кадр в "стеке вызовов" для хранения локальных переменных функции, параметров, адреса возврата и других данных.
        2. -
        3. Возврат: когда функция завершает выполнение и возвращает результат, соответствующий стековый кадр удаляется из "стека вызовов", восстанавливая среду выполнения предыдущей функции.
        4. +
        5. Вызов: когда вызывается функция, система выделяет для нее новый стековый кадр в «стеке вызовов» для хранения локальных переменных функции, параметров, адреса возврата и других данных.
        6. +
        7. Возврат: когда функция завершает выполнение и возвращает результат, соответствующий стековый кадр удаляется из «стека вызовов», восстанавливая среду выполнения предыдущей функции.

        Таким образом, можно использовать явный стек для моделирования поведения стека вызовов, чтобы преобразовать рекурсию в итеративную форму:

        diff --git a/ru/chapter_computational_complexity/performance_evaluation/index.html b/ru/chapter_computational_complexity/performance_evaluation/index.html index 095b0010b..bc9b7fbd1 100644 --- a/ru/chapter_computational_complexity/performance_evaluation/index.html +++ b/ru/chapter_computational_complexity/performance_evaluation/index.html @@ -4371,15 +4371,15 @@

        Методы оценки эффективности делятся на два типа: практическое тестирование и теоретическую оценку.

        2.1.1   Практическое тестирование

        Предположим, у нас есть алгоритмы A и B, которые решают одну и ту же задачу, и необходимо сравнить их эффективность. Самый прямой метод - это запустить оба алгоритма на компьютере и зафиксировать время их выполнения и объем используемой памяти. Этот метод отражает реальную ситуацию, но имеет значительные ограничения.

        -

        С одной стороны, сложно исключить влияние факторов тестовой среды. Аппаратная конфигурация влияет на производительность алгоритма. Например, если алгоритм обладает высокой степенью параллелизма, он будет лучше работать на многоядерных CPU; если алгоритм интенсивно использует память, его производительность будет выше на высокопроизводительной памяти. Это означает, что результаты тестирования на разных машинах могут значительно отличаться, а для получения средней эффективности пришлось бы тестировать на различных платформах, что крайне затруднительно.

        +

        С одной стороны, сложно исключить влияние факторов тестовой среды. Аппаратная конфигурация влияет на производительность алгоритма. Например, если алгоритм обладает высокой степенью параллелизма, он будет лучше работать на многоядерных CPU. Если алгоритм интенсивно использует память, его производительность будет выше на высокопроизводительной памяти. Это означает, что результаты тестирования на разных машинах могут значительно отличаться, а для получения средней эффективности пришлось бы тестировать на различных платформах, что крайне затруднительно.

        С другой стороны, проведение полного тестирования требует значительных ресурсов. С изменением объема входных данных алгоритмы демонстрируют разную эффективность. Например, при небольшом объеме данных алгоритм A может работать быстрее, чем алгоритм B, но при большом объеме данных результат может быть противоположным. Следовательно, для получения убедительных выводов необходимо тестировать различные масштабы входных данных, что требует значительных вычислительных ресурсов.

        2.1.2   Теоретическая оценка

        Из-за значительных ограничений практического тестирования можно рассмотреть возможность оценки эффективности алгоритмов только с помощью вычислений. Такой метод называется анализом асимптотической сложности (asymptotic complexity analysis), или сокращенно анализом сложности.

        Анализ сложности позволяет отразить зависимость между ресурсами времени и пространства, необходимыми для выполнения алгоритма, и размером входных данных. Он описывает тенденцию роста времени и пространства, необходимых для выполнения алгоритма, по мере увеличения размера входных данных. Это определение может показаться сложным, но его можно разбить на три ключевых момента.

          -
        • "Ресурсы времени и пространства" соответствуют временной сложности (time complexity) и пространственной сложности (space complexity).
        • -
        • "По мере увеличения размера входных данных" означает, что сложность отражает зависимость эффективности алгоритма от объема входных данных.
        • -
        • "Тенденция роста времени и пространства" указывает, что анализ сложности фокусируется не на конкретных значениях времени выполнения или объема занимаемой памяти, а на скорости их роста.
        • +
        • «Ресурсы времени и пространства» соответствуют временной сложности (time complexity) и пространственной сложности (space complexity).
        • +
        • «По мере увеличения размера входных данных» означает, что сложность отражает зависимость эффективности алгоритма от объема входных данных.
        • +
        • «Тенденция роста времени и пространства» указывает, что анализ сложности фокусируется не на конкретных значениях времени выполнения или объема занимаемой памяти, а на скорости их роста.

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

          diff --git a/ru/chapter_computational_complexity/space_complexity/index.html b/ru/chapter_computational_complexity/space_complexity/index.html index 302c148be..1a451067e 100644 --- a/ru/chapter_computational_complexity/space_complexity/index.html +++ b/ru/chapter_computational_complexity/space_complexity/index.html @@ -4535,7 +4535,7 @@

          Временное пространство можно дополнительно разделить на три части.

          • Временные данные: используются для хранения различных констант, переменных, объектов и т.д., возникающих во время выполнения алгоритма.
          • -
          • Пространство кадров стека: используется для хранения контекстных данных вызываемых функций. При каждом вызове функции система создает на вершине стека новый кадр; после возврата функции пространство этого кадра освобождается.
          • +
          • Пространство кадров стека: используется для хранения контекстных данных вызываемых функций. При каждом вызове функции система создает на вершине стека новый кадр. После возврата функции пространство этого кадра освобождается.
          • Пространство инструкций: используется для хранения скомпилированных инструкций программы и в реальном подсчете обычно не учитывается.

          При анализе пространственной сложности программы обычно учитываются временные данные, пространство стека и выходные данные, как показано на рисунке 2-15.

          @@ -4864,7 +4864,7 @@

          Рассмотрим следующий код. Понятие худшей пространственной сложности здесь имеет два значения.

          1. Ориентир на худшие входные данные: когда \(n < 10\) , пространственная сложность равна \(O(1)\) ; но когда \(n > 10\) , инициализированный массив nums занимает \(O(n)\) пространства, поэтому худшая пространственная сложность равна \(O(n)\) .
          2. -
          3. Ориентир на пиковое использование памяти во время выполнения: например, до выполнения последней строки программа занимает \(O(1)\) пространства; при инициализации массива nums она занимает \(O(n)\) пространства, поэтому худшая пространственная сложность также равна \(O(n)\) .
          4. +
          5. Ориентир на пиковое использование памяти во время выполнения: например, до выполнения последней строки программа занимает \(O(1)\) пространства. При инициализации массива nums она занимает \(O(n)\) пространства, поэтому худшая пространственная сложность также равна \(O(n)\) .
          @@ -5246,7 +5246,7 @@

          Функции loop() и recur() имеют временную сложность \(O(n)\) , но их пространственная сложность различается.

            -
          • Функция loop() вызывает function() в цикле \(n\) раз; на каждой итерации function() возвращается и освобождает пространство своего кадра стека, поэтому пространственная сложность по-прежнему равна \(O(1)\) .
          • +
          • Функция loop() вызывает function() в цикле \(n\) раз. На каждой итерации function() возвращается и освобождает пространство своего кадра стека, поэтому пространственная сложность по-прежнему равна \(O(1)\) .
          • Рекурсивная функция recur() во время выполнения одновременно содержит \(n\) еще не завершившихся экземпляров recur() , поэтому занимает \(O(n)\) пространства кадров стека.

          2.4.3   Распространенные типы

          @@ -6207,7 +6207,7 @@

          -

          Как показано на рисунке 2-18, глубина рекурсии этой функции равна \(n\) , и в каждой рекурсивной функции инициализируется массив длины \(n\) , \(n-1\) , \(\dots\) , \(2\) , \(1\) ; его средняя длина равна \(n / 2\) , поэтому в сумме используется \(O(n^2)\) пространства:

          +

          Как показано на рисунке 2-18, глубина рекурсии этой функции равна \(n\) , и в каждой рекурсивной функции инициализируется массив длины \(n\) , \(n-1\) , \(\dots\) , \(2\) , \(1\). Его средняя длина равна \(n / 2\) , поэтому в сумме используется \(O(n^2)\) пространства:

          @@ -6375,7 +6375,7 @@

          Рисунок 2-18   Квадратичная пространственная сложность, порождаемая рекурсивной функцией

          4.   Экспоненциальная сложность \(O(2^n)\)

          -

          Экспоненциальная сложность часто встречается у бинарных деревьев. Полное бинарное дерево с \(n\) уровнями содержит \(2^n - 1\) узлов и занимает \(O(2^n)\) пространства:

          +

          Экспоненциальная сложность часто встречается у бинарных деревьев. Как видно на рисунке 2-19, полное бинарное дерево с \(n\) уровнями содержит \(2^n - 1\) узлов и занимает \(O(2^n)\) пространства:

          @@ -6559,11 +6559,11 @@

          Рисунок 2-19   Экспоненциальная пространственная сложность, порождаемая полным бинарным деревом

          5.   Логарифмическая сложность \(O(\log n)\)

          -

          Логарифмическая сложность часто встречается в алгоритмах "разделяй и властвуй". Например, при сортировке слиянием входной массив длины \(n\) на каждом шаге рекурсии делится пополам, образуя рекурсивное дерево высоты \(\log n\) и используя \(O(\log n)\) пространства кадров стека.

          +

          Логарифмическая сложность часто встречается в алгоритмах «разделяй и властвуй». Например, при сортировке слиянием входной массив длины \(n\) на каждом шаге рекурсии делится пополам, образуя рекурсивное дерево высоты \(\log n\) и используя \(O(\log n)\) пространства кадров стека.

          Еще один пример - преобразование числа в строку. Если задано положительное целое число \(n\) , то количество его цифр равно \(\lfloor \log_{10} n \rfloor + 1\) , то есть длина соответствующей строки тоже равна \(\lfloor \log_{10} n \rfloor + 1\) , следовательно, пространственная сложность составляет \(O(\log_{10} n + 1) = O(\log n)\) .

          2.4.4   Компромисс между временем и пространством

          В идеальных условиях хотелось бы, чтобы и временная, и пространственная сложность алгоритма были оптимальными. Однако на практике одновременно оптимизировать и время, и память обычно очень трудно.

          -

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

          +

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

          Выбор между этими двумя идеями зависит от того, что важнее в конкретной задаче. В большинстве случаев время ценнее памяти, поэтому стратегия обмена пространства на время используется чаще. Но при очень больших объемах данных контроль пространственной сложности тоже становится крайне важным.

          diff --git a/ru/chapter_computational_complexity/summary/index.html b/ru/chapter_computational_complexity/summary/index.html index 65d3865e2..aa8065505 100644 --- a/ru/chapter_computational_complexity/summary/index.html +++ b/ru/chapter_computational_complexity/summary/index.html @@ -4391,10 +4391,10 @@
        • Java и C# - объектно-ориентированные языки программирования, в которых блоки кода (методы) обычно являются частью класса. Статические методы по поведению похожи на функции, потому что они привязаны к классу и не могут обращаться к конкретным переменным экземпляра.
        • C++ и Python поддерживают как процедурное программирование (функции), так и объектно-ориентированное программирование (методы).
        -

        Q: Отражает ли диаграмма "распространенных типов пространственной сложности" абсолютный размер занятой памяти?

        +

        Q: Отражает ли диаграмма «распространенных типов пространственной сложности» абсолютный размер занятой памяти?

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

        Если взять \(n = 8\) , можно заметить, что значения на кривых не совпадают напрямую с соответствующими функциями. Это связано с тем, что каждая кривая содержит константный член, который сжимает диапазон значений до визуально удобного масштаба.

        -

        На практике, поскольку мы обычно не знаем, какова "константная" сложность каждого метода, только по сложности мы, как правило, не можем выбрать оптимальное решение для случая \(n = 8\) . Но для \(n = 8^5\) выбор уже очевиден: в этой области доминирует именно тенденция роста.

        +

        На практике, поскольку мы обычно не знаем, какова «константная» сложность каждого метода, только по сложности мы, как правило, не можем выбрать оптимальное решение для случая \(n = 8\) . Но для \(n = 8^5\) выбор уже очевиден: в этой области доминирует именно тенденция роста.

        Q: Бывают ли случаи, когда в реальных сценариях алгоритм специально проектируют так, чтобы жертвовать временем ради пространства или пространством ради времени?

        На практике в большинстве случаев выбирают обмен пространства на время. Например, для индексов в базах данных обычно строят B+ деревья или хеш-индексы, расходуя значительный объем памяти ради эффективных запросов уровня \(O(\log n)\) или даже \(O(1)\).

        В сценариях, где память особенно дорога, наоборот, могут жертвовать временем ради пространства. Например, в embedded-разработке память устройства очень ограничена, поэтому инженеры могут отказаться от хеш-таблиц и выбрать последовательный поиск по массиву, экономя память ценой более медленного поиска.

        diff --git a/ru/chapter_computational_complexity/time_complexity/index.html b/ru/chapter_computational_complexity/time_complexity/index.html index 6a72e2aa6..f6248b800 100644 --- a/ru/chapter_computational_complexity/time_complexity/index.html +++ b/ru/chapter_computational_complexity/time_complexity/index.html @@ -4830,7 +4830,7 @@

        Но на практике подсчитывать реальное время выполнения алгоритма и неразумно, и нереалистично. Во-первых, мы не хотим привязывать оценку времени к конкретной платформе, потому что алгоритм должен запускаться на самых разных платформах. Во-вторых, нам трудно определить время выполнения каждого типа операций, а это делает точную оценку крайне затруднительной.

        2.3.1   Подсчет тенденции роста времени

        Анализ временной сложности оценивает не само время выполнения алгоритма, а тенденцию роста этого времени по мере увеличения объема данных.

        -

        Понятие "тенденции роста времени" выглядит довольно абстрактным, поэтому разберем его на примере. Предположим, размер входных данных равен \(n\) , и даны три алгоритма A , B и C :

        +

        Понятие «тенденции роста времени» выглядит довольно абстрактным, поэтому разберем его на примере. Предположим, размер входных данных равен \(n\) , и даны три алгоритма A , B и C :

        @@ -5077,11 +5077,11 @@
        -

        Ниже показаны временные сложности трех приведенных выше функций.

        +

        На рисунке 2-7 показаны временные сложности трех приведенных выше функций.

        • У алгоритма A есть только одна операция вывода, и время его работы не растет с увеличением \(n\) . Такую временную сложность называют постоянной.
        • В алгоритме B операция вывода выполняется в цикле \(n\) раз, поэтому время работы растет линейно по мере увеличения \(n\) . Такая временная сложность называется линейной.
        • -
        • В алгоритме C операция вывода выполняется \(1000000\) раз; хотя время работы велико, оно не зависит от размера входных данных \(n\) . Поэтому временная сложность C такая же, как у A , и тоже является постоянной.
        • +
        • В алгоритме C операция вывода выполняется \(1000000\) раз. Хотя время работы велико, оно не зависит от размера входных данных \(n\) . Поэтому временная сложность C такая же, как у A , и тоже является постоянной.

        Тенденции роста времени для алгоритмов A, B и C

        Рисунок 2-7   Тенденции роста времени для алгоритмов A, B и C

        @@ -5253,16 +5253,16 @@
    -

    Пусть количество операций алгоритма является функцией от размера входных данных \(n\) и обозначается как \(T(n)\) ; тогда для приведенной выше функции число операций равно:

    +

    Пусть количество операций алгоритма является функцией от размера входных данных \(n\) и обозначается как \(T(n)\). Тогда для приведенной выше функции число операций равно:

    \[ T(n) = 3 + 2n \]

    \(T(n)\) - линейная функция, а это означает, что тенденция роста времени работы линейна, следовательно, временная сложность здесь тоже линейна.

    -

    Линейную временную сложность записывают как \(O(n)\) ; этот математический символ называется нотацией Big \(O\) (big-\(O\) notation) и обозначает асимптотическую верхнюю границу (asymptotic upper bound) функции \(T(n)\) .

    +

    Линейную временную сложность записывают как \(O(n)\). Этот математический символ называется нотацией Big \(O\) (big-\(O\) notation) и обозначает асимптотическую верхнюю границу (asymptotic upper bound) функции \(T(n)\) .

    Иными словами, анализ временной сложности сводится к определению асимптотической верхней границы числа операций \(T(n)\), и у этого понятия есть строгое математическое определение.

    Асимптотическая верхняя граница функции

    -

    Если существуют положительное действительное число \(c\) и действительное число \(n_0\) , такие что для всех \(n > n_0\) выполняется \(T(n) \leq c \cdot f(n)\) , то можно считать, что \(f(n)\) задает асимптотическую верхнюю границу для \(T(n)\) ; это записывается как \(T(n) = O(f(n))\) .

    +

    Если существуют положительное действительное число \(c\) и действительное число \(n_0\) , такие что для всех \(n > n_0\) выполняется \(T(n) \leq c \cdot f(n)\) , то можно считать, что \(f(n)\) задает асимптотическую верхнюю границу для \(T(n)\). Это записывается как \(T(n) = O(f(n))\) .

    Как показано на рисунке 2-8, вычислить асимптотическую верхнюю границу - значит найти такую функцию \(f(n)\) , что при стремлении \(n\) к бесконечности функции \(T(n)\) и \(f(n)\) имеют один и тот же порядок роста и отличаются только постоянным коэффициентом \(c\).

    Асимптотическая верхняя граница функции

    @@ -5276,7 +5276,7 @@ T(n) = 3 + 2n
    1. Игнорировать константы в \(T(n)\). Они не зависят от \(n\) , а значит не влияют на временную сложность.
    2. Опускать все коэффициенты. Например, циклы на \(2n\) раз или \(5n + 1\) раз можно упростить до \(n\) раз, потому что коэффициент перед \(n\) не влияет на временную сложность.
    3. -
    4. При вложенных циклах использовать умножение. Общее число операций равно произведению числа операций внешнего и внутреннего циклов; при этом для каждого уровня цикла по-прежнему можно применять приемы из пунктов 1. и 2. .
    5. +
    6. При вложенных циклах использовать умножение. Общее число операций равно произведению числа операций внешнего и внутреннего циклов. При этом для каждого уровня цикла по-прежнему можно применять приемы из пунктов 1. и 2. .

    Для заданной функции мы можем использовать перечисленные выше приемы и подсчитать число операций:

    @@ -5498,7 +5498,7 @@ T(n) = 3 + 2n
    -

    Следующая формула показывает результаты подсчета до и после использования перечисленных выше приемов; в обоих случаях выводимая временная сложность равна \(O(n^2)\) .

    +

    Следующая формула показывает результаты подсчета до и после использования перечисленных выше приемов. В обоих случаях выводимая временная сложность равна \(O(n^2)\) .

    \[ \begin{aligned} T(n) & = 2n(n + 1) + (5n + 1) + 2 & \text{полный подсчет (-.-|||)} \newline @@ -5544,7 +5544,7 @@ T(n) & = n^2 + n & \text{ленивый подсчет (o.O)}

    2.3.4   Распространенные типы

    -

    Пусть размер входных данных равен \(n\) ; распространенные типы временной сложности показаны на рисунке 2-9 в порядке от меньшей к большей.

    +

    Пусть размер входных данных равен \(n\). Распространенные типы временной сложности показаны на рисунке 2-9 в порядке от меньшей к большей.

    \[ \begin{aligned} & O(1) < O(\log n) < O(n) < O(n \log n) < O(n^2) < O(2^n) < O(n!) \newline @@ -6029,7 +6029,7 @@ T(n) & = n^2 + n & \text{ленивый подсчет (o.O)}

    -

    Стоит отметить, что размер входных данных \(n\) нужно определять конкретно в зависимости от типа входа. Например, в первом примере переменная \(n\) сама является размером входных данных; во втором примере размером данных служит длина массива.

    +

    Стоит отметить, что размер входных данных \(n\) нужно определять конкретно в зависимости от типа входа. Например, в первом примере переменная \(n\) сама является размером входных данных. Во втором примере размером данных служит длина массива.

    3.   Квадратичная сложность \(O(n^2)\)

    Квадратичная сложность характеризуется тем, что число операций растет квадратично относительно размера входных данных \(n\) . Квадратичная сложность обычно встречается во вложенных циклах: временная сложность внешнего и внутреннего циклов равна \(O(n)\) , поэтому общая временная сложность составляет \(O(n^2)\) :

    @@ -6521,8 +6521,8 @@ T(n) & = n^2 + n & \text{ленивый подсчет (o.O)}

    4.   Экспоненциальная сложность \(O(2^n)\)

    -

    Типичный пример экспоненциального роста в биологии - деление клеток: в начальном состоянии есть одна клетка, после одного деления их становится 2, после двух делений - 4 и так далее; после \(n\) раундов деления клеток становится \(2^n\) .

    -

    На рисунке 2-11 и в следующем коде моделируется процесс деления клеток; временная сложность равна \(O(2^n)\) . Здесь входное значение \(n\) обозначает число раундов деления, а возвращаемое значение count обозначает общее число делений.

    +

    Типичный пример экспоненциального роста в биологии - деление клеток: в начальном состоянии есть одна клетка, после одного деления их становится 2, после двух делений - 4 и так далее. После \(n\) раундов деления клеток становится \(2^n\) .

    +

    На рисунке 2-11 и в следующем коде моделируется процесс деления клеток. Временная сложность равна \(O(2^n)\) . Здесь входное значение \(n\) обозначает число раундов деления, а возвращаемое значение count обозначает общее число делений.

    @@ -6958,8 +6958,8 @@ T(n) & = n^2 + n & \text{ленивый подсчет (o.O)}

    Экспоненциальный рост происходит очень быстро и часто встречается в переборных методах, грубой силе, поиске с возвратом и тому подобных подходах. Для задач большого масштаба экспоненциальная сложность неприемлема, и обычно приходится применять динамическое программирование, жадные алгоритмы и другие стратегии.

    5.   Логарифмическая сложность \(O(\log n)\)

    -

    В противоположность экспоненциальной, логарифмическая сложность описывает ситуацию, когда в каждом раунде размер задачи уменьшается вдвое. Пусть размер входных данных равен \(n\) ; так как на каждом шаге размер уменьшается вдвое, число итераций равно \(\log_2 n\) , то есть является обратной функцией к \(2^n\) .

    -

    На рисунке 2-12 и в следующем коде моделируется процесс, в котором в каждом раунде размер задачи уменьшается вдвое; временная сложность равна \(O(\log_2 n)\) и кратко записывается как \(O(\log n)\) :

    +

    В противоположность экспоненциальной, логарифмическая сложность описывает ситуацию, когда в каждом раунде размер задачи уменьшается вдвое. Пусть размер входных данных равен \(n\). Так как на каждом шаге размер уменьшается вдвое, число итераций равно \(\log_2 n\) , то есть является обратной функцией к \(2^n\) .

    +

    На рисунке 2-12 и в следующем коде моделируется процесс, в котором в каждом раунде размер задачи уменьшается вдвое. Временная сложность равна \(O(\log_2 n)\) и кратко записывается как \(O(\log n)\) :

    @@ -7376,10 +7376,10 @@ T(n) & = n^2 + n & \text{ленивый подсчет (o.O)}

    -

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

    +

    Логарифмическая сложность часто встречается в алгоритмах, основанных на стратегии «разделяй и властвуй», и отражает идеи разбиения на части и упрощения сложной задачи. Она растет медленно и считается одной из самых желательных временных сложностей после константной.

    Каково основание у \(O(\log n)\) ?

    -

    Точнее говоря, "разделение на \(m\) частей" соответствует временной сложности \(O(\log_m n)\) . А по формуле перехода к другому основанию логарифма мы получаем равные по сложности выражения с разными основаниями:

    +

    Точнее говоря, «разделение на \(m\) частей» соответствует временной сложности \(O(\log_m n)\) . А по формуле перехода к другому основанию логарифма мы получаем равные по сложности выражения с разными основаниями:

    \[ O(\log_m n) = O(\log_k n / \log_k m) = O(\log_k n) \]
    @@ -7775,7 +7775,7 @@ n! = n \times (n - 1) \times (n - 2) \times \dots \times 2 \times 1

    Следует отметить, что поскольку при \(n \geq 4\) всегда выполняется \(n! > 2^n\) , факториальная сложность растет еще быстрее, чем экспоненциальная, и при больших \(n\) становится неприемлемой.

    2.3.5   Худшая, лучшая и средняя временная сложность

    -

    Временная эффективность алгоритма часто не фиксирована, а зависит от распределения входных данных. Предположим, на вход подается массив nums длины \(n\) , состоящий из чисел от \(1\) до \(n\) , каждое из которых встречается ровно один раз; при этом порядок элементов случайно перемешан. Задача состоит в том, чтобы вернуть индекс элемента \(1\) . Тогда можно сделать следующие выводы.

    +

    Временная эффективность алгоритма часто не фиксирована, а зависит от распределения входных данных. Предположим, на вход подается массив nums длины \(n\) , состоящий из чисел от \(1\) до \(n\) , каждое из которых встречается ровно один раз. При этом порядок элементов случайно перемешан. Задача состоит в том, чтобы вернуть индекс элемента \(1\) . Тогда можно сделать следующие выводы.

    • Когда nums = [?, ?, ..., 1] , то есть когда последний элемент равен \(1\) , нужно полностью пройти по массиву, что дает худшую временную сложность \(O(n)\) .
    • Когда nums = [1, ?, ?, ...] , то есть когда первый элемент равен \(1\) , независимо от длины массива продолжать обход не нужно, что дает лучшую временную сложность \(\Omega(1)\) .
    • @@ -8139,12 +8139,12 @@ n! = n \times (n - 1) \times (n - 2) \times \dots \times 2 \times 1

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

      -

      Из приведенного выше примера видно, что худшая и лучшая временные сложности возникают только при особых распределениях данных; вероятность таких случаев может быть низкой, и они не всегда реально отражают эффективность алгоритма. Напротив, средняя временная сложность способна показать эффективность алгоритма на случайных входных данных и обозначается символом \(\Theta\) .

      -

      Для некоторых алгоритмов можно относительно просто вывести средний случай при случайном распределении данных. Например, в приведенном выше примере входной массив перемешан, а вероятность появления элемента \(1\) на любом индексе одинакова; следовательно, среднее число итераций алгоритма равно половине длины массива, то есть \(n / 2\) , а средняя временная сложность равна \(\Theta(n / 2) = \Theta(n)\) .

      +

      Из приведенного выше примера видно, что худшая и лучшая временные сложности возникают только при особых распределениях данных. Вероятность таких случаев может быть низкой, и они не всегда реально отражают эффективность алгоритма. Напротив, средняя временная сложность способна показать эффективность алгоритма на случайных входных данных и обозначается символом \(\Theta\) .

      +

      Для некоторых алгоритмов можно относительно просто вывести средний случай при случайном распределении данных. Например, в приведенном выше примере входной массив перемешан, а вероятность появления элемента \(1\) на любом индексе одинакова. Следовательно, среднее число итераций алгоритма равно половине длины массива, то есть \(n / 2\) , а средняя временная сложность равна \(\Theta(n / 2) = \Theta(n)\) .

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

      Почему символ \(\Theta\) встречается так редко?

      -

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

      +

      Возможно, потому что символ \(O\) звучит слишком привычно, и мы часто используем его для обозначения средней временной сложности. Но строго говоря, это некорректно. В этой книге и в других материалах, если встретится выражение вроде «средняя временная сложность \(O(n)\)», просто понимай его как \(\Theta(n)\) .

      diff --git a/ru/chapter_data_structure/basic_data_types/index.html b/ru/chapter_data_structure/basic_data_types/index.html index ffa0f93ff..af75cb1ad 100644 --- a/ru/chapter_data_structure/basic_data_types/index.html +++ b/ru/chapter_data_structure/basic_data_types/index.html @@ -4273,7 +4273,7 @@
    • Целочисленные типы byte , short , int , long .
    • Типы с плавающей точкой float , double , используемые для представления дробных чисел.
    • Символьный тип char , используемый для представления букв, знаков препинания и даже эмодзи в разных языках.
    • -
    • Логический тип bool , используемый для представления суждений "да" и "нет".
    • +
    • Логический тип bool , используемый для представления суждений «да» и «нет».

    Базовые типы данных хранятся в компьютере в двоичной форме. Один двоичный разряд равен \(1\) биту. В большинстве современных операционных систем \(1\) байт (byte) состоит из \(8\) битов (bit).

    Диапазон значений базовых типов данных зависит от объема занимаемого ими пространства. Ниже в качестве примера используется Java.

    @@ -4281,7 +4281,7 @@
  • Целочисленный тип byte занимает \(1\) байт = \(8\) бит и может представлять \(2^{8}\) чисел.
  • Целочисленный тип int занимает \(4\) байта = \(32\) бита и может представлять \(2^{32}\) чисел.
  • -

    В таблице 3-1 перечислены объем памяти, диапазон значений и значения по умолчанию для различных базовых типов данных в Java. Эту таблицу не нужно заучивать наизусть; достаточно иметь общее представление и при необходимости обращаться к ней.

    +

    В таблице 3-1 перечислены объем памяти, диапазон значений и значения по умолчанию для различных базовых типов данных в Java. Эту таблицу не нужно заучивать наизусть. Достаточно иметь общее представление и при необходимости обращаться к ней.

    Таблица 3-1   Объем памяти и диапазоны значений базовых типов данных

    @@ -4364,16 +4364,16 @@
    -

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

    +

    Обрати внимание: в таблице 3-1 приведены данные именно для базовых типов данных Java. В каждом языке программирования свои определения типов, поэтому объем памяти, диапазон значений и значения по умолчанию могут различаться.

      -
    • В Python целочисленный тип int может иметь произвольный размер, ограниченный только доступной памятью; тип float является 64-битным числом двойной точности; типа char нет, а отдельный символ на деле является строкой str длины 1.
    • -
    • В C и C++ размер базовых типов данных явно не зафиксирован и зависит от реализации и платформы. таблица 3-1 соответствует модели данных LP64 data model, используемой в 64-битных Unix-системах, включая Linux и macOS.
    • -
    • Размер символа char в C и C++ составляет 1 байт, а в большинстве других языков программирования зависит от конкретного способа кодирования символов; подробнее это рассматривается в разделе "Кодирование символов".
    • +
    • В Python целочисленный тип int может иметь произвольный размер, ограниченный только доступной памятью. Тип float является 64-битным числом двойной точности. Типа char нет, а отдельный символ на деле является строкой str длины 1.
    • +
    • В C и C++ размер базовых типов данных явно не зафиксирован и зависит от реализации и платформы. В таблице 3-1 приведены данные для модели LP64 data model, используемой в 64-битных Unix-системах, включая Linux и macOS.
    • +
    • Размер символа char в C и C++ составляет 1 байт, а в большинстве других языков программирования зависит от конкретного способа кодирования символов. Подробнее это рассматривается в разделе «Кодирование символов».
    • Хотя для представления логического значения достаточно 1 бита ( \(0\) или \(1\) ), в памяти оно обычно хранится как 1 байт. Это связано с тем, что современные CPU обычно используют 1 байт как минимальную адресуемую единицу памяти.
    -

    Какова же связь между базовыми типами данных и структурами данных? Мы знаем, что структура данных - это способ организации и хранения данных в компьютере. Подлежащее в этой фразе - "структура", а не "данные".

    -

    Если мы хотим представить "ряд чисел", то естественно подумаем об использовании массива. Это связано с тем, что линейная структура массива может выразить отношения соседства и порядка между числами, а то, что именно хранится внутри - целые int , вещественные float или символы char , - к "структуре данных" отношения не имеет.

    -

    Иными словами, базовые типы данных задают "тип содержимого" данных, а структуры данных задают "способ организации" данных. Например, в следующем коде мы используем одну и ту же структуру данных (массив) для хранения и представления различных базовых типов данных, включая int , float , char , bool и т.д.

    +

    Какова же связь между базовыми типами данных и структурами данных? Мы знаем, что структура данных - это способ организации и хранения данных в компьютере. Подлежащее в этой фразе - «структура», а не «данные».

    +

    Если мы хотим представить «ряд чисел», то естественно подумаем об использовании массива. Это связано с тем, что линейная структура массива может выразить отношения соседства и порядка между числами, а то, что именно хранится внутри - целые int , вещественные float или символы char , - к «структуре данных» отношения не имеет.

    +

    Иными словами, базовые типы данных задают «тип содержимого» данных, а структуры данных задают «способ организации» данных. Например, в следующем коде мы используем одну и ту же структуру данных (массив) для хранения и представления различных базовых типов данных, включая int , float , char , bool и т.д.

    diff --git a/ru/chapter_data_structure/character_encoding/index.html b/ru/chapter_data_structure/character_encoding/index.html index 534707708..639d2f8fb 100644 --- a/ru/chapter_data_structure/character_encoding/index.html +++ b/ru/chapter_data_structure/character_encoding/index.html @@ -4423,9 +4423,9 @@

    3.4   Кодирование символов *

    -

    В компьютере все данные хранятся в виде двоичных чисел, и символьный тип данных char не является исключением. Для представления символов необходимо задать "таблицу символов", которая устанавливает взаимно-однозначное соответствие между каждым символом и двоичным числом. С помощью этой таблицы компьютер может преобразовывать двоичные числа в символы.

    +

    В компьютере все данные хранятся в виде двоичных чисел, и символьный тип данных char не является исключением. Для представления символов необходимо задать «таблицу символов», которая устанавливает взаимно-однозначное соответствие между каждым символом и двоичным числом. С помощью этой таблицы компьютер может преобразовывать двоичные числа в символы.

    3.4.1   Таблица символов ASCII

    -

    Код ASCII - это самая ранняя таблица символов; ее полное название - American Standard Code for Information Interchange (американский стандартный код обмена информацией). Для представления символов в ней используются 7 двоичных битов (нижние 7 битов одного байта), что позволяет закодировать до 128 различных символов. Как показано на рисунке 3-6, ASCII включает заглавные и строчные буквы английского алфавита, цифры 0 ~ 9, некоторые знаки препинания, а также некоторые управляющие символы (например перевод строки и табуляцию).

    +

    Код ASCII - это самая ранняя таблица символов. Ее полное название - American Standard Code for Information Interchange (американский стандартный код обмена информацией). Для представления символов в ней используются 7 двоичных битов (нижние 7 битов одного байта), что позволяет закодировать до 128 различных символов. Как показано на рисунке 3-6, ASCII включает заглавные и строчные буквы английского алфавита, цифры 0 ~ 9, некоторые знаки препинания, а также некоторые управляющие символы (например перевод строки и табуляцию).

    Таблица ASCII

    Рисунок 3-6   Таблица ASCII

    @@ -4435,11 +4435,11 @@

    Позже люди обнаружили, что кодов EASCII все равно недостаточно для количества символов во многих языках. Например, китайских иероглифов существует почти сто тысяч, а в повседневном употреблении нужны тысячи. В 1980 году Государственное управление стандартов Китая выпустило таблицу символов GB2312, включающую 6763 иероглифа, что в основном удовлетворило потребности компьютерной обработки китайского текста.

    Однако GB2312 не умеет работать с некоторыми редкими иероглифами и традиционными формами письма. Таблица символов GBK представляет собой расширение GB2312 и в общей сложности содержит 21886 иероглифов. В схеме кодирования GBK символы ASCII представляются одним байтом, а китайские иероглифы - двумя байтами.

    3.4.3   Таблица символов Unicode

    -

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

    +

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

    Исследователи той эпохи задумались: если создать достаточно полную таблицу символов, которая включит все языки и знаки мира, разве это не решит проблемы многоязычной среды и искаженного текста? Под влиянием этой идеи и появилась большая и всеобъемлющая таблица символов Unicode.

    -

    Unicode по-китайски называется "единый код" и теоретически способен вместить более миллиона символов. Его цель - собрать символы со всего мира в единую таблицу символов, предоставить универсальный стандарт для обработки и отображения текстов на разных языках и уменьшить количество проблем с искажением текста, вызванных различиями стандартов кодирования. С момента публикации в 1991 году Unicode непрерывно расширялся, добавляя новые языки и символы. По состоянию на сентябрь 2022 года Unicode уже включал 149186 символов, в том числе буквы разных языков, знаки, а также эмодзи.

    -

    Как универсальный набор символов, Unicode по сути присваивает каждому символу уникальную "кодовую точку" (числовой идентификатор символа), диапазон которой составляет от U+0000 до U+10FFFF, образуя единое пространство нумерации символов. Однако Unicode не определяет, как именно хранить эти кодовые точки в компьютере. Тут неизбежно возникает вопрос: если в одном тексте одновременно встречаются кодовые точки Unicode разной длины, как система должна разбирать символы? Например, если дан код длиной 2 байта, как понять, является ли это одним 2-байтовым символом или двумя 1-байтовыми?

    -

    Для этой проблемы прямолинейное решение состоит в том, чтобы хранить все символы в кодировке одинаковой длины. Как показано на рисунке 3-7, каждый символ в "Hello" занимает 1 байт, а каждый символ в "алгоритм" занимает 2 байта. Мы можем дополнить старшие биты нулями и закодировать все символы в "Hello алгоритм" в виде 2-байтовых единиц. Тогда система сможет считывать по одному символу каждые 2 байта и восстановить эту фразу.

    +

    Unicode по-китайски называется «единый код» и теоретически способен вместить более миллиона символов. Его цель - собрать символы со всего мира в единую таблицу символов, предоставить универсальный стандарт для обработки и отображения текстов на разных языках и уменьшить количество проблем с искажением текста, вызванных различиями стандартов кодирования. С момента публикации в 1991 году Unicode непрерывно расширялся, добавляя новые языки и символы. По состоянию на сентябрь 2022 года Unicode уже включал 149186 символов, в том числе буквы разных языков, знаки, а также эмодзи.

    +

    Как универсальный набор символов, Unicode по сути присваивает каждому символу уникальную «кодовую точку» (числовой идентификатор символа), диапазон которой составляет от U+0000 до U+10FFFF, образуя единое пространство нумерации символов. Однако Unicode не определяет, как именно хранить эти кодовые точки в компьютере. Тут неизбежно возникает вопрос: если в одном тексте одновременно встречаются кодовые точки Unicode разной длины, как система должна разбирать символы? Например, если дан код длиной 2 байта, как понять, является ли это одним 2-байтовым символом или двумя 1-байтовыми?

    +

    Для этой проблемы прямолинейное решение состоит в том, чтобы хранить все символы в кодировке одинаковой длины. Как показано на рисунке 3-7, каждый символ в «Hello» занимает 1 байт, а каждый символ в «алгоритм» занимает 2 байта. Мы можем дополнить старшие биты нулями и закодировать все символы в «Hello алгоритм» в виде 2-байтовых единиц. Тогда система сможет считывать по одному символу каждые 2 байта и восстановить эту фразу.

    Пример кодирования Unicode

    Рисунок 3-7   Пример кодирования Unicode

    @@ -4449,9 +4449,9 @@

    Правила кодирования UTF-8 не слишком сложны и делятся на два случая.

    • Для символов длиной 1 байт старший бит устанавливается в \(0\) , а оставшиеся 7 битов содержат кодовую точку Unicode. Стоит отметить, что символы ASCII занимают первые 128 кодовых точек в наборе Unicode. Иными словами, кодировка UTF-8 обратно совместима с ASCII. Это означает, что мы можем использовать UTF-8 для разбора очень старых ASCII-текстов.
    • -
    • Для символов длиной \(n\) байт (где \(n > 1\)) старшие \(n\) битов первого байта устанавливаются в \(1\) , а \((n + 1)\)-й бит устанавливается в \(0\) ; начиная со второго байта, старшие 2 бита каждого байта устанавливаются в \(10\) ; все остальные биты используются для заполнения кодовой точки Unicode соответствующего символа.
    • +
    • Для символов длиной \(n\) байт (где \(n > 1\)) старшие \(n\) битов первого байта устанавливаются в \(1\) , а \((n + 1)\)-й бит устанавливается в \(0\). Начиная со второго байта, старшие 2 бита каждого байта устанавливаются в \(10\). Все остальные биты используются для заполнения кодовой точки Unicode соответствующего символа.
    -

    На рисунке 3-8 показана UTF-8-кодировка для строки "Hello алгоритм". Можно заметить, что поскольку старшие \(n\) битов установлены в \(1\) , система может определить длину символа как \(n\) , подсчитав число ведущих единиц.

    +

    На рисунке 3-8 показана UTF-8-кодировка для строки «Hello алгоритм». Можно заметить, что поскольку старшие \(n\) битов установлены в \(1\) , система может определить длину символа как \(n\) , подсчитав число ведущих единиц.

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

    Причина выбора \(10\) в качестве контрольного маркера в том, что по правилам UTF-8 символ не может иметь старшие два бита, равные \(10\) . Это можно доказать от противного: если предположить, что у некоторого символа старшие два бита равны \(10\) , то длина такого символа должна быть 1 байт, то есть это ASCII. Но у ASCII старший бит обязан быть \(0\) , что противоречит предположению.

    Пример кодировки UTF-8

    @@ -4459,10 +4459,10 @@

    Помимо UTF-8, распространены еще два следующих способа кодирования.

      -
    • Кодировка UTF-16: использует 2 или 4 байта для представления символа. Все символы ASCII и часто используемые неанглийские символы представляются 2 байтами; небольшая часть символов требует 4 байта. Для 2-байтовых символов кодировка UTF-16 совпадает с кодовой точкой Unicode.
    • +
    • Кодировка UTF-16: использует 2 или 4 байта для представления символа. Все символы ASCII и часто используемые неанглийские символы представляются 2 байтами. Небольшая часть символов требует 4 байта. Для 2-байтовых символов кодировка UTF-16 совпадает с кодовой точкой Unicode.
    • Кодировка UTF-32: каждый символ занимает 4 байта. Это означает, что UTF-32 требует больше места, чем UTF-8 и UTF-16, особенно в текстах с большой долей ASCII-символов.
    -

    С точки зрения занимаемого места UTF-8 очень эффективна для английских символов, потому что им нужен всего 1 байт; а для некоторых неанглийских символов (например китайских) UTF-16 может быть эффективнее, потому что ей требуется только 2 байта, тогда как UTF-8 может потребовать 3 байта.

    +

    С точки зрения занимаемого места UTF-8 очень эффективна для английских символов, потому что им нужен всего 1 байт. А для некоторых неанглийских символов (например китайских) UTF-16 может быть эффективнее, потому что ей требуется только 2 байта, тогда как UTF-8 может потребовать 3 байта.

    С точки зрения совместимости у UTF-8 наилучшая универсальность, и многие инструменты и библиотеки в первую очередь поддерживают именно UTF-8.

    3.4.5   Кодирование символов в языках программирования

    Для большинства языков программирования прошлого строки во время выполнения программы использовали фиксированные по длине кодировки, такие как UTF-16 или UTF-32. При кодировке фиксированной длины строку можно обрабатывать как массив, и такой подход дает следующие преимущества.

    @@ -4473,14 +4473,14 @@

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

      -
    • Тип String в Java использует кодировку UTF-16, и каждый символ занимает 2 байта. Это связано с тем, что на раннем этапе проектирования Java считалось, что 16 битов достаточно для представления всех возможных символов. Но это оказалось неверным предположением. Позднее Unicode вышел за пределы 16 битов, поэтому символы в Java теперь могут представляться парой 16-битных значений (так называемой "суррогатной парой").
    • +
    • Тип String в Java использует кодировку UTF-16, и каждый символ занимает 2 байта. Это связано с тем, что на раннем этапе проектирования Java считалось, что 16 битов достаточно для представления всех возможных символов. Но это оказалось неверным предположением. Позднее Unicode вышел за пределы 16 битов, поэтому символы в Java теперь могут представляться парой 16-битных значений (так называемой «суррогатной парой»).
    • Строки в JavaScript и TypeScript используют UTF-16 по причинам, похожим на Java. Когда Netscape впервые выпустила JavaScript в 1995 году, Unicode еще находился на ранней стадии развития, и 16-битного кодирования тогда было достаточно для представления всех символов Unicode.
    • C# использует UTF-16 главным образом потому, что платформа .NET была разработана Microsoft, а многие технологии Microsoft (включая Windows) широко используют именно UTF-16.
    -

    Из-за недооценки общего числа символов перечисленным выше языкам пришлось использовать "суррогатные пары" для представления Unicode-символов длиной больше 16 бит. Это вынужденный компромисс. С одной стороны, в строках с суррогатными парами один символ может занимать 2 байта или 4 байта, из-за чего теряется преимущество кодировки фиксированной длины. С другой стороны, обработка суррогатных пар требует дополнительного кода, что повышает сложность разработки и отладки.

    +

    Из-за недооценки общего числа символов перечисленным выше языкам пришлось использовать «суррогатные пары» для представления Unicode-символов длиной больше 16 бит. Это вынужденный компромисс. С одной стороны, в строках с суррогатными парами один символ может занимать 2 байта или 4 байта, из-за чего теряется преимущество кодировки фиксированной длины. С другой стороны, обработка суррогатных пар требует дополнительного кода, что повышает сложность разработки и отладки.

    По этим причинам некоторые языки программирования предложили иные схемы кодирования.

      -
    • str в Python использует Unicode и гибкое строковое представление, где длина хранимого символа зависит от наибольшей кодовой точки Unicode в строке. Если все символы строки принадлежат ASCII, каждый символ занимает 1 байт; если есть символы за пределами ASCII, но все они лежат в базовой многоязычной плоскости (BMP), каждый символ занимает 2 байта; если встречаются символы за пределами BMP, каждый символ занимает 4 байта.
    • +
    • str в Python использует Unicode и гибкое строковое представление, где длина хранимого символа зависит от наибольшей кодовой точки Unicode в строке. Если все символы строки принадлежат ASCII, каждый символ занимает 1 байт. Если есть символы за пределами ASCII, но все они лежат в базовой многоязычной плоскости (BMP), каждый символ занимает 2 байта. Если встречаются символы за пределами BMP, каждый символ занимает 4 байта.
    • Тип string в Go внутри использует кодировку UTF-8. Язык Go также предоставляет тип rune, предназначенный для представления одной кодовой точки Unicode.
    • Типы str и String в Rust внутри используют UTF-8. В Rust также есть тип char, представляющий одну кодовую точку Unicode.
    diff --git a/ru/chapter_data_structure/classification_of_data_structure/index.html b/ru/chapter_data_structure/classification_of_data_structure/index.html index e5d05baef..728fcb2b4 100644 --- a/ru/chapter_data_structure/classification_of_data_structure/index.html +++ b/ru/chapter_data_structure/classification_of_data_structure/index.html @@ -4359,16 +4359,16 @@

    3.1   Классификация структур данных

    К распространенным структурам данных относятся массивы, связные списки, стеки, очереди, хеш-таблицы, деревья, кучи и графы. Их можно классифицировать по двум измерениям: логической структуре и физической структуре.

    3.1.1   Логическая структура: линейная и нелинейная

    -

    Логическая структура раскрывает логические отношения между элементами данных. В массивах и связных списках данные расположены в определенном порядке, что отражает линейные отношения между элементами. В деревьях данные расположены по уровням сверху вниз, что демонстрирует отношения "предок" и "потомок". Графы состоят из вершин и ребер, отражая сложные сетевые отношения.

    +

    Логическая структура раскрывает логические отношения между элементами данных. В массивах и связных списках данные расположены в определенном порядке, что отражает линейные отношения между элементами. В деревьях данные расположены по уровням сверху вниз, что демонстрирует отношения «предок» и «потомок». Графы состоят из вершин и ребер, отражая сложные сетевые отношения.

    Как показано на рисунке 3-1, логические структуры делятся на две большие категории: линейные и нелинейные. Линейные структуры более интуитивны, поскольку в них данные расположены линейно и логически связаны. Нелинейные структуры, напротив, представляют собой нелинейное расположение элементов данных.

      -
    • Линейные структуры данных: массивы, связные списки, стеки, очереди, хеш-таблицы, в которых элементы связаны отношением "один к одному".
    • +
    • Линейные структуры данных: массивы, связные списки, стеки, очереди, хеш-таблицы, в которых элементы связаны отношением «один к одному».
    • Нелинейные структуры данных: деревья, кучи, графы, хеш-таблицы.

    Нелинейные структуры данных можно дополнительно разделить на древовидные и сетевые.

      -
    • Древовидные структуры: деревья, кучи, хеш-таблицы, в которых элементы связаны отношением "один ко многим".
    • -
    • Сетевые структуры: графы, в которых элементы связаны отношением "многие ко многим".
    • +
    • Древовидные структуры: деревья, кучи, хеш-таблицы, в которых элементы связаны отношением «один ко многим».
    • +
    • Сетевые структуры: графы, в которых элементы связаны отношением «многие ко многим».

    Линейные и нелинейные структуры данных

    Рисунок 3-1   Линейные и нелинейные структуры данных

    @@ -4381,19 +4381,19 @@

    Tip

    -

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

    +

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

    -

    Память - общий ресурс для всех программ. Когда некоторый участок памяти занят одной программой, другие программы обычно не могут использовать его одновременно. Поэтому при проектировании структур данных и алгоритмов память занимает важное место. Например, пиковое потребление памяти алгоритмом не должно превышать объем доступной свободной памяти системы; если не хватает непрерывных крупных участков памяти, выбранная структура данных должна уметь размещаться в разрозненных областях памяти.

    +

    Память - общий ресурс для всех программ. Когда некоторый участок памяти занят одной программой, другие программы обычно не могут использовать его одновременно. Поэтому при проектировании структур данных и алгоритмов память занимает важное место. Например, пиковое потребление памяти алгоритмом не должно превышать объем доступной свободной памяти системы. Если не хватает непрерывных крупных участков памяти, выбранная структура данных должна уметь размещаться в разрозненных областях памяти.

    Как показано на рисунке 3-3, физическая структура отражает способ хранения данных в памяти компьютера. Ее можно разделить на хранение в непрерывном пространстве (массивы) и хранение в разрозненном пространстве (связные списки). Физическая структура на низком уровне определяет способы доступа к данным, их обновления, вставки и удаления. Эти два типа физических структур взаимно дополняют друг друга по временной и пространственной эффективности.

    Хранение в непрерывном и разрозненном пространстве

    Рисунок 3-3   Хранение в непрерывном и разрозненном пространстве

    -

    Стоит отметить, что все структуры данных реализуются на основе массивов, связных списков или их комбинации. Например, стек и очередь можно реализовать как с помощью массивов, так и с помощью связных списков; реализация хеш-таблицы также может одновременно включать массивы и связные списки.

    +

    Стоит отметить, что все структуры данных реализуются на основе массивов, связных списков или их комбинации. Например, стек и очередь можно реализовать как с помощью массивов, так и с помощью связных списков. Реализация хеш-таблицы также может одновременно включать массивы и связные списки.

    • Можно реализовать на основе массивов: стеки, очереди, хеш-таблицы, деревья, кучи, графы, матрицы, тензоры (массивы размерности \(\geq 3\) ) и т.д.
    • Можно реализовать на основе связных списков: стеки, очереди, хеш-таблицы, деревья, кучи, графы и т.д.
    -

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

    +

    После инициализации длину связного списка все еще можно изменять во время выполнения программы, поэтому его также называют «динамической структурой данных». Длина массива после инициализации неизменна, поэтому его также называют «статической структурой данных». Стоит отметить, что массив может изменять длину за счет повторного выделения памяти, тем самым приобретая определенную «динамичность».

    Tip

    Если тебе пока трудно понять физическую структуру, рекомендуется сначала прочитать следующую главу, а затем вернуться к этому разделу.

    diff --git a/ru/chapter_data_structure/number_encoding/index.html b/ru/chapter_data_structure/number_encoding/index.html index b846d58d0..96f2c80ae 100644 --- a/ru/chapter_data_structure/number_encoding/index.html +++ b/ru/chapter_data_structure/number_encoding/index.html @@ -4363,11 +4363,11 @@

    3.3.1   Прямой, обратный и дополнительный коды

    В таблице из предыдущего раздела можно заметить, что все целочисленные типы могут представлять на одно отрицательное число больше, чем положительных. Например, диапазон byte равен \([-128, 127]\) . Это выглядит не слишком интуитивно, и внутренняя причина связана с прямым, обратным и дополнительным кодами.

    -

    Прежде всего нужно отметить, что числа хранятся в компьютере в виде "дополнительного кода". Прежде чем разбирать причины такого решения, сначала дадим определения всем трем способам представления.

    +

    Прежде всего нужно отметить, что числа хранятся в компьютере в виде «дополнительного кода». Прежде чем разбирать причины такого решения, сначала дадим определения всем трем способам представления.

      -
    • Прямой код: старший бит двоичного представления числа рассматривается как знаковый, где \(0\) означает положительное число, а \(1\) - отрицательное; остальные биты представляют значение числа.
    • -
    • Обратный код: для положительного числа обратный код совпадает с прямым; для отрицательного числа он получается инверсией всех битов прямого кода, кроме знакового бита.
    • -
    • Дополнительный код: для положительного числа дополнительный код совпадает с прямым; для отрицательного числа он получается добавлением \(1\) к его обратному коду.
    • +
    • Прямой код: старший бит двоичного представления числа рассматривается как знаковый, где \(0\) означает положительное число, а \(1\) - отрицательное. Остальные биты представляют значение числа.
    • +
    • Обратный код: для положительного числа обратный код совпадает с прямым. Для отрицательного числа он получается инверсией всех битов прямого кода, кроме знакового бита.
    • +
    • Дополнительный код: для положительного числа дополнительный код совпадает с прямым. Для отрицательного числа он получается добавлением \(1\) к его обратному коду.

    На рисунке 3-4 показаны способы преобразования между прямым, обратным и дополнительным кодами.

    Преобразования между прямым, обратным и дополнительным кодами

    @@ -4377,8 +4377,8 @@
    \[ \begin{aligned} & 1 + (-2) \newline -& \rightarrow 0000 \; 0001 + 1000 \; 0010 \newline -& = 1000 \; 0011 \newline +& \rightarrow 0000 \. 0001 + 1000 \. 0010 \newline +& = 1000 \. 0011 \newline & \rightarrow -3 \end{aligned} \]
    @@ -4386,45 +4386,45 @@
    \[ \begin{aligned} & 1 + (-2) \newline -& \rightarrow 0000 \; 0001 \; \text{(прямой код)} + 1000 \; 0010 \; \text{(прямой код)} \newline -& = 0000 \; 0001 \; \text{(обратный код)} + 1111 \; 1101 \; \text{(обратный код)} \newline -& = 1111 \; 1110 \; \text{(обратный код)} \newline -& = 1000 \; 0001 \; \text{(прямой код)} \newline +& \rightarrow 0000 \. 0001 \. \text{(прямой код)} + 1000 \. 0010 \. \text{(прямой код)} \newline +& = 0000 \. 0001 \. \text{(обратный код)} + 1111 \. 1101 \. \text{(обратный код)} \newline +& = 1111 \. 1110 \. \text{(обратный код)} \newline +& = 1000 \. 0001 \. \text{(прямой код)} \newline & \rightarrow -1 \end{aligned} \]

    С другой стороны, **в прямом коде у нуля есть два представления: \(+0\) и \(-0\) **. Это означает, что числу ноль соответствуют два разных двоичных кода, что может приводить к неоднозначности. Например, если в условном выражении не различать положительный и отрицательный ноль, можно получить ошибочный результат. А если специально обрабатывать такую неоднозначность, придется вводить дополнительные проверки, что может снизить вычислительную эффективность компьютера.

    \[ \begin{aligned} -+0 & \rightarrow 0000 \; 0000 \newline --0 & \rightarrow 1000 \; 0000 ++0 & \rightarrow 0000 \. 0000 \newline +-0 & \rightarrow 1000 \. 0000 \end{aligned} \]

    Как и прямой код, обратный код тоже страдает от неоднозначности положительного и отрицательного нуля, поэтому компьютеры ввели дополнительный код (2's complement). Сначала посмотрим на процесс преобразования отрицательного нуля из прямого кода в обратный, а затем в дополнительный:

    \[ \begin{aligned} --0 \rightarrow \; & 1000 \; 0000 \; \text{(прямой код)} \newline -= \; & 1111 \; 1111 \; \text{(обратный код)} \newline -= 1 \; & 0000 \; 0000 \; \text{(дополнительный код)} \newline +-0 \rightarrow \. & 1000 \. 0000 \. \text{(прямой код)} \newline += \. & 1111 \. 1111 \. \text{(обратный код)} \newline += 1 \. & 0000 \. 0000 \. \text{(дополнительный код)} \newline \end{aligned} \]
    -

    При добавлении \(1\) к обратному коду отрицательного нуля возникает перенос, но длина типа byte составляет всего 8 бит, поэтому переполнившаяся в 9-й бит единица отбрасывается. Иными словами, дополнительный код отрицательного нуля равен \(0000 \; 0000\) и совпадает с дополнительным кодом положительного нуля. Значит, в представлении дополнительного кода существует только один ноль, и проблема неоднозначности положительного и отрицательного нуля тем самым устраняется.

    +

    При добавлении \(1\) к обратному коду отрицательного нуля возникает перенос, но длина типа byte составляет всего 8 бит, поэтому переполнившаяся в 9-й бит единица отбрасывается. Иными словами, дополнительный код отрицательного нуля равен \(0000 \. 0000\) и совпадает с дополнительным кодом положительного нуля. Значит, в представлении дополнительного кода существует только один ноль, и проблема неоднозначности положительного и отрицательного нуля тем самым устраняется.

    Остается последний вопрос: диапазон типа byte равен \([-128, 127]\) , откуда берется лишнее отрицательное число \(-128\) ? Мы замечаем, что у всех целых чисел из интервала \([-127, +127]\) есть соответствующие прямой, обратный и дополнительный коды, а прямой и дополнительный коды можно преобразовывать друг в друга.

    -

    Однако дополнительный код \(1000 \; 0000\) является исключением: у него нет соответствующего прямого кода. Согласно правилу преобразования, прямой код для этого дополнительного кода должен быть равен \(0000 \; 0000\) . Это очевидное противоречие, потому что такой прямой код обозначает число \(0\) , а его дополнительный код должен совпадать с ним самим. Компьютер просто определяет, что этот особый дополнительный код \(1000 \; 0000\) представляет число \(-128\) . На самом деле результат вычисления \((-1) + (-127)\) в дополнительном коде как раз и равен \(-128\) .

    +

    Однако дополнительный код \(1000 \. 0000\) является исключением: у него нет соответствующего прямого кода. Согласно правилу преобразования, прямой код для этого дополнительного кода должен быть равен \(0000 \. 0000\) . Это очевидное противоречие, потому что такой прямой код обозначает число \(0\) , а его дополнительный код должен совпадать с ним самим. Компьютер просто определяет, что этот особый дополнительный код \(1000 \. 0000\) представляет число \(-128\) . На самом деле результат вычисления \((-1) + (-127)\) в дополнительном коде как раз и равен \(-128\) .

    \[ \begin{aligned} & (-127) + (-1) \newline -& \rightarrow 1111 \; 1111 \; \text{(прямой код)} + 1000 \; 0001 \; \text{(прямой код)} \newline -& = 1000 \; 0000 \; \text{(обратный код)} + 1111 \; 1110 \; \text{(обратный код)} \newline -& = 1000 \; 0001 \; \text{(дополнительный код)} + 1111 \; 1111 \; \text{(дополнительный код)} \newline -& = 1000 \; 0000 \; \text{(дополнительный код)} \newline +& \rightarrow 1111 \. 1111 \. \text{(прямой код)} + 1000 \. 0001 \. \text{(прямой код)} \newline +& = 1000 \. 0000 \. \text{(обратный код)} + 1111 \. 1110 \. \text{(обратный код)} \newline +& = 1000 \. 0001 \. \text{(дополнительный код)} + 1111 \. 1111 \. \text{(дополнительный код)} \newline +& = 1000 \. 0000 \. \text{(дополнительный код)} \newline & \rightarrow -128 \end{aligned} \]

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

    -

    Обрати внимание: это не означает, что компьютер умеет только складывать. Комбинируя сложение с некоторыми базовыми логическими операциями, компьютер может реализовать и другие математические операции. Например, вычитание \(a - b\) можно преобразовать в сложение \(a + (-b)\) ; умножение и деление можно свести к многократному сложению или вычитанию.

    +

    Обрати внимание: это не означает, что компьютер умеет только складывать. Комбинируя сложение с некоторыми базовыми логическими операциями, компьютер может реализовать и другие математические операции. Например, вычитание \(a - b\) можно преобразовать в сложение \(a + (-b)\). Умножение и деление можно свести к многократному сложению или вычитанию.

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

    -

    Идея дополнительного кода очень изящна; из-за ограничений по объему мы на этом остановимся. Если тебе интересно, стоит изучить эту тему глубже.

    +

    Идея дополнительного кода очень изящна. Из-за ограничений по объему мы на этом остановимся. Если тебе интересно, стоит изучить эту тему глубже.

    3.3.2   Кодирование чисел с плавающей точкой

    Внимательный читатель может заметить: int и float имеют одинаковую длину, по 4 байта , но почему диапазон значений у float намного больше, чем у int ? Это выглядит парадоксально, ведь float должен еще представлять дробные числа, а значит диапазон вроде бы должен быть меньше.

    На самом деле это связано с тем, что число с плавающей точкой float использует другой способ представления. Обозначим двоичное число длиной 32 бита как:

    @@ -4455,12 +4455,12 @@ b_{31} b_{30} b_{29} \ldots b_2 b_1 b_0

    Пример вычисления float по стандарту IEEE 754

    Рисунок 3-5   Пример вычисления float по стандарту IEEE 754

    -

    Посмотрим на рисунок 3-5: если взять пример \(\mathrm{S} = 0\) , \(\mathrm{E} = 124\) , \(\mathrm{N} = 2^{-2} + 2^{-3} = 0.375\) , то получим:

    +

    Как видно на рисунке 3-5, если взять пример \(\mathrm{S} = 0\) , \(\mathrm{E} = 124\) , \(\mathrm{N} = 2^{-2} + 2^{-3} = 0.375\) , то получим:

    \[ \text { val } = (-1)^0 \times 2^{124 - 127} \times (1 + 0.375) = 0.171875 \]
    -

    Теперь мы можем ответить на исходный вопрос: в представлении float присутствуют биты экспоненты, поэтому его диапазон значений намного больше, чем у int. Согласно приведенным выше вычислениям, максимально возможное положительное число для float равно \(2^{254 - 127} \times (2 - 2^{-23}) \approx 3.4 \times 10^{38}\) ; если изменить бит знака, получим минимальное отрицательное число.

    -

    Хотя число с плавающей точкой float расширяет диапазон значений, побочным эффектом становится потеря точности. Целочисленный тип int использует все 32 бита для представления числа, и числа распределены равномерно; а из-за существования битов экспоненты у float чем больше число, тем больше обычно становится разница между двумя соседними представимыми значениями.

    +

    Теперь мы можем ответить на исходный вопрос: в представлении float присутствуют биты экспоненты, поэтому его диапазон значений намного больше, чем у int. Согласно приведенным выше вычислениям, максимально возможное положительное число для float равно \(2^{254 - 127} \times (2 - 2^{-23}) \approx 3.4 \times 10^{38}\). Если изменить бит знака, получим минимальное отрицательное число.

    +

    Хотя число с плавающей точкой float расширяет диапазон значений, побочным эффектом становится потеря точности. Целочисленный тип int использует все 32 бита для представления числа, и числа распределены равномерно. А из-за существования битов экспоненты у float чем больше число, тем больше обычно становится разница между двумя соседними представимыми значениями.

    Как показано в таблице 3-2, значения экспоненты \(\mathrm{E} = 0\) и \(\mathrm{E} = 255\) имеют специальный смысл и используются для представления нуля, бесконечности, \(\mathrm{NaN}\) и т.д.

    Таблица 3-2   Значение поля экспоненты

    diff --git a/ru/chapter_data_structure/summary/index.html b/ru/chapter_data_structure/summary/index.html index 9ceaa4db8..9378d83c5 100644 --- a/ru/chapter_data_structure/summary/index.html +++ b/ru/chapter_data_structure/summary/index.html @@ -4372,32 +4372,32 @@

    2.   Q & A

    Q: Почему хеш-таблица одновременно включает линейные и нелинейные структуры данных?

    -

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

    +

    В основе хеш-таблицы лежит массив, а для разрешения коллизий мы можем использовать «цепочки адресации» (об этом будет рассказано в последующем разделе «Хеш-коллизии»): каждый бакет массива указывает на связный список, а если длина списка превышает некоторый порог, он может быть преобразован в дерево (обычно в красно-черное дерево).

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

    Q: Длина типа char равна 1 байту?

    Длина типа char определяется используемым в языке программирования способом кодирования. Например, Java, JavaScript, TypeScript и C# используют кодировку UTF-16 (для хранения кодовых точек Unicode), поэтому длина char у них равна 2 байтам.

    -

    Q: Не является ли двусмысленным утверждение, что структуры данных, реализованные на основе массива, также называются "статическими структурами данных"? Ведь стек тоже поддерживает операции push и pop, а они явно "динамические".

    -

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

    -

    Q: При построении стека (очереди) его размер не задается явно, почему же его относят к "статическим структурам данных"?

    -

    В языках высокого уровня нам не нужно вручную задавать начальную емкость стека (очереди): это автоматически делает сама реализация класса. Например, начальная емкость ArrayList в Java обычно равна 10. Кроме того, автоматом реализуется и расширение емкости. Подробнее это рассматривается в последующем разделе о "списках".

    -

    Q: Если метод преобразования из прямого кода в дополнительный - это "сначала инвертировать, затем прибавить 1", то обратное преобразование из дополнительного кода в прямой, по идее, должно быть обратной операцией "сначала вычесть 1, затем инвертировать". Почему же дополнительный код также можно перевести в прямой тем же способом "сначала инвертировать, затем прибавить 1"?

    -

    Это связано с тем, что взаимное преобразование прямого и дополнительного кодов по сути является вычислением "дополнения". Сначала дадим определение дополнения: если \(a + b = c\) , то говорят, что \(a\) является дополнением числа \(b\) до \(c\) ; аналогично, \(b\) является дополнением числа \(a\) до \(c\) .

    -

    Для двоичного числа длины \(n = 4\) со значением \(0010\) , если рассматривать его как прямой код (не учитывая знаковый бит), то его дополнительный код получается правилом "сначала инвертировать, затем прибавить 1":

    +

    Q: Не является ли двусмысленным утверждение, что структуры данных, реализованные на основе массива, также называются «статическими структурами данных»? Ведь стек тоже поддерживает операции push и pop, а они явно «динамические».

    +

    Стек действительно может поддерживать динамические операции над данными, но сама структура данных при этом остается «статической» (ее длина неизменна). Хотя структуры на основе массива могут динамически добавлять и удалять элементы, их емкость фиксирована. Если количество данных превышает заранее выделенный размер, приходится создавать новый, более крупный массив и копировать в него содержимое старого.

    +

    Q: При построении стека (очереди) его размер не задается явно, почему же его относят к «статическим структурам данных»?

    +

    В языках высокого уровня нам не нужно вручную задавать начальную емкость стека (очереди): это автоматически делает сама реализация класса. Например, начальная емкость ArrayList в Java обычно равна 10. Кроме того, автоматом реализуется и расширение емкости. Подробнее это рассматривается в последующем разделе о «списках».

    +

    Q: Если метод преобразования из прямого кода в дополнительный - это «сначала инвертировать, затем прибавить 1», то обратное преобразование из дополнительного кода в прямой, по идее, должно быть обратной операцией «сначала вычесть 1, затем инвертировать». Почему же дополнительный код также можно перевести в прямой тем же способом «сначала инвертировать, затем прибавить 1»?

    +

    Это связано с тем, что взаимное преобразование прямого и дополнительного кодов по сути является вычислением «дополнения». Сначала дадим определение дополнения: если \(a + b = c\) , то говорят, что \(a\) является дополнением числа \(b\) до \(c\). Аналогично, \(b\) является дополнением числа \(a\) до \(c\) .

    +

    Для двоичного числа длины \(n = 4\) со значением \(0010\) , если рассматривать его как прямой код (не учитывая знаковый бит), то его дополнительный код получается правилом «сначала инвертировать, затем прибавить 1»:

    \[ 0010 \rightarrow 1101 \rightarrow 1110 \]
    -

    Мы видим, что сумма прямого и дополнительного кодов равна \(0010 + 1110 = 10000\) , то есть дополнительный код \(1110\) является "дополнением" прямого кода \(0010\) до \(10000\) . **Это означает, что описанная выше операция "сначала инвертировать, затем прибавить 1" на самом деле вычисляет дополнение до \(10000\) **.

    -

    Тогда чему равно "дополнение" дополнительного кода \(1110\) до \(10000\) ? Мы снова можем получить его правилом "сначала инвертировать, затем прибавить 1":

    +

    Мы видим, что сумма прямого и дополнительного кодов равна \(0010 + 1110 = 10000\) , то есть дополнительный код \(1110\) является «дополнением» прямого кода \(0010\) до \(10000\) . **Это означает, что описанная выше операция «сначала инвертировать, затем прибавить 1» на самом деле вычисляет дополнение до \(10000\) **.

    +

    Тогда чему равно «дополнение» дополнительного кода \(1110\) до \(10000\) ? Мы снова можем получить его правилом «сначала инвертировать, затем прибавить 1»:

    \[ 1110 \rightarrow 0001 \rightarrow 0010 \]
    -

    Иначе говоря, прямой и дополнительный коды являются взаимными "дополнениями" друг друга до \(10000\) , поэтому и "прямой код -> дополнительный код", и "дополнительный код -> прямой код" можно реализовать одной и той же операцией (сначала инвертировать, затем прибавить 1).

    -

    Разумеется, можно получить прямой код из дополнительного кода \(1110\) и обратной операцией, то есть "сначала вычесть 1, затем инвертировать":

    +

    Иначе говоря, прямой и дополнительный коды являются взаимными «дополнениями» друг друга до \(10000\) , поэтому и «прямой код -> дополнительный код», и «дополнительный код -> прямой код» можно реализовать одной и той же операцией (сначала инвертировать, затем прибавить 1).

    +

    Разумеется, можно получить прямой код из дополнительного кода \(1110\) и обратной операцией, то есть «сначала вычесть 1, затем инвертировать»:

    \[ 1110 \rightarrow 1101 \rightarrow 0010 \]
    -

    В итоге и "сначала инвертировать, затем прибавить 1", и "сначала вычесть 1, затем инвертировать" - это два эквивалентных способа вычисления дополнения до \(10000\) .

    -

    По сути операция "инвертировать" сама по себе вычисляет дополнение до \(1111\) (потому что всегда выполняется прямой код + обратный код = 1111 ); а дополнительный код, получающийся после добавления 1 к обратному коду, и есть дополнение до \(10000\) .

    +

    В итоге и «сначала инвертировать, затем прибавить 1», и «сначала вычесть 1, затем инвертировать» - это два эквивалентных способа вычисления дополнения до \(10000\) .

    +

    По сути операция «инвертировать» сама по себе вычисляет дополнение до \(1111\) (потому что всегда выполняется прямой код + обратный код = 1111 ). А дополнительный код, получающийся после добавления 1 к обратному коду, и есть дополнение до \(10000\) .

    Приведенный выше пример использовал \(n = 4\) , но его можно обобщить на двоичные числа любой длины.

    diff --git a/ru/chapter_divide_and_conquer/binary_search_recur/index.html b/ru/chapter_divide_and_conquer/binary_search_recur/index.html index d8a9d34f5..f2bfc9702 100644 --- a/ru/chapter_divide_and_conquer/binary_search_recur/index.html +++ b/ru/chapter_divide_and_conquer/binary_search_recur/index.html @@ -3202,7 +3202,7 @@ - 1.   Реализация двоичного поиска на основе "разделяй и властвуй" + 1.   Реализация двоичного поиска на основе «разделяй и властвуй» @@ -4290,7 +4290,7 @@ - 1.   Реализация двоичного поиска на основе "разделяй и властвуй" + 1.   Реализация двоичного поиска на основе «разделяй и властвуй» @@ -4340,32 +4340,32 @@
  • Полный перебор: реализуется через обход структуры данных, временная сложность равна \(O(n)\) .
  • Адаптивный поиск: использует особую организацию данных или априорную информацию, временная сложность может достигать \(O(\log n)\) и даже \(O(1)\) .
  • -

    На практике алгоритмы поиска с временной сложностью \(O(\log n)\) обычно реализуются на основе стратегии "разделяй и властвуй", например двоичный поиск и деревья.

    +

    На практике алгоритмы поиска с временной сложностью \(O(\log n)\) обычно реализуются на основе стратегии «разделяй и властвуй», например двоичный поиск и деревья.

    • На каждом шаге двоичный поиск раскладывает задачу (поиск целевого элемента в массиве) на более мелкую задачу (поиск целевого элемента в одной половине массива), и этот процесс продолжается, пока массив не станет пустым или пока не будет найден целевой элемент.
    • -
    • Деревья являются типичными представителями идей "разделяй и властвуй"; в таких структурах данных, как двоичное дерево поиска, AVL-дерево и куча, временная сложность различных операций равна \(O(\log n)\) .
    • +
    • Деревья являются типичными представителями идей «разделяй и властвуй». В таких структурах данных, как двоичное дерево поиска, AVL-дерево и куча, временная сложность различных операций равна \(O(\log n)\) .
    -

    Стратегия "разделяй и властвуй" для двоичного поиска выглядит следующим образом.

    +

    Стратегия «разделяй и властвуй» для двоичного поиска выглядит следующим образом.

    • Задача раскладывается на части: двоичный поиск рекурсивно разбивает исходную задачу (поиск в массиве) на подзадачу (поиск в одной половине массива), и это достигается сравнением среднего элемента с целевым значением.
    • Подзадачи независимы: в двоичном поиске на каждом шаге обрабатывается только одна подзадача, и она не зависит от других подзадач.
    • Решения подзадач не нужно объединять: двоичный поиск нацелен на поиск конкретного элемента, поэтому объединять решения подзадач не требуется. Как только подзадача решена, одновременно считается решенной и исходная задача.
    -

    Иными словами, стратегия "разделяй и властвуй" повышает эффективность поиска потому, что при полном переборе за один шаг удается исключить только один вариант, тогда как при поиске на основе "разделяй и властвуй" за один шаг можно исключить половину вариантов.

    -

    1.   Реализация двоичного поиска на основе "разделяй и властвуй"

    -

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

    +

    Иными словами, стратегия «разделяй и властвуй» повышает эффективность поиска потому, что при полном переборе за один шаг удается исключить только один вариант, тогда как при поиске на основе «разделяй и властвуй» за один шаг можно исключить половину вариантов.

    +

    1.   Реализация двоичного поиска на основе «разделяй и властвуй»

    +

    В предыдущих главах двоичный поиск реализовывался через итерацию. Теперь реализуем его с помощью стратегии «разделяй и властвуй», то есть через рекурсию.

    Question

    Дан отсортированный массив nums длины \(n\) , в котором все элементы уникальны. Найдите элемент target .

    -

    С точки зрения стратегии "разделяй и властвуй" обозначим подзадачу, соответствующую интервалу поиска \([i, j]\) , через \(f(i, j)\) .

    +

    С точки зрения стратегии «разделяй и властвуй» обозначим подзадачу, соответствующую интервалу поиска \([i, j]\) , через \(f(i, j)\) .

    Начиная с исходной задачи \(f(0, n-1)\) , выполняем двоичный поиск по следующим шагам.

    1. Вычислить середину \(m\) интервала поиска \([i, j]\) и с ее помощью исключить половину интервала.
    2. -
    3. Рекурсивно решить подзадачу вдвое меньшего размера; это может быть либо \(f(i, m-1)\) , либо \(f(m+1, j)\) .
    4. +
    5. Рекурсивно решить подзадачу вдвое меньшего размера. Это может быть либо \(f(i, m-1)\) , либо \(f(m+1, j)\) .
    6. Повторять шаг 1. и шаг 2. , пока не будет найден target или пока интервал не станет пустым.
    -

    На рисунке 12-4 показан процесс применения стратегии "разделяй и властвуй" для поиска элемента \(6\) в массиве.

    +

    На рисунке 12-4 показан процесс применения стратегии «разделяй и властвуй» для поиска элемента \(6\) в массиве.

    Процесс двоичного поиска в стиле разделяй и властвуй

    Рисунок 12-4   Процесс двоичного поиска в стиле разделяй и властвуй

    diff --git a/ru/chapter_divide_and_conquer/build_binary_tree_problem/index.html b/ru/chapter_divide_and_conquer/build_binary_tree_problem/index.html index 2ac3acaec..2982d6d0b 100644 --- a/ru/chapter_divide_and_conquer/build_binary_tree_problem/index.html +++ b/ru/chapter_divide_and_conquer/build_binary_tree_problem/index.html @@ -3230,7 +3230,7 @@ - 1.   Проверка, является ли это задачей "разделяй и властвуй" + 1.   Проверка, является ли это задачей «разделяй и властвуй» @@ -4323,7 +4323,7 @@ - 1.   Проверка, является ли это задачей "разделяй и властвуй" + 1.   Проверка, является ли это задачей «разделяй и властвуй» @@ -4408,24 +4408,24 @@

    Пример данных для построения двоичного дерева

    Рисунок 12-5   Пример данных для построения двоичного дерева

    -

    1.   Проверка, является ли это задачей "разделяй и властвуй"

    -

    Исходная задача - построить двоичное дерево по preorder и inorder - является типичной задачей для стратегии "разделяй и властвуй".

    +

    1.   Проверка, является ли это задачей «разделяй и властвуй»

    +

    Исходная задача - построить двоичное дерево по preorder и inorder - является типичной задачей для стратегии «разделяй и властвуй».

      -
    • Задача раскладывается на части: если смотреть с точки зрения стратегии "разделяй и властвуй", исходную задачу можно разбить на две подзадачи: построение левого поддерева и построение правого поддерева, плюс одно действие: инициализация корневого узла. Для каждого поддерева (подзадачи) можно использовать тот же способ разбиения, пока не будет достигнута наименьшая подзадача (пустое поддерево).
    • +
    • Задача раскладывается на части: если смотреть с точки зрения стратегии «разделяй и властвуй», исходную задачу можно разбить на две подзадачи: построение левого поддерева и построение правого поддерева, плюс одно действие: инициализация корневого узла. Для каждого поддерева (подзадачи) можно использовать тот же способ разбиения, пока не будет достигнута наименьшая подзадача (пустое поддерево).
    • Подзадачи независимы: левое и правое поддеревья независимы друг от друга и не пересекаются. При построении левого поддерева нам нужно смотреть только на ту часть прямого и симметричного обходов, которая соответствует левому поддереву. Для правого поддерева рассуждение аналогично.
    • Решения подзадач можно объединить: когда левое и правое поддеревья (решения подзадач) уже построены, их можно присоединить к корневому узлу и тем самым получить решение исходной задачи.

    2.   Как разделить поддеревья

    -

    Из анализа выше видно, что эта задача действительно решается через "разделяй и властвуй", но как именно, имея прямой обход preorder и симметричный обход inorder, отделить левое и правое поддеревья?

    +

    Из анализа выше видно, что эта задача действительно решается через «разделяй и властвуй», но как именно, имея прямой обход preorder и симметричный обход inorder, отделить левое и правое поддеревья?

    По определению и preorder , и inorder можно разбить на три части.

    • Прямой обход: [ корневой узел | левое поддерево | правое поддерево ] , например для дерева на рисунке 12-5 это [ 3 | 9 | 2 1 7 ] .
    • Симметричный обход: [ левое поддерево | корневой узел | правое поддерево ] , например для дерева на рисунке 12-5 это [ 9 | 3 | 1 2 7 ] .
    -

    На примере данных с рисунка можно получить результат разбиения по следующим шагам.

    +

    На примере данных на рисунке 12-5 разбиение можно выполнить по шагам, показанным на рисунке 12-6.

    1. Первый элемент прямого обхода, равный 3, является значением корневого узла.
    2. -
    3. Найти индекс корневого узла 3 в inorder ; используя этот индекс, можно разбить inorder на [ 9 | 3 | 1 2 7 ] .
    4. +
    5. Найти индекс корневого узла 3 в inorder. Используя этот индекс, можно разбить inorder на [ 9 | 3 | 1 2 7 ] .
    6. По результату разбиения inorder нетрудно определить, что число узлов в левом и правом поддеревьях равно 1 и 3 соответственно, а значит, preorder можно разбить как [ 3 | 9 | 2 1 7 ] .

    Разбиение поддеревьев в прямом и симметричном обходах

    @@ -4469,7 +4469,7 @@
    -

    Стоит отметить, что \((m-l)\) в индексе корневого узла правого поддерева означает число узлов в левом поддереве; лучше всего понимать это выражение вместе с рисунком ниже.

    +

    Стоит отметить, что \((m-l)\) в индексе корневого узла правого поддерева означает число узлов в левом поддереве. Лучше всего понять это выражение, сопоставив его с тем, что показано на рисунке 12-7.

    Представление индексных интервалов корня и поддеревьев

    Рисунок 12-7   Представление индексных интервалов корня и поддеревьев

    @@ -4899,7 +4899,7 @@

    -

    На рисунке 12-8 показан рекурсивный процесс построения двоичного дерева: каждый узел создается в фазе "спуска", а каждое ребро (ссылка) формируется в фазе "подъема".

    +

    На рисунке 12-8 показан рекурсивный процесс построения двоичного дерева: каждый узел создается в фазе «спуска», а каждое ребро (ссылка) формируется в фазе «подъема».

    @@ -4937,7 +4937,7 @@

    Результаты разбиения в каждом рекурсивном вызове

    Рисунок 12-9   Результаты разбиения в каждом рекурсивном вызове

    -

    Пусть число узлов дерева равно \(n\) ; инициализация каждого узла (то есть выполнение одного рекурсивного вызова dfs() ) занимает \(O(1)\) времени. Следовательно, общая временная сложность равна \(O(n)\) .

    +

    Пусть число узлов дерева равно \(n\). Инициализация каждого узла (то есть выполнение одного рекурсивного вызова dfs() ) занимает \(O(1)\) времени. Следовательно, общая временная сложность равна \(O(n)\) .

    Хеш-таблица хранит отображение значений inorder в индексы, поэтому ее пространственная сложность равна \(O(n)\) . В худшем случае, когда двоичное дерево вырождается в связный список, глубина рекурсии достигает \(n\) и требует \(O(n)\) памяти стека. Следовательно, общая пространственная сложность также равна \(O(n)\) .

    diff --git a/ru/chapter_divide_and_conquer/divide_and_conquer/index.html b/ru/chapter_divide_and_conquer/divide_and_conquer/index.html index 67904c789..198388d41 100644 --- a/ru/chapter_divide_and_conquer/divide_and_conquer/index.html +++ b/ru/chapter_divide_and_conquer/divide_and_conquer/index.html @@ -3174,7 +3174,7 @@ - 12.1.1   Как определить задачу "разделяй и властвуй" + 12.1.1   Как определить задачу «разделяй и властвуй» @@ -3185,12 +3185,12 @@ - 12.1.2   Повышение эффективности с помощью "разделяй и властвуй" + 12.1.2   Повышение эффективности с помощью «разделяй и властвуй» -
    -

    Это означает, что при \(n > 4\) число операций после разбиения становится меньше, а значит, сортировка должна работать быстрее. При этом важно заметить, что временная сложность после разбиения все еще остается квадратичной, то есть \(O(n^2)\) ; уменьшается лишь константный множитель.

    -

    Если пойти дальше и продолжать делить каждый подмассив пополам, пока в нем не останется только один элемент, то мы фактически получим "сортировку слиянием", чья временная сложность равна \(O(n \log n)\) .

    +

    Это означает, что при \(n > 4\) число операций после разбиения становится меньше, а значит, сортировка должна работать быстрее. При этом важно заметить, что временная сложность после разбиения все еще остается квадратичной, то есть \(O(n^2)\). Уменьшается лишь константный множитель.

    +

    Если пойти дальше и продолжать делить каждый подмассив пополам, пока в нем не останется только один элемент, то мы фактически получим «сортировку слиянием», чья временная сложность равна \(O(n \log n)\) .

    Можно пойти еще дальше и спросить: что если задать несколько точек разделения и равномерно разбить исходный массив на \(k\) подмассивов? Такая ситуация очень похожа на блочную сортировку, которая особенно хорошо подходит для сортировки очень больших объемов данных и теоретически может достигать временной сложности \(O(n + k)\) .

    2.   Оптимизация параллельных вычислений

    -

    Мы знаем, что подзадачи, порождаемые стратегией "разделяй и властвуй", являются независимыми, а значит, их обычно можно решать параллельно. Иначе говоря, "разделяй и властвуй" не только может уменьшить временную сложность алгоритма, но и хорошо сочетается с параллельной оптимизацией на уровне системы.

    +

    Мы знаем, что подзадачи, порождаемые стратегией «разделяй и властвуй», являются независимыми, а значит, их обычно можно решать параллельно. Иначе говоря, «разделяй и властвуй» не только может уменьшить временную сложность алгоритма, но и хорошо сочетается с параллельной оптимизацией на уровне системы.

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

    -

    Например, в показанной ниже "блочной сортировке" большой объем данных равномерно распределяется по блокам. Тогда сортировку каждого блока можно поручить отдельным вычислительным единицам, а после завершения просто объединить результаты.

    +

    Например, в «блочной сортировке», показанной на рисунке 12-3, большой объем данных равномерно распределяется по блокам. Тогда сортировку каждого блока можно поручить отдельным вычислительным единицам, а после завершения просто объединить результаты.

    Параллельные вычисления в блочной сортировке

    Рисунок 12-3   Параллельные вычисления в блочной сортировке

    -

    12.1.3   Типичные применения стратегии "разделяй и властвуй"

    -

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

    +

    12.1.3   Типичные применения стратегии «разделяй и властвуй»

    +

    С одной стороны, стратегию «разделяй и властвуй» можно использовать для решения многих классических алгоритмических задач.

    • Поиск ближайшей пары точек: сначала множество точек делится на две части, затем ищется ближайшая пара в каждой части, а затем ближайшая пара, пересекающая границу между двумя частями.
    • Умножение больших чисел: например, алгоритм Карацубы, который раскладывает умножение больших чисел на несколько умножений и сложений меньших чисел.
    • Умножение матриц: например, алгоритм Штрассена, который раскладывает умножение больших матриц на несколько умножений и сложений матриц меньшего размера.
    • -
    • Задача о Ханойской башне: задача о Ханойской башне решается рекурсивно и является типичным примером применения стратегии "разделяй и властвуй".
    • -
    • Подсчет инверсий: если в последовательности предыдущее число больше следующего, то такая пара образует инверсию. Эту задачу можно решить с помощью идей "разделяй и властвуй", опираясь на сортировку слиянием.
    • +
    • Задача о Ханойской башне: задача о Ханойской башне решается рекурсивно и является типичным примером применения стратегии «разделяй и властвуй».
    • +
    • Подсчет инверсий: если в последовательности предыдущее число больше следующего, то такая пара образует инверсию. Эту задачу можно решить с помощью идей «разделяй и властвуй», опираясь на сортировку слиянием.
    -

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

    +

    С другой стороны, стратегия «разделяй и властвуй» очень широко применяется при проектировании алгоритмов и структур данных.

    • Двоичный поиск: двоичный поиск делит отсортированный массив на две части по индексу середины, а затем, в зависимости от результата сравнения целевого значения со средним элементом, исключает одну из половин и повторяет ту же операцию на оставшемся интервале.
    • Сортировка слиянием: она уже была рассмотрена в начале этого раздела, поэтому не будем повторяться.
    • Быстрая сортировка: в ней выбирается опорное значение, после чего массив делится на два подмассива: один содержит элементы меньше опорного, а другой - больше. Затем такая же операция повторяется для обеих частей, пока в подмассиве не останется один элемент.
    • Блочная сортировка: ее основная идея заключается в распределении данных по нескольким блокам, сортировке элементов внутри каждого блока и последующем последовательном извлечении элементов из блоков для построения отсортированного массива.
    • -
    • Деревья: например, двоичные деревья поиска, AVL-деревья, красно-черные деревья, B-деревья, B+ деревья и т.д. Их операции поиска, вставки и удаления можно рассматривать как применение стратегии "разделяй и властвуй".
    • -
    • Кучи: куча является особым видом полного двоичного дерева, а такие операции, как вставка, удаление и упорядочивание, по сути содержат идеи "разделяй и властвуй".
    • -
    • Хеш-таблицы: хотя хеш-таблицы напрямую не используют стратегию "разделяй и властвуй", некоторые способы разрешения коллизий косвенно опираются на эту идею. Например, длинные цепочки в методе цепочек могут преобразовываться в красно-черные деревья для повышения эффективности поиска.
    • +
    • Деревья: например, двоичные деревья поиска, AVL-деревья, красно-черные деревья, B-деревья, B+ деревья и т.д. Их операции поиска, вставки и удаления можно рассматривать как применение стратегии «разделяй и властвуй».
    • +
    • Кучи: куча является особым видом полного двоичного дерева, а такие операции, как вставка, удаление и упорядочивание, по сути содержат идеи «разделяй и властвуй».
    • +
    • Хеш-таблицы: хотя хеш-таблицы напрямую не используют стратегию «разделяй и властвуй», некоторые способы разрешения коллизий косвенно опираются на эту идею. Например, длинные цепочки в методе цепочек могут преобразовываться в красно-черные деревья для повышения эффективности поиска.
    -

    Нетрудно заметить, что "разделяй и властвуй" - это "тихая" алгоритмическая идея, скрыто присутствующая внутри самых разных алгоритмов и структур данных.

    +

    Нетрудно заметить, что «разделяй и властвуй» - это «тихая» алгоритмическая идея, скрыто присутствующая внутри самых разных алгоритмов и структур данных.

    diff --git a/ru/chapter_divide_and_conquer/hanota_problem/index.html b/ru/chapter_divide_and_conquer/hanota_problem/index.html index 330bf77ff..7a2298f35 100644 --- a/ru/chapter_divide_and_conquer/hanota_problem/index.html +++ b/ru/chapter_divide_and_conquer/hanota_problem/index.html @@ -4434,7 +4434,7 @@

    Процесс решения задачи \(f(2)\) можно кратко описать так: переместить два диска с A на C с помощью B . Здесь C называется целевым стержнем, а B - буферным стержнем.

    2.   Разбиение на подзадачи

    Для задачи \(f(3)\) , то есть когда имеется три диска, ситуация становится сложнее.

    -

    Поскольку решения \(f(1)\) и \(f(2)\) уже известны, можно подойти к задаче с точки зрения стратегии "разделяй и властвуй" и рассматривать два верхних диска на A как единое целое, выполняя шаги, показанные на рисунке 12-13. Так три диска успешно перемещаются с A на C .

    +

    Поскольку решения \(f(1)\) и \(f(2)\) уже известны, можно подойти к задаче с точки зрения стратегии «разделяй и властвуй» и рассматривать два верхних диска на A как единое целое, выполняя шаги, показанные на рисунке 12-13. Так три диска успешно перемещаются с A на C .

    1. Сделать B целевым стержнем, а C буферным, и переместить два диска с A на B .
    2. Переместить оставшийся один диск с A напрямую на C .
    3. @@ -4459,7 +4459,7 @@

      Рисунок 12-13   Решение задачи размера 3

      Иначе говоря, мы разбиваем задачу \(f(3)\) на две подзадачи \(f(2)\) и одну подзадачу \(f(1)\) . Если последовательно решить эти три подзадачи, исходная задача тоже будет решена. Это показывает, что подзадачи независимы и что их решения можно объединить.

      -

      Таким образом, можно сформулировать показанную на рисунке 12-14 стратегию "разделяй и властвуй" для задачи о Ханойской башне: исходная задача \(f(n)\) разбивается на две подзадачи \(f(n-1)\) и одну подзадачу \(f(1)\) , которые затем решаются в следующем порядке.

      +

      Таким образом, можно сформулировать показанную на рисунке 12-14 стратегию «разделяй и властвуй» для задачи о Ханойской башне: исходная задача \(f(n)\) разбивается на две подзадачи \(f(n-1)\) и одну подзадачу \(f(1)\) , которые затем решаются в следующем порядке.

      1. Переместить \(n-1\) дисков с A на B с помощью C .
      2. Переместить оставшийся \(1\) диск напрямую с A на C .
      3. @@ -4900,7 +4900,7 @@

        -

        Как показано на рисунке 12-15, задача о Ханойской башне формирует дерево рекурсии высоты \(n\) , в котором каждый узел представляет подзадачу и соответствует одному открытому вызову dfs() ; поэтому временная сложность равна \(O(2^n)\) , а пространственная сложность равна \(O(n)\) .

        +

        Как показано на рисунке 12-15, задача о Ханойской башне формирует дерево рекурсии высоты \(n\) , в котором каждый узел представляет подзадачу и соответствует одному открытому вызову dfs(). Поэтому временная сложность равна \(O(2^n)\) , а пространственная сложность равна \(O(n)\) .

        Дерево рекурсии задачи о Ханойской башне

        Рисунок 12-15   Дерево рекурсии задачи о Ханойской башне

        diff --git a/ru/chapter_divide_and_conquer/index.html b/ru/chapter_divide_and_conquer/index.html index 43374334c..e61f54d7f 100644 --- a/ru/chapter_divide_and_conquer/index.html +++ b/ru/chapter_divide_and_conquer/index.html @@ -4280,7 +4280,7 @@

        Abstract

        Сложная задача раскладывается слой за слоем, и каждое новое разбиение делает ее проще.

        -

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

        +

        Принцип «разделяй и властвуй» показывает важный факт: если начать с простого, многое перестает быть сложным.

        Содержание главы

          diff --git a/ru/chapter_divide_and_conquer/summary/index.html b/ru/chapter_divide_and_conquer/summary/index.html index fc1909b49..01d81012d 100644 --- a/ru/chapter_divide_and_conquer/summary/index.html +++ b/ru/chapter_divide_and_conquer/summary/index.html @@ -4337,13 +4337,13 @@

          12.5   Резюме

          1.   Ключевые выводы

            -
          • "Разделяй и властвуй" - это распространенная стратегия проектирования алгоритмов, которая включает два этапа: разделение (декомпозицию) и объединение (синтез), и обычно реализуется с помощью рекурсии.
          • +
          • «Разделяй и властвуй» - это распространенная стратегия проектирования алгоритмов, которая включает два этапа: разделение (декомпозицию) и объединение (синтез), и обычно реализуется с помощью рекурсии.
          • Критерии применимости этой стратегии к задаче включают: возможность разложения задачи, независимость подзадач и возможность объединения их решений.
          • -
          • Сортировка слиянием является типичным применением стратегии "разделяй и властвуй": она рекурсивно делит массив на два равных по длине подмассива, пока не останется массив из одного элемента, после чего начинает поэтапное объединение.
          • -
          • Использование стратегии "разделяй и властвуй" часто позволяет повысить эффективность алгоритма. С одной стороны, она уменьшает число операций; с другой - после разбиения способствует параллельной оптимизации на уровне системы.
          • -
          • "Разделяй и властвуй" не только помогает решать многие алгоритмические задачи, но и широко используется при проектировании структур данных и алгоритмов, поэтому его можно встретить буквально повсюду.
          • -
          • По сравнению с полным перебором адаптивный поиск работает эффективнее. Алгоритмы поиска со сложностью \(O(\log n)\) обычно реализуются на основе стратегии "разделяй и властвуй".
          • -
          • Двоичный поиск - еще одно типичное применение стратегии "разделяй и властвуй", в котором отсутствует шаг объединения решений подзадач. Его можно реализовать рекурсивно, опираясь на эту стратегию.
          • +
          • Сортировка слиянием является типичным применением стратегии «разделяй и властвуй»: она рекурсивно делит массив на два равных по длине подмассива, пока не останется массив из одного элемента, после чего начинает поэтапное объединение.
          • +
          • Использование стратегии «разделяй и властвуй» часто позволяет повысить эффективность алгоритма. С одной стороны, она уменьшает число операций. С другой - после разбиения способствует параллельной оптимизации на уровне системы.
          • +
          • «Разделяй и властвуй» не только помогает решать многие алгоритмические задачи, но и широко используется при проектировании структур данных и алгоритмов, поэтому его можно встретить буквально повсюду.
          • +
          • По сравнению с полным перебором адаптивный поиск работает эффективнее. Алгоритмы поиска со сложностью \(O(\log n)\) обычно реализуются на основе стратегии «разделяй и властвуй».
          • +
          • Двоичный поиск - еще одно типичное применение стратегии «разделяй и властвуй», в котором отсутствует шаг объединения решений подзадач. Его можно реализовать рекурсивно, опираясь на эту стратегию.
          • В задаче построения двоичного дерева исходная задача построения дерева может быть разбита на две подзадачи: построение левого и правого поддеревьев, а реализуется это через разбиение индексных интервалов прямого и симметричного обходов.
          • В задаче о Ханойской башне задача размера \(n\) разбивается на две подзадачи размера \(n-1\) и одну подзадачу размера \(1\) . После последовательного решения этих трех подзадач исходная задача также оказывается решенной.
          diff --git a/ru/chapter_dynamic_programming/dp_problem_features/index.html b/ru/chapter_dynamic_programming/dp_problem_features/index.html index e863ae427..a592c4747 100644 --- a/ru/chapter_dynamic_programming/dp_problem_features/index.html +++ b/ru/chapter_dynamic_programming/dp_problem_features/index.html @@ -4357,10 +4357,10 @@

          14.2   Свойства задач динамического программирования

          -

          В предыдущем разделе мы увидели, как динамическое программирование решает исходную задачу через разложение на подзадачи. На самом деле разложение на подзадачи - это общий алгоритмический подход, но в методе "разделяй и властвуй", динамическом программировании и поиске с возвратом акценты расставлены по-разному.

          +

          В предыдущем разделе мы увидели, как динамическое программирование решает исходную задачу через разложение на подзадачи. На самом деле разложение на подзадачи - это общий алгоритмический подход, но в методе «разделяй и властвуй», динамическом программировании и поиске с возвратом акценты расставлены по-разному.

            -
          • Алгоритмы "разделяй и властвуй" рекурсивно раскладывают исходную задачу на несколько независимых подзадач, пока не будет достигнута наименьшая подзадача, а затем в процессе возврата объединяют решения подзадач в решение исходной задачи.
          • -
          • Динамическое программирование тоже раскладывает задачу рекурсивно, но его главное отличие от метода "разделяй и властвуй" в том, что подзадачи здесь зависят друг от друга и в процессе разложения возникает много перекрывающихся подзадач.
          • +
          • Алгоритмы «разделяй и властвуй» рекурсивно раскладывают исходную задачу на несколько независимых подзадач, пока не будет достигнута наименьшая подзадача, а затем в процессе возврата объединяют решения подзадач в решение исходной задачи.
          • +
          • Динамическое программирование тоже раскладывает задачу рекурсивно, но его главное отличие от метода «разделяй и властвуй» в том, что подзадачи здесь зависят друг от друга и в процессе разложения возникает много перекрывающихся подзадач.
          • Алгоритм поиска с возвратом перебирает все возможные решения через попытки и откат и с помощью обрезки избегает ненужных ветвей поиска. Решение исходной задачи состоит из последовательности решений, и подзадачей можно считать префикс этой последовательности решений.

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

          @@ -4380,7 +4380,7 @@ dp[i] = \min(dp[i-1], dp[i-2]) + cost[i] \]

    Отсюда и возникает смысл оптимальной подструктуры: оптимальное решение исходной задачи строится из оптимальных решений подзадач.

    Очевидно, что эта задача обладает оптимальной подструктурой: мы берем лучшее из двух оптимальных решений подзадач \(dp[i-1]\) и \(dp[i-2]\) и на его основе строим оптимальное решение исходной задачи \(dp[i]\) .

    -

    А обладает ли оптимальной подструктурой исходная задача о числе способов подъема по лестнице из прошлого раздела? Формально она не про оптимум, а про подсчет количества. Но если переформулировать ее как "найдите максимальное количество способов", мы неожиданно увидим, что хотя исходная задача осталась по сути той же, оптимальная подструктура стала явной: максимальное число способов добраться до ступени \(n\) равно сумме максимальных чисел способов добраться до ступеней \(n-1\) и \(n-2\) . То есть объяснение оптимальной подструктуры в разных задачах может быть довольно гибким.

    +

    А обладает ли оптимальной подструктурой исходная задача о числе способов подъема по лестнице из прошлого раздела? Формально она не про оптимум, а про подсчет количества. Но если переформулировать ее как «найдите максимальное количество способов», мы неожиданно увидим, что хотя исходная задача осталась по сути той же, оптимальная подструктура стала явной: максимальное число способов добраться до ступени \(n\) равно сумме максимальных чисел способов добраться до ступеней \(n-1\) и \(n-2\) . То есть объяснение оптимальной подструктуры в разных задачах может быть довольно гибким.

    Зная уравнение перехода состояния, а также начальные состояния \(dp[1] = cost[1]\) и \(dp[2] = cost[2]\) , мы можем сразу написать код динамического программирования:

    @@ -4883,7 +4883,7 @@ dp[i] = \min(dp[i-1], dp[i-2]) + cost[i]

    14.2.2   Отсутствие последствий

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

    -

    Для примера снова рассмотрим задачу о лестнице. Если дано состояние \(i\) , то из него можно перейти в состояния \(i+1\) и \(i+2\) , соответствующие прыжкам на \(1\) и на \(2\) ступени. Чтобы сделать один из этих выборов, не нужно знать, какими были состояния до \(i\) ; на будущее влияет только текущее состояние \(i\) .

    +

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

    Однако если добавить в задачу дополнительное ограничение, ситуация изменится.

    Подъем по лестнице с ограничением

    @@ -4910,7 +4910,7 @@ dp[i, 2] = dp[i-2, 1] + dp[i-2, 2]

    Рекуррентная связь с учетом ограничения

    Рисунок 14-9   Рекуррентная связь с учетом ограничения

    -

    В конце достаточно вернуть \(dp[n, 1] + dp[n, 2]\) ; эта сумма и представляет общее число способов добраться до ступени \(n\) :

    +

    В конце достаточно вернуть \(dp[n, 1] + dp[n, 2]\). Эта сумма и представляет общее число способов добраться до ступени \(n\) :

    @@ -5208,7 +5208,7 @@ dp[i, 2] = dp[i-2, 1] + dp[i-2, 2]

    -

    В этом примере достаточно дополнительно учитывать только одно предыдущее состояние, поэтому после расширения определения состояния задача снова начинает удовлетворять свойству отсутствия последствий. Однако в некоторых задачах "зависимость от прошлого" бывает гораздо серьезнее.

    +

    В этом примере достаточно дополнительно учитывать только одно предыдущее состояние, поэтому после расширения определения состояния задача снова начинает удовлетворять свойству отсутствия последствий. Однако в некоторых задачах «зависимость от прошлого» бывает гораздо серьезнее.

    Подъем по лестнице с порождением препятствий

    Дана лестница из \(n\) ступеней. За один шаг можно подняться на \(1\) или на \(2\) ступени. При этом, если вы попали на ступень \(i\) , система автоматически создает препятствие на ступени \(2i\) , и на всех последующих шагах становиться на ступень \(2i\) уже нельзя. Например, если в первых двух раундах вы попали на ступени \(2\) и \(3\) , то после этого нельзя будет попадать на ступени \(4\) и \(6\) . Сколько существует способов добраться до вершины?

    diff --git a/ru/chapter_dynamic_programming/dp_solution_pipeline/index.html b/ru/chapter_dynamic_programming/dp_solution_pipeline/index.html index cb26cbc9e..d8a4fa891 100644 --- a/ru/chapter_dynamic_programming/dp_solution_pipeline/index.html +++ b/ru/chapter_dynamic_programming/dp_solution_pipeline/index.html @@ -4464,22 +4464,22 @@

    14.3.1   Определение задачи

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

    -

    Задачи, подходящие для поиска с возвратом, обычно удовлетворяют "модели дерева решений". Такие задачи можно описать деревом, где каждый узел представляет одно решение, а каждый путь представляет последовательность решений.

    +

    Задачи, подходящие для поиска с возвратом, обычно удовлетворяют «модели дерева решений». Такие задачи можно описать деревом, где каждый узел представляет одно решение, а каждый путь представляет последовательность решений.

    Иначе говоря, если в задаче есть четко выраженные решения и ответ порождается последовательностью таких решений, то она удовлетворяет модели дерева решений и обычно допускает решение через поиск с возвратом.

    -

    Поверх этого у задач динамического программирования есть и некоторые дополнительные "плюсы".

    +

    Поверх этого у задач динамического программирования есть и некоторые дополнительные «плюсы».

      -
    • В условии задачи фигурируют слова "максимальный", "минимальный", "наибольший", "наименьший" и другие формулировки оптимизации.
    • +
    • В условии задачи фигурируют слова «максимальный», «минимальный», «наибольший», «наименьший» и другие формулировки оптимизации.
    • Состояния задачи можно описать списком, многомерной матрицей или деревом, и между состоянием и соседними состояниями существует рекуррентная зависимость.
    -

    Соответственно, существуют и некоторые "минусы".

    +

    Соответственно, существуют и некоторые «минусы».

    • Цель задачи состоит в поиске всех возможных решений, а не одного оптимального решения.
    • В формулировке явно присутствуют признаки комбинаторного перечисления, и требуется вернуть сразу много конкретных вариантов.
    -

    Если задача удовлетворяет модели дерева решений и имеет достаточно явные "плюсы", мы можем предположить, что это задача динамического программирования, а затем проверить это предположение уже в процессе решения.

    +

    Если задача удовлетворяет модели дерева решений и имеет достаточно явные «плюсы», мы можем предположить, что это задача динамического программирования, а затем проверить это предположение уже в процессе решения.

    14.3.2   Этапы решения задачи

    Конкретный процесс решения задач динамического программирования зависит от природы и сложности задачи, но обычно включает следующие шаги: описание решений, определение состояний, построение таблицы \(dp\) , вывод уравнения перехода состояния, определение граничных условий и порядка переходов.

    -

    Чтобы нагляднее показать этот процесс, рассмотрим классическую задачу "минимальная сумма пути".

    +

    Чтобы нагляднее показать этот процесс, рассмотрим классическую задачу «минимальная сумма пути».

    Question

    Дана двумерная сетка grid размера \(n \times m\) , в каждой клетке которой записано неотрицательное целое число, означающее стоимость прохождения через эту клетку. Робот стартует из левой верхней клетки и за один шаг может двигаться только вправо или вниз, пока не достигнет правой нижней клетки. Верните минимальную сумму пути от левой верхней клетки до правой нижней.

    @@ -4489,7 +4489,7 @@

    Рисунок 14-10   Пример данных для задачи о минимальной сумме пути

    Шаг 1: понять решения на каждом раунде, определить состояние и тем самым получить таблицу \(dp\)

    -

    В этой задаче на каждом раунде решение состоит в том, чтобы из текущей клетки сделать один шаг вниз или вправо. Пусть индексы строки и столбца текущей клетки равны \([i, j]\) ; тогда после шага вниз или вправо индексы становятся равными \([i+1, j]\) или \([i, j+1]\) . Значит, состояние должно включать два переменных индекса: строки и столбца, то есть состояние обозначается как \([i, j]\) .

    +

    В этой задаче на каждом раунде решение состоит в том, чтобы из текущей клетки сделать один шаг вниз или вправо. Пусть индексы строки и столбца текущей клетки равны \([i, j]\). Тогда после шага вниз или вправо индексы становятся равными \([i+1, j]\) или \([i, j+1]\) . Значит, состояние должно включать два переменных индекса: строки и столбца, то есть состояние обозначается как \([i, j]\) .

    Подзадача, соответствующая состоянию \([i, j]\) , такова: минимальная сумма пути от стартовой клетки \([0, 0]\) до клетки \([i, j]\) . Ее решение обозначается через \(dp[i, j]\) .

    На этом этапе мы получаем двумерную матрицу \(dp\) , показанную на рисунке 14-11, размер которой совпадает с размером входной сетки grid .

    Определение состояния и таблицы dp

    @@ -4498,7 +4498,7 @@

    Note

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

    -

    Каждому состоянию соответствует некоторая подзадача, и для хранения решений всех подзадач мы определяем таблицу \(dp\) ; каждая независимая переменная состояния становится одним измерением таблицы \(dp\) . По сути таблица \(dp\) - это отображение от состояния к решению соответствующей подзадачи.

    +

    Каждому состоянию соответствует некоторая подзадача, и для хранения решений всех подзадач мы определяем таблицу \(dp\). Каждая независимая переменная состояния становится одним измерением таблицы \(dp\) . По сути таблица \(dp\) - это отображение от состояния к решению соответствующей подзадачи.

    Шаг 2: найти оптимальную подструктуру и на ее основе вывести уравнение перехода состояния

    Для состояния \([i, j]\) возможны только два источника: клетка сверху \([i-1, j]\) и клетка слева \([i, j-1]\) . Следовательно, оптимальная подструктура выглядит так: минимальная сумма пути до \([i, j]\) определяется меньшим из двух значений - минимальной суммы пути до \([i-1, j]\) и минимальной суммы пути до \([i, j-1]\) .

    @@ -4525,7 +4525,7 @@ dp[i, j] = \min(dp[i-1, j], dp[i, j-1]) + grid[i, j]

    В динамическом программировании граничные условия используются для инициализации таблицы \(dp\) , а в поиске - для обрезки.

    Смысл порядка перехода состояния в том, чтобы к моменту вычисления текущей подзадачи все более мелкие подзадачи, от которых она зависит, уже были вычислены корректно.

    -

    После этого анализа мы уже можем напрямую написать код динамического программирования. Однако разложение на подзадачи - это мышление "сверху вниз", поэтому с точки зрения мышления более естественно реализовывать задачу в порядке "полный перебор \(\rightarrow\) поиск с мемоизацией \(\rightarrow\) динамическое программирование".

    +

    После этого анализа мы уже можем напрямую написать код динамического программирования. Однако разложение на подзадачи - это мышление «сверху вниз», поэтому с точки зрения мышления более естественно реализовывать задачу в порядке «полный перебор \(\rightarrow\) поиск с мемоизацией \(\rightarrow\) динамическое программирование».

    1.   Метод 1: полный перебор

    Начав со состояния \([i, j]\) , мы непрерывно раскладываем его на меньшие состояния \([i-1, j]\) и \([i, j-1]\) . Рекурсивная функция при этом имеет следующие элементы.

      @@ -4789,7 +4789,7 @@ dp[i, j] = \min(dp[i-1, j], dp[i, j-1]) + grid[i, j]

      -

      На рисунке 14-14 показано дерево рекурсии с корнем в \(dp[2, 1]\) ; в нем содержатся перекрывающиеся подзадачи, и их число будет резко расти вместе с размером сетки grid .

      +

      На рисунке 14-14 показано дерево рекурсии с корнем в \(dp[2, 1]\). В нем содержатся перекрывающиеся подзадачи, и их число будет резко расти вместе с размером сетки grid .

      По своей сути причина появления перекрывающихся подзадач такова: существует много разных путей от левого верхнего угла до одной и той же клетки.

      Дерево рекурсии полного перебора

      Рисунок 14-14   Дерево рекурсии полного перебора

      diff --git a/ru/chapter_dynamic_programming/edit_distance_problem/index.html b/ru/chapter_dynamic_programming/edit_distance_problem/index.html index 3964132c7..2919680ba 100644 --- a/ru/chapter_dynamic_programming/edit_distance_problem/index.html +++ b/ru/chapter_dynamic_programming/edit_distance_problem/index.html @@ -4385,7 +4385,7 @@

      Даны две строки \(s\) и \(t\) . Верните минимальное число шагов редактирования, необходимое для преобразования \(s\) в \(t\) .

      Для строки допускаются три операции редактирования: вставка одного символа, удаление одного символа и замена одного символа на произвольный другой символ.

    -

    Как показано на рисунке 14-27, для преобразования kitten в sitting требуется 3 шага редактирования: 2 операции замены и 1 операция вставки; для преобразования hello в algo также требуется 3 шага: 2 замены и 1 удаление.

    +

    Как показано на рисунке 14-27, для преобразования kitten в sitting требуется 3 шага редактирования: 2 операции замены и 1 операция вставки. Для преобразования hello в algo также требуется 3 шага: 2 замены и 1 удаление.

    Пример данных для задачи о расстоянии редактирования

    Рисунок 14-27   Пример данных для задачи о расстоянии редактирования

    @@ -4398,7 +4398,7 @@

    1.   Идея динамического программирования

    Шаг 1: продумать решения на каждом раунде, определить состояние и тем самым получить таблицу \(dp\)

    На каждом раунде решение состоит в выполнении одной операции редактирования над строкой \(s\) .

    -

    Нам нужно, чтобы в ходе выполнения операций размер задачи постепенно уменьшался; только тогда можно строить подзадачи. Пусть длины строк \(s\) и \(t\) равны соответственно \(n\) и \(m\) ; сначала рассмотрим последние символы этих строк, то есть \(s[n-1]\) и \(t[m-1]\) .

    +

    Нам нужно, чтобы в ходе выполнения операций размер задачи постепенно уменьшался. Только тогда можно строить подзадачи. Пусть длины строк \(s\) и \(t\) равны соответственно \(n\) и \(m\). Сначала рассмотрим последние символы этих строк, то есть \(s[n-1]\) и \(t[m-1]\) .

    • Если \(s[n-1]\) и \(t[m-1]\) совпадают, их можно просто пропустить и сразу перейти к сравнению \(s[n-2]\) и \(t[m-2]\) .
    • Если \(s[n-1]\) и \(t[m-1]\) различны, нужно выполнить над \(s\) одну операцию редактирования (вставку, удаление или замену), чтобы последние символы стали одинаковыми, после чего можно перейти к задаче меньшего размера.
    • @@ -4409,9 +4409,9 @@

      Шаг 2: найти оптимальную подструктуру и на ее основе вывести уравнение перехода состояния

      Рассмотрим подзадачу \(dp[i, j]\) . Ее последние символы - это \(s[i-1]\) и \(t[j-1]\) . В зависимости от операции редактирования возможны три случая, показанные на рисунке 14-29.

        -
      1. Вставить после \(s[i-1]\) символ \(t[j-1]\) ; тогда остается подзадача \(dp[i, j-1]\) .
      2. -
      3. Удалить \(s[i-1]\) ; тогда остается подзадача \(dp[i-1, j]\) .
      4. -
      5. Заменить \(s[i-1]\) на \(t[j-1]\) ; тогда остается подзадача \(dp[i-1, j-1]\) .
      6. +
      7. Вставить после \(s[i-1]\) символ \(t[j-1]\). Тогда остается подзадача \(dp[i, j-1]\) .
      8. +
      9. Удалить \(s[i-1]\). Тогда остается подзадача \(dp[i-1, j]\) .
      10. +
      11. Заменить \(s[i-1]\) на \(t[j-1]\). Тогда остается подзадача \(dp[i-1, j-1]\) .

      Переходы состояния в задаче о расстоянии редактирования

      Рисунок 14-29   Переходы состояния в задаче о расстоянии редактирования

      @@ -4865,7 +4865,7 @@ dp[i, j] = dp[i-1, j-1]

      3.   Оптимизация пространства

      Поскольку \(dp[i,j]\) зависит от значения сверху \(dp[i-1, j]\) , слева \(dp[i, j-1]\) и слева сверху \(dp[i-1, j-1]\) , прямой обход после оптимизации памяти теряет значение слева сверху, а обратный обход не позволяет заранее построить значение слева \(dp[i, j-1]\) . Значит, оба наивных варианта обхода здесь непригодны.

      -

      Чтобы решить эту проблему, можно использовать переменную leftup для временного сохранения значения слева сверху \(dp[i-1, j-1]\) ; после этого остается учитывать только верхнее и левое значения. Тогда ситуация становится аналогичной задаче о полном рюкзаке, и можно выполнять прямой обход. Код приведен ниже:

      +

      Чтобы решить эту проблему, можно использовать переменную leftup для временного сохранения значения слева сверху \(dp[i-1, j-1]\). После этого остается учитывать только верхнее и левое значения. Тогда ситуация становится аналогичной задаче о полном рюкзаке, и можно выполнять прямой обход. Код приведен ниже:

      diff --git a/ru/chapter_dynamic_programming/intro_to_dynamic_programming/index.html b/ru/chapter_dynamic_programming/intro_to_dynamic_programming/index.html index bef16c88a..b2377a049 100644 --- a/ru/chapter_dynamic_programming/intro_to_dynamic_programming/index.html +++ b/ru/chapter_dynamic_programming/intro_to_dynamic_programming/index.html @@ -4411,7 +4411,7 @@

      Число способов подняться на 3-ю ступень

      Рисунок 14-1   Число способов подняться на 3-ю ступень

      -

      Цель этой задачи - вычислить количество способов. Поэтому можно попробовать использовать для ее решения метод поиска с возвратом. Если представить подъем по лестнице как последовательность решений, то мы начинаем от земли и на каждом раунде выбираем прыжок на \(1\) или на \(2\) ступени; всякий раз, когда достигаем вершины, увеличиваем число способов на \(1\) , а если перескакиваем вершину, обрезаем эту ветвь. Код выглядит так:

      +

      Цель этой задачи - вычислить количество способов. Поэтому можно попробовать использовать для ее решения метод поиска с возвратом. Если представить подъем по лестнице как последовательность решений, то мы начинаем от земли и на каждом раунде выбираем прыжок на \(1\) или на \(2\) ступени. Всякий раз, когда достигаем вершины, увеличиваем число способов на \(1\) , а если перескакиваем вершину, обрезаем эту ветвь. Код выглядит так:

      @@ -4796,8 +4796,8 @@

      14.1.1   Метод 1: полный перебор

      -

      Алгоритм поиска с возвратом обычно не раскладывает задачу явно на подзадачи; вместо этого он рассматривает решение как последовательность решений, используя попытки и обрезку для поиска всех возможных ответов.

      -

      Попробуем посмотреть на задачу именно как на разложение подзадач. Пусть число способов добраться до ступени \(i\) равно \(dp[i]\) ; тогда \(dp[i]\) - это исходная задача, а ее подзадачи включают:

      +

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

      +

      Попробуем посмотреть на задачу именно как на разложение подзадач. Пусть число способов добраться до ступени \(i\) равно \(dp[i]\). Тогда \(dp[i]\) - это исходная задача, а ее подзадачи включают:

      \[ dp[i-1], dp[i-2], \dots, dp[2], dp[1] \]
      @@ -5041,7 +5041,7 @@ dp[i] = dp[i-1] + dp[i-2]

      Дерево рекурсии для подъема по лестнице

      Рисунок 14-3   Дерево рекурсии для подъема по лестнице

      -

      Если посмотреть на рисунок 14-3, то видно, что экспоненциальная временная сложность порождается "перекрывающимися подзадачами". Например, \(dp[9]\) раскладывается в \(dp[8]\) и \(dp[7]\) , а \(dp[8]\) - в \(dp[7]\) и \(dp[6]\) ; обе ветви содержат подзадачу \(dp[7]\) .

      +

      Как видно на рисунке 14-3, экспоненциальная временная сложность порождается «перекрывающимися подзадачами». Например, \(dp[9]\) раскладывается в \(dp[8]\) и \(dp[7]\) , а \(dp[8]\) - в \(dp[7]\) и \(dp[6]\). Обе ветви содержат подзадачу \(dp[7]\) .

      Продолжая это рассуждение, мы видим, что подзадачи порождают все более мелкие перекрывающиеся подзадачи без конца. Подавляющая часть вычислительных ресурсов уходит именно на них.

      14.1.2   Метод 2: поиск с мемоизацией

      Чтобы ускорить алгоритм, мы хотим, чтобы каждая перекрывающаяся подзадача вычислялась только один раз. Для этого объявим массив mem для хранения решения каждой подзадачи и будем обрезать повторные вычисления в процессе поиска.

      @@ -5381,9 +5381,9 @@ dp[i] = dp[i-1] + dp[i-2]

      Рисунок 14-4   Дерево рекурсии для поиска с мемоизацией

      14.1.3   Метод 3: динамическое программирование

      -

      Поиск с мемоизацией - это метод "сверху вниз" : мы начинаем с исходной задачи (корня), рекурсивно раскладываем более крупные подзадачи на меньшие, пока не достигнем наименьших подзадач с уже известным ответом (листьев). Затем в процессе возврата постепенно собираем решения подзадач и тем самым получаем решение исходной задачи.

      -

      Напротив, динамическое программирование - это метод "снизу вверх" : начиная с решений наименьших подзадач, мы итеративно строим решения для более крупных подзадач, пока не получим ответ на исходную задачу.

      -

      Поскольку в динамическом программировании нет этапа возврата, для его реализации достаточно обычных циклов, без рекурсии. В приведенном ниже коде мы инициализируем массив dp для хранения решений подзадач; он выполняет ту же роль, что и массив mem в мемоизированном поиске:

      +

      Поиск с мемоизацией - это метод «сверху вниз» : мы начинаем с исходной задачи (корня), рекурсивно раскладываем более крупные подзадачи на меньшие, пока не достигнем наименьших подзадач с уже известным ответом (листьев). Затем в процессе возврата постепенно собираем решения подзадач и тем самым получаем решение исходной задачи.

      +

      Напротив, динамическое программирование - это метод «снизу вверх» : начиная с решений наименьших подзадач, мы итеративно строим решения для более крупных подзадач, пока не получим ответ на исходную задачу.

      +

      Поскольку в динамическом программировании нет этапа возврата, для его реализации достаточно обычных циклов, без рекурсии. В приведенном ниже коде мы инициализируем массив dp для хранения решений подзадач. Он выполняет ту же роль, что и массив mem в мемоизированном поиске:

      @@ -5628,7 +5628,7 @@ dp[i] = dp[i-1] + dp[i-2]

      Процесс динамического программирования для подъема по лестнице

      Рисунок 14-5   Процесс динамического программирования для подъема по лестнице

      -

      Как и в поиске с возвратом, в динамическом программировании используется понятие "состояние" для обозначения некоторого этапа решения задачи; каждое состояние соответствует одной подзадаче и ее локально оптимальному решению. Например, в задаче о лестнице состояние определяется текущим номером ступени \(i\) .

      +

      Как и в поиске с возвратом, в динамическом программировании используется понятие «состояние» для обозначения некоторого этапа решения задачи. Каждое состояние соответствует одной подзадаче и ее локально оптимальному решению. Например, в задаче о лестнице состояние определяется текущим номером ступени \(i\) .

      На основе сказанного можно подвести несколько часто используемых терминов динамического программирования.

      • Массив dp называют таблицей dp, а \(dp[i]\) обозначает решение подзадачи, соответствующей состоянию \(i\) .
      • @@ -5636,7 +5636,7 @@ dp[i] = dp[i-1] + dp[i-2]
      • Рекуррентную формулу \(dp[i] = dp[i-1] + dp[i-2]\) называют уравнением перехода состояния.

      14.1.4   Оптимизация пространства

      -

      Внимательный читатель мог заметить, что поскольку \(dp[i]\) зависит только от \(dp[i-1]\) и \(dp[i-2]\) , нам не нужен весь массив dp для хранения ответов всех подзадач ; достаточно двух переменных, которые будут "перекатываться" вперед. Код имеет вид:

      +

      Внимательный читатель мог заметить, что поскольку \(dp[i]\) зависит только от \(dp[i-1]\) и \(dp[i-2]\) , нам не нужен весь массив dp для хранения ответов всех подзадач. Достаточно двух переменных, которые будут «перекатываться» вперед. Код имеет вид:

      @@ -5835,7 +5835,7 @@ dp[i] = dp[i-1] + dp[i-2]

      Из кода видно, что после отказа от массива dp пространственная сложность уменьшается с \(O(n)\) до \(O(1)\) .

      -

      Во многих задачах динамического программирования текущее состояние зависит лишь от ограниченного числа предыдущих состояний. Тогда можно сохранять только действительно нужные состояния и за счет "уменьшения размерности" экономить память. Этот прием оптимизации памяти называют "скользящими переменными" или "скользящим массивом".

      +

      Во многих задачах динамического программирования текущее состояние зависит лишь от ограниченного числа предыдущих состояний. Тогда можно сохранять только действительно нужные состояния и за счет «уменьшения размерности» экономить память. Этот прием оптимизации памяти называют «скользящими переменными» или «скользящим массивом».

      diff --git a/ru/chapter_dynamic_programming/knapsack_problem/index.html b/ru/chapter_dynamic_programming/knapsack_problem/index.html index 6a18b1cdd..31631c4a5 100644 --- a/ru/chapter_dynamic_programming/knapsack_problem/index.html +++ b/ru/chapter_dynamic_programming/knapsack_problem/index.html @@ -4412,9 +4412,9 @@

      Рисунок 14-17   Пример данных для задачи о рюкзаке 0-1

      Задачу о рюкзаке 0-1 можно рассматривать как процесс из \(n\) раундов принятия решений: для каждого предмета есть два решения - не класть его в рюкзак или положить в рюкзак. Поэтому задача удовлетворяет модели дерева решений.

      -

      Цель задачи - найти "максимальную суммарную стоимость при ограниченной вместимости рюкзака", а это с большой вероятностью указывает на задачу динамического программирования.

      +

      Цель задачи - найти «максимальную суммарную стоимость при ограниченной вместимости рюкзака», а это с большой вероятностью указывает на задачу динамического программирования.

      Шаг 1: продумать решения на каждом раунде, определить состояние и тем самым получить таблицу \(dp\)

      -

      Для каждого предмета возможны два случая: не класть его в рюкзак, тогда вместимость не меняется; или положить его в рюкзак, тогда оставшаяся вместимость уменьшается. Отсюда получается определение состояния: текущий номер предмета \(i\) и текущая вместимость рюкзака \(c\) , то есть состояние обозначается как \([i, c]\) .

      +

      Для каждого предмета возможны два случая: не класть его в рюкзак, тогда вместимость не меняется. Или положить его в рюкзак, тогда оставшаяся вместимость уменьшается. Отсюда получается определение состояния: текущий номер предмета \(i\) и текущая вместимость рюкзака \(c\) , то есть состояние обозначается как \([i, c]\) .

      Подзадача, соответствующая состоянию \([i, c]\) , такова: максимальная стоимость, которую можно получить, используя первые \(i\) предметов и рюкзак вместимости \(c\). Ее решение обозначается через \(dp[i, c]\) .

      Искомым значением является \(dp[n, cap]\) , значит, нам нужна двумерная таблица \(dp\) размера \((n+1) \times (cap+1)\) .

      Шаг 2: найти оптимальную подструктуру и на ее основе вывести уравнение перехода состояния

      @@ -4429,7 +4429,7 @@ dp[i, c] = \max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1]) \]

      Нужно учитывать, что если вес текущего предмета \(wgt[i - 1]\) превышает оставшуюся вместимость \(c\) , то предмет можно только не брать.

      Шаг 3: определить граничные условия и порядок переходов

      -

      Когда предметов нет или вместимость рюкзака равна \(0\) , максимальная стоимость равна \(0\) ; то есть весь первый столбец \(dp[i, 0]\) и вся первая строка \(dp[0, c]\) заполняются нулями.

      +

      Когда предметов нет или вместимость рюкзака равна \(0\) , максимальная стоимость равна \(0\). То есть весь первый столбец \(dp[i, 0]\) и вся первая строка \(dp[0, c]\) заполняются нулями.

      Текущее состояние \([i, c]\) зависит от состояния сверху \([i-1, c]\) и состояния слева сверху \([i-1, c-wgt[i-1]]\) , поэтому достаточно двумя вложенными циклами пройти по всей таблице \(dp\) в прямом порядке.

      После этого анализа реализуем по порядку: полный перебор, поиск с мемоизацией и динамическое программирование.

      1.   Метод 1: полный перебор

      @@ -4699,7 +4699,7 @@ dp[i, c] = \max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1])

      -

      Как показано на рисунке 14-18, поскольку каждый предмет создает две ветви поиска - "не брать" и "брать", временная сложность равна \(O(2^n)\) .

      +

      Как показано на рисунке 14-18, поскольку каждый предмет создает две ветви поиска - «не брать» и «брать», временная сложность равна \(O(2^n)\) .

      Посмотрев на дерево рекурсии, легко заметить наличие перекрывающихся подзадач, например \(dp[1, 10]\) и подобных. Когда число предметов растет, вместимость рюкзака велика, а особенно когда много предметов с одинаковым весом, количество перекрывающихся подзадач быстро увеличивается.

      Дерево полного перебора для задачи о рюкзаке 0-1

      Рисунок 14-18   Дерево полного перебора для задачи о рюкзаке 0-1

      diff --git a/ru/chapter_dynamic_programming/summary/index.html b/ru/chapter_dynamic_programming/summary/index.html index 9d5d8f84c..99ab6c9f9 100644 --- a/ru/chapter_dynamic_programming/summary/index.html +++ b/ru/chapter_dynamic_programming/summary/index.html @@ -4339,23 +4339,23 @@
      • Динамическое программирование раскладывает задачу на подзадачи и повышает вычислительную эффективность за счет хранения решений этих подзадач и устранения повторных вычислений.
      • Если не учитывать затраты времени, то любую задачу динамического программирования можно решить с помощью поиска с возвратом (полного перебора), однако в дереве рекурсии возникает множество перекрывающихся подзадач, из-за чего эффективность крайне низка. После введения таблицы памяти можно хранить решения всех уже вычисленных подзадач и гарантировать, что каждая перекрывающаяся подзадача будет вычисляться только один раз.
      • -
      • Поиск с мемоизацией - это рекурсивный метод "сверху вниз", а соответствующее ему динамическое программирование - это итеративный метод "снизу вверх", похожий на заполнение таблицы. Поскольку текущее состояние обычно зависит только от части локальных состояний, можно убрать одно измерение таблицы \(dp\) и тем самым снизить пространственную сложность.
      • -
      • Разложение на подзадачи - это общий алгоритмический подход, но в методе "разделяй и властвуй", динамическом программировании и поиске с возвратом он имеет разные свойства.
      • +
      • Поиск с мемоизацией - это рекурсивный метод «сверху вниз», а соответствующее ему динамическое программирование - это итеративный метод «снизу вверх», похожий на заполнение таблицы. Поскольку текущее состояние обычно зависит только от части локальных состояний, можно убрать одно измерение таблицы \(dp\) и тем самым снизить пространственную сложность.
      • +
      • Разложение на подзадачи - это общий алгоритмический подход, но в методе «разделяй и властвуй», динамическом программировании и поиске с возвратом он имеет разные свойства.
      • Для задач динамического программирования характерны три главных свойства: перекрывающиеся подзадачи, оптимальная подструктура и отсутствие последствий.
      • Если оптимальное решение исходной задачи можно построить из оптимальных решений подзадач, то задача обладает оптимальной подструктурой.
      • Отсутствие последствий означает, что для данного состояния его дальнейшее развитие определяется только этим состоянием и не зависит от всех прошлых состояний. Многие задачи комбинаторной оптимизации этим свойством не обладают и потому не могут эффективно решаться с помощью динамического программирования.

      Задачи о рюкзаке

        -
      • Задача о рюкзаке - один из самых типичных классов задач динамического программирования; она включает варианты 0-1 рюкзака, полного рюкзака, многократного рюкзака и другие.
      • +
      • Задача о рюкзаке - один из самых типичных классов задач динамического программирования. Она включает варианты 0-1 рюкзака, полного рюкзака, многократного рюкзака и другие.
      • В задаче о рюкзаке 0-1 состояние определяется как максимальная стоимость первых \(i\) предметов в рюкзаке вместимости \(c\) . Рассматривая два решения - не брать предмет и брать предмет, - можно получить оптимальную подструктуру и вывести уравнение перехода состояния. При оптимизации памяти, поскольку каждое состояние зависит от значения сверху и слева сверху, внутренний цикл нужно выполнять в обратном порядке, чтобы не перезаписать нужное значение.
      • В задаче о полном рюкзаке число экземпляров каждого предмета не ограничено, поэтому при выборе предмета переход состояния отличается от варианта 0-1. Поскольку состояние зависит от значения сверху и слева, после оптимизации памяти внутренний цикл следует выполнять в прямом порядке.
      • -
      • Задача о размене монет - это вариант задачи о полном рюкзаке. Здесь вместо "максимальной стоимости" ищется "минимальное число монет", поэтому в уравнении перехода \(\max()\) заменяется на \(\min()\) . Кроме того, вместо условия "не превышать вместимость рюкзака" нужно ровно набрать целевую сумму, поэтому значение \(amt + 1\) используется как обозначение недопустимого решения "сумму набрать нельзя".
      • -
      • В задаче о размене монет II вместо "минимального числа монет" требуется найти "число комбинаций монет", поэтому в уравнении перехода оператор \(\min()\) заменяется на суммирование.
      • +
      • Задача о размене монет - это вариант задачи о полном рюкзаке. Здесь вместо «максимальной стоимости» ищется «минимальное число монет», поэтому в уравнении перехода \(\max()\) заменяется на \(\min()\) . Кроме того, вместо условия «не превышать вместимость рюкзака» нужно ровно набрать целевую сумму, поэтому значение \(amt + 1\) используется как обозначение недопустимого решения «сумму набрать нельзя».
      • +
      • В задаче о размене монет II вместо «минимального числа монет» требуется найти «число комбинаций монет», поэтому в уравнении перехода оператор \(\min()\) заменяется на суммирование.

      Задача о расстоянии редактирования

        -
      • Расстояние редактирования (расстояние Левенштейна) используется для измерения сходства двух строк и определяется как минимальное число операций редактирования, необходимых для преобразования одной строки в другую; допустимые операции - вставка, удаление и замена.
      • +
      • Расстояние редактирования (расстояние Левенштейна) используется для измерения сходства двух строк и определяется как минимальное число операций редактирования, необходимых для преобразования одной строки в другую. Допустимые операции - вставка, удаление и замена.
      • В задаче о расстоянии редактирования состояние определяется как минимальное число шагов редактирования, необходимых для преобразования первых \(i\) символов строки \(s\) в первые \(j\) символов строки \(t\) . Если \(s[i] \ne t[j]\) , то существуют три решения: вставка, удаление и замена, и каждому из них соответствует своя остаточная подзадача. На этой основе выводятся оптимальная подструктура и уравнение перехода состояния. Если же \(s[i] = t[j]\) , то редактировать текущий символ не нужно.
      • В задаче о расстоянии редактирования состояние зависит от значений сверху, слева и слева сверху. Поэтому после оптимизации памяти ни прямой, ни обратный обход сам по себе не дает корректного перехода состояния. Для решения этой проблемы значение слева сверху временно сохраняется в отдельной переменной, что делает ситуацию эквивалентной задаче о полном рюкзаке и позволяет использовать прямой обход.
      diff --git a/ru/chapter_dynamic_programming/unbounded_knapsack_problem/index.html b/ru/chapter_dynamic_programming/unbounded_knapsack_problem/index.html index eaccc7fd6..9b7bfc24d 100644 --- a/ru/chapter_dynamic_programming/unbounded_knapsack_problem/index.html +++ b/ru/chapter_dynamic_programming/unbounded_knapsack_problem/index.html @@ -4623,7 +4623,7 @@

      Рисунок 14-22   Пример данных для задачи о полном рюкзаке

      1.   Идея динамического программирования

      -

      Задача о полном рюкзаке очень похожа на задачу о рюкзаке 0-1; разница состоит только в том, что количество выборов каждого предмета не ограничено.

      +

      Задача о полном рюкзаке очень похожа на задачу о рюкзаке 0-1. Разница состоит только в том, что количество выборов каждого предмета не ограничено.

      • В задаче о рюкзаке 0-1 каждого предмета существует только один экземпляр, поэтому после того как предмет \(i\) помещен в рюкзак, выбирать можно только из первых \(i-1\) предметов.
      • В задаче о полном рюкзаке количество предметов не ограничено, поэтому после того как предмет \(i\) помещен в рюкзак, можно продолжать выбирать из первых \(i\) предметов.
      • @@ -4638,7 +4638,7 @@ dp[i, c] = \max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1]) \]

      2.   Реализация кода

      -

      Если сравнить код этой задачи с кодом задачи о рюкзаке 0-1, то окажется, что в переходе состояний меняется только одна деталь: вместо \(i-1\) появляется \(i\) ; все остальное остается таким же:

      +

      Если сравнить код этой задачи с кодом задачи о рюкзаке 0-1, то окажется, что в переходе состояний меняется только одна деталь: вместо \(i-1\) появляется \(i\). Все остальное остается таким же:

      @@ -4957,7 +4957,7 @@ dp[i, c] = \max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1])

      3.   Оптимизация пространства

      Поскольку текущее состояние переходит из состояния слева и состояния сверху, после оптимизации памяти каждую строку таблицы \(dp\) нужно обходить слева направо.

      -

      Этот порядок обхода как раз противоположен задаче о рюкзаке 0-1. Разницу удобно понять по рисунку ниже.

      +

      Этот порядок обхода как раз противоположен задаче о рюкзаке 0-1. Эту разницу удобно понять, рассмотрев то, что показано на рисунке 14-23.

      @@ -5317,9 +5317,9 @@ dp[i, c] = \max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1])

      Рисунок 14-24   Пример данных для задачи о размене монет

      1.   Идея динамического программирования

      -

      Задачу о размене монет можно рассматривать как частный случай задачи о полном рюкзаке ; между ними существуют следующие соответствия и различия.

      +

      Задачу о размене монет можно рассматривать как частный случай задачи о полном рюкзаке. Между ними существуют следующие соответствия и различия.

        -
      • Эти две задачи можно взаимно преобразовать: "предмет" соответствует "монете", "вес предмета" соответствует "номиналу монеты", а "вместимость рюкзака" соответствует "целевой сумме".
      • +
      • Эти две задачи можно взаимно преобразовать: «предмет» соответствует «монете», «вес предмета» соответствует «номиналу монеты», а «вместимость рюкзака» соответствует «целевой сумме».
      • Цель оптимизации противоположна: в задаче о полном рюкзаке нужно максимизировать стоимость предметов, а в задаче о размене монет - минимизировать число монет.
      • В задаче о полном рюкзаке ищется решение, не превышающее вместимость, а в задаче о размене монет требуется ровно набрать целевую сумму.
      @@ -5337,10 +5337,10 @@ dp[i, a] = \min(dp[i-1, a], dp[i, a - coins[i-1]] + 1) \]

      Шаг 3: определить граничные условия и порядок переходов

      Когда целевая сумма равна \(0\) , минимальное число монет для ее набора равно \(0\) , то есть весь первый столбец \(dp[i, 0]\) заполняется нулями.

      -

      Когда монет нет, невозможно набрать никакую целевую сумму \(> 0\) ; это и есть недопустимое решение. Чтобы функция \(\min()\) в уравнении перехода состояния могла распознавать и отбрасывать такие недопустимые решения, удобно использовать значение \(+ \infty\) ; то есть всю первую строку \(dp[0, a]\) нужно инициализировать значением \(+ \infty\) .

      +

      Когда монет нет, невозможно набрать никакую целевую сумму \(> 0\). Это и есть недопустимое решение. Чтобы функция \(\min()\) в уравнении перехода состояния могла распознавать и отбрасывать такие недопустимые решения, удобно использовать значение \(+ \infty\). То есть всю первую строку \(dp[0, a]\) нужно инициализировать значением \(+ \infty\) .

      2.   Реализация кода

      Большинство языков программирования не предоставляет представление для \(+ \infty\) в целочисленном виде, поэтому обычно приходится заменять его на максимальное значение типа int . Но тогда возникает риск переполнения: операция \(+ 1\) в уравнении перехода может переполнить большое число.

      -

      Поэтому здесь мы используем число \(amt + 1\) как обозначение недопустимого решения, потому что для набора суммы \(amt\) максимум нужно не больше чем \(amt\) монет. Перед возвратом результата проверяем, равно ли \(dp[n, amt]\) значению \(amt + 1\) ; если да, то возвращаем \(-1\) , что означает невозможность набрать целевую сумму. Код приведен ниже:

      +

      Поэтому здесь мы используем число \(amt + 1\) как обозначение недопустимого решения, потому что для набора суммы \(amt\) максимум нужно не больше чем \(amt\) монет. Перед возвратом результата проверяем, равно ли \(dp[n, amt]\) значению \(amt + 1\). Если да, то возвращаем \(-1\) , что означает невозможность набрать целевую сумму. Код приведен ниже:

      diff --git a/ru/chapter_graph/graph/index.html b/ru/chapter_graph/graph/index.html index ac630272e..b13102182 100644 --- a/ru/chapter_graph/graph/index.html +++ b/ru/chapter_graph/graph/index.html @@ -4464,21 +4464,21 @@ G & = \{ V, E \} \newline

      Связный и несвязный графы

      Рисунок 9-3   Связный и несвязный графы

      -

      Мы также можем добавить к ребрам переменную "вес" и получить показанный ниже взвешенный граф (weighted graph). Например, в мобильных играх вроде Honor of Kings система рассчитывает "близость" между игроками по времени совместной игры, и такую сеть близости можно представить взвешенным графом.

      +

      Мы также можем добавить к ребрам переменную «вес» и получить взвешенный граф (weighted graph), как показано на рисунке 9-4. Например, в мобильных играх вроде Honor of Kings система рассчитывает «близость» между игроками по времени совместной игры, и такую сеть близости можно представить взвешенным графом.

      Взвешенный и невзвешенный графы

      Рисунок 9-4   Взвешенный и невзвешенный графы

      -

      Со структурой данных "граф" связаны следующие основные термины.

      +

      Со структурой данных «граф» связаны следующие основные термины.

      • Смежность (adjacency): если между двумя вершинами существует ребро, то такие вершины называются смежными. На рисунке 9-4 с вершиной 1 смежны вершины 2, 3 и 5.
      • Путь (path): последовательность ребер от вершины A до вершины B называется путем из A в B. На рисунке 9-4 последовательность ребер 1-5-2-4 является одним из путей от вершины 1 к вершине 4.
      • Степень (degree): количество ребер, принадлежащих вершине. Для ориентированного графа входящая степень (in-degree) показывает, сколько ребер входит в вершину, а исходящая степень (out-degree) показывает, сколько ребер из нее выходит.

      9.1.2   Представление графа

      -

      Распространенные способы представления графа включают "матрицу смежности" и "список смежности". Ниже для примера рассматривается неориентированный граф.

      +

      Распространенные способы представления графа включают «матрицу смежности» и «список смежности». Ниже для примера рассматривается неориентированный граф.

      1.   Матрица смежности

      -

      Пусть число вершин графа равно \(n\) ; тогда матрица смежности (adjacency matrix) использует матрицу размера \(n \times n\) для представления графа, где каждая строка и каждый столбец соответствуют вершине, а элементы матрицы показывают наличие или отсутствие ребра.

      -

      Как показано на рисунке 9-5, обозначим матрицу смежности через \(M\) , а список вершин через \(V\) ; тогда элемент матрицы \(M[i, j] = 1\) означает наличие ребра между вершинами \(V[i]\) и \(V[j]\) , а элемент \(M[i, j] = 0\) означает отсутствие ребра.

      +

      Пусть число вершин графа равно \(n\). Тогда матрица смежности (adjacency matrix) использует матрицу размера \(n \times n\) для представления графа, где каждая строка и каждый столбец соответствуют вершине, а элементы матрицы показывают наличие или отсутствие ребра.

      +

      Как показано на рисунке 9-5, обозначим матрицу смежности через \(M\) , а список вершин через \(V\). Тогда элемент матрицы \(M[i, j] = 1\) означает наличие ребра между вершинами \(V[i]\) и \(V[j]\) , а элемент \(M[i, j] = 0\) означает отсутствие ребра.

      Представление графа матрицей смежности

      Рисунок 9-5   Представление графа матрицей смежности

      @@ -4495,7 +4495,7 @@ G & = \{ V, E \} \newline

      Рисунок 9-6   Представление графа списком смежности

      Список смежности хранит только реально существующие ребра, а общее число ребер обычно значительно меньше \(n^2\) , поэтому он лучше экономит память. Однако для поиска ребра в списке смежности требуется обходить список, поэтому по времени он уступает матрице смежности.

      -

      Если посмотреть на рисунок 9-6, можно заметить, что структура списка смежности очень похожа на цепную адресацию в хеш-таблицах, поэтому здесь можно использовать похожие методы оптимизации эффективности. Например, если список слишком длинный, его можно преобразовать в AVL-дерево или красно-черное дерево, чтобы снизить временную сложность с \(O(n)\) до \(O(\log n)\) ; также список можно преобразовать в хеш-таблицу, чтобы довести временную сложность до \(O(1)\) .

      +

      Как видно на рисунке 9-6, структура списка смежности очень похожа на цепную адресацию в хеш-таблицах, поэтому здесь можно использовать похожие методы оптимизации эффективности. Например, если список слишком длинный, его можно преобразовать в AVL-дерево или красно-черное дерево, чтобы снизить временную сложность с \(O(n)\) до \(O(\log n)\). Также список можно преобразовать в хеш-таблицу, чтобы довести временную сложность до \(O(1)\) .

      9.1.3   Типичные применения графов

      Как показано в таблице 9-1, многие реальные системы можно моделировать с помощью графов, а соответствующие задачи затем сводить к задачам вычислений на графах.

      Таблица 9-1   Распространенные графы в реальной жизни

      diff --git a/ru/chapter_graph/graph_operations/index.html b/ru/chapter_graph/graph_operations/index.html index a6cd916c4..56bbbaf04 100644 --- a/ru/chapter_graph/graph_operations/index.html +++ b/ru/chapter_graph/graph_operations/index.html @@ -4379,14 +4379,14 @@

      9.2   Базовые операции графа

      -

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

      +

      Базовые операции графа можно разделить на операции над «ребрами» и операции над «вершинами». При двух способах представления, «матрице смежности» и «списке смежности», реализация этих операций различается.

      9.2.1   Реализация на основе матрицы смежности

      -

      Пусть дан неориентированный граф с числом вершин \(n\) . Тогда способы реализации различных операций показаны на рисунках ниже.

      +

      Пусть дан неориентированный граф с числом вершин \(n\) . Тогда способы реализации различных операций показаны на рисунке 9-7.

      • Добавление или удаление ребра: достаточно изменить соответствующее ребро в матрице смежности, что требует \(O(1)\) времени. Поскольку граф неориентированный, необходимо одновременно обновить ребра в обоих направлениях.
      • -
      • Добавление вершины: в конец матрицы смежности добавляется строка и столбец, полностью заполненные нулями; это требует \(O(n)\) времени.
      • -
      • Удаление вершины: из матрицы смежности удаляется строка и столбец. При удалении первой строки и первого столбца достигается худший случай, когда требуется "сдвинуть влево вверх" \((n-1)^2\) элементов, поэтому используется \(O(n^2)\) времени.
      • -
      • Инициализация: передаются \(n\) вершин, затем инициализируется список вершин vertices длины \(n\) , что требует \(O(n)\) времени; после этого инициализируется матрица смежности adjMat размера \(n \times n\) , что требует \(O(n^2)\) времени.
      • +
      • Добавление вершины: в конец матрицы смежности добавляется строка и столбец, полностью заполненные нулями. Это требует \(O(n)\) времени.
      • +
      • Удаление вершины: из матрицы смежности удаляется строка и столбец. При удалении первой строки и первого столбца достигается худший случай, когда требуется «сдвинуть влево вверх» \((n-1)^2\) элементов, поэтому используется \(O(n^2)\) времени.
      • +
      • Инициализация: передаются \(n\) вершин, затем инициализируется список вершин vertices длины \(n\) , что требует \(O(n)\) времени. После этого инициализируется матрица смежности adjMat размера \(n \times n\) , что требует \(O(n^2)\) времени.
      @@ -5566,13 +5566,13 @@

      9.2.2   Реализация на основе списка смежности

      -

      Пусть неориентированный граф содержит в сумме \(n\) вершин и \(m\) ребер. Тогда различные операции можно реализовать способом, показанным на рисунках ниже.

      +

      Пусть неориентированный граф содержит в сумме \(n\) вершин и \(m\) ребер. Тогда различные операции можно реализовать способом, показанным на рисунке 9-8.

        -
      • Добавление ребра: достаточно добавить ребро в конец списка, соответствующего вершине; это требует \(O(1)\) времени. Поскольку граф неориентированный, необходимо одновременно добавить ребра в обоих направлениях.
      • -
      • Удаление ребра: нужно найти и удалить указанное ребро в списке, соответствующем вершине; это требует \(O(m)\) времени. В неориентированном графе необходимо удалить ребра в обоих направлениях.
      • -
      • Добавление вершины: в список смежности добавляется еще один список, а новая вершина становится его головным узлом; это требует \(O(1)\) времени.
      • -
      • Удаление вершины: требуется пройти по всему списку смежности и удалить все ребра, содержащие указанную вершину; это требует \(O(n + m)\) времени.
      • -
      • Инициализация: в списке смежности создаются \(n\) вершин и \(2m\) ребер; это требует \(O(n + m)\) времени.
      • +
      • Добавление ребра: достаточно добавить ребро в конец списка, соответствующего вершине. Это требует \(O(1)\) времени. Поскольку граф неориентированный, необходимо одновременно добавить ребра в обоих направлениях.
      • +
      • Удаление ребра: нужно найти и удалить указанное ребро в списке, соответствующем вершине. Это требует \(O(m)\) времени. В неориентированном графе необходимо удалить ребра в обоих направлениях.
      • +
      • Добавление вершины: в список смежности добавляется еще один список, а новая вершина становится его головным узлом. Это требует \(O(1)\) времени.
      • +
      • Удаление вершины: требуется пройти по всему списку смежности и удалить все ребра, содержащие указанную вершину. Это требует \(O(n + m)\) времени.
      • +
      • Инициализация: в списке смежности создаются \(n\) вершин и \(2m\) ребер. Это требует \(O(n + m)\) времени.
      @@ -5595,7 +5595,7 @@

      Рисунок 9-8   Инициализация списка смежности, добавление и удаление ребер и вершин

      -

      Ниже приведен код списка смежности. По сравнению с рисунками выше, реальная реализация имеет следующие отличия.

      +

      Ниже приведен код списка смежности. По сравнению с тем, что показано на рисунке 9-8, реальная реализация имеет следующие отличия.

      • Чтобы упростить добавление и удаление вершин, а также сделать код проще, мы используем список, то есть динамический массив, вместо связного списка.
      • Для хранения списка смежности используется хеш-таблица, где key - это экземпляр вершины, а value - список смежных вершин данной вершины.
      • @@ -6757,7 +6757,7 @@
      -

      Если смотреть только на таблицу, может показаться, что список смежности на основе хеш-таблицы является лучшим и по времени, и по памяти. Но на практике операции над ребрами в матрице смежности обычно выполняются быстрее, потому что там нужен лишь один доступ к массиву или одно присваивание. В целом матрица смежности воплощает принцип "обмена пространства на время", а список смежности - принцип "обмена времени на пространство".

      +

      Если судить только по данным в таблице 9-2, может показаться, что список смежности на основе хеш-таблицы является лучшим и по времени, и по памяти. Но на практике операции над ребрами в матрице смежности обычно выполняются быстрее, потому что там нужен лишь один доступ к массиву или одно присваивание. В целом матрица смежности воплощает принцип «обмена пространства на время», а список смежности - принцип «обмена времени на пространство».

      diff --git a/ru/chapter_graph/graph_traversal/index.html b/ru/chapter_graph/graph_traversal/index.html index ad62a418e..d803dfe38 100644 --- a/ru/chapter_graph/graph_traversal/index.html +++ b/ru/chapter_graph/graph_traversal/index.html @@ -4469,7 +4469,7 @@

      9.3   Обход графа

      -

      Дерево представляет отношение "один ко многим", тогда как граф обладает большей свободой и может выражать произвольные отношения "многие ко многим". Поэтому дерево можно рассматривать как частный случай графа. Очевидно, что операции обхода дерева также являются частным случаем операций обхода графа.

      +

      Дерево представляет отношение «один ко многим», тогда как граф обладает большей свободой и может выражать произвольные отношения «многие ко многим». Поэтому дерево можно рассматривать как частный случай графа. Очевидно, что операции обхода дерева также являются частным случаем операций обхода графа.

      И графы, и деревья требуют применения алгоритмов обхода. Способы обхода графа также делятся на два типа: обход в ширину и обход в глубину.

      9.3.1   Обход в ширину

      Обход в ширину - это способ обхода от ближнего к дальнему, при котором начиная с некоторого узла сначала посещают ближайшие вершины, а затем слой за слоем расширяются наружу. Как показано на рисунке 9-9, начиная с вершины в левом верхнем углу, мы сначала обходим все смежные вершины этой вершины, затем все смежные вершины следующей вершины и так далее, пока не будут посещены все вершины.

      @@ -4477,7 +4477,7 @@

      Рисунок 9-9   Обход графа в ширину

      1.   Реализация алгоритма

      -

      BFS обычно реализуется с помощью очереди, код приведен ниже. Очередь обладает свойством "первым пришел - первым вышел", что хорошо соответствует идее BFS "от ближнего к дальнему".

      +

      BFS обычно реализуется с помощью очереди, код приведен ниже. Очередь обладает свойством «первым пришел - первым вышел», что хорошо соответствует идее BFS «от ближнего к дальнему».

      1. Поместить стартовую вершину обхода startVet в очередь и запустить цикл.
      2. На каждой итерации цикла извлекать вершину из головы очереди и записывать ее посещение, после чего добавлять все смежные вершины этой вершины в хвост очереди.
      3. @@ -4916,7 +4916,7 @@

        -

        Код сравнительно абстрактен, поэтому рекомендуется сверяться с рисунками ниже для лучшего понимания.

        +

        Код сравнительно абстрактен, поэтому для лучшего понимания рекомендуется сопоставлять его с тем, что показано на рисунке 9-10.

        @@ -4958,10 +4958,10 @@

        Является ли последовательность обхода в ширину единственной?

        -

        Нет. Обход в ширину требует только соблюдения порядка "от ближнего к дальнему", а порядок обхода нескольких вершин на одинаковом расстоянии может произвольно меняться. Например, на рисунке 9-10 можно поменять местами порядок посещения вершин \(1\) и \(3\) , а вершины \(2\), \(4\), \(6\) также можно переставлять произвольно.

        +

        Нет. Обход в ширину требует только соблюдения порядка «от ближнего к дальнему», а порядок обхода нескольких вершин на одинаковом расстоянии может произвольно меняться. Например, на рисунке 9-10 можно поменять местами порядок посещения вершин \(1\) и \(3\) , а вершины \(2\), \(4\), \(6\) также можно переставлять произвольно.

        2.   Анализ сложности

        -

        Временная сложность: все вершины по одному разу помещаются в очередь и извлекаются из нее, что требует \(O(|V|)\) времени; при обходе смежных вершин, поскольку граф неориентированный, все ребра будут посещены по \(2\) раза, что требует \(O(2|E|)\) времени; в сумме получается \(O(|V| + |E|)\) .

        +

        Временная сложность: все вершины по одному разу помещаются в очередь и извлекаются из нее, что требует \(O(|V|)\) времени. При обходе смежных вершин, поскольку граф неориентированный, все ребра будут посещены по \(2\) раза, что требует \(O(2|E|)\) времени. В сумме получается \(O(|V| + |E|)\) .

        Пространственная сложность: список res , хеш-множество visited и очередь que в худшем случае могут содержать до \(|V|\) вершин, поэтому требуется \(O(|V|)\) памяти.

        9.3.2   Обход в глубину

        Обход в глубину - это способ обхода, при котором сначала идут до самого конца, а когда дальше идти нельзя, возвращаются назад. Как показано на рисунке 9-11, начиная с вершины в левом верхнем углу, мы выбираем некоторую смежную вершину текущей вершины, идем до упора, затем возвращаемся назад, снова идем до упора и так далее, пока не будут посещены все вершины.

        @@ -4969,7 +4969,7 @@

        Рисунок 9-11   Обход графа в глубину

        1.   Реализация алгоритма

        -

        Такой алгоритмический шаблон "дойти до конца и вернуться" обычно реализуется через рекурсию. Подобно обходу в ширину, в обходе в глубину мы также используем хеш-множество visited для записи уже посещенных вершин и тем самым избегаем повторного посещения.

        +

        Такой алгоритмический шаблон «дойти до конца и вернуться» обычно реализуется через рекурсию. Подобно обходу в ширину, в обходе в глубину мы также используем хеш-множество visited для записи уже посещенных вершин и тем самым избегаем повторного посещения.

        @@ -5349,12 +5349,12 @@

        -

        Алгоритмический процесс обхода в глубину показан на рисунках ниже.

        +

        Алгоритмический процесс обхода в глубину показан на рисунке 9-12.

        • Прямая пунктирная линия обозначает нисходящую рекурсию , то есть запуск нового рекурсивного метода для посещения новой вершины.
        • Изогнутая пунктирная линия обозначает восходящую рекурсию , то есть данный рекурсивный метод завершился и управление вернулось туда, откуда он был вызван.
        -

        Чтобы лучше понять алгоритм, рекомендуется совместить рисунки ниже с кодом и мысленно проследить весь процесс DFS, включая моменты запуска и возврата каждого рекурсивного вызова.

        +

        Чтобы лучше понять алгоритм, рекомендуется сопоставить код с тем, что показано на рисунке 9-12, и мысленно проследить весь процесс DFS, включая моменты запуска и возврата каждого рекурсивного вызова.

        @@ -5397,10 +5397,10 @@

        Является ли последовательность обхода в глубину единственной?

        Как и в случае обхода в ширину, последовательность DFS тоже не является единственной. Для заданной вершины допустимо сначала углубиться в любое направление, то есть порядок смежных вершин может быть произвольным, и все такие варианты будут корректными обходами в глубину.

        -

        Если взять в качестве примера обход дерева, то варианты "корень \(\rightarrow\) лево \(\rightarrow\) право", "лево \(\rightarrow\) корень \(\rightarrow\) право" и "лево \(\rightarrow\) право \(\rightarrow\) корень" соответствуют прямому, симметричному и обратному обходам соответственно. Они показывают три разных приоритета обхода, но все они относятся к обходу в глубину.

        +

        Если взять в качестве примера обход дерева, то варианты «корень \(\rightarrow\) лево \(\rightarrow\) право», «лево \(\rightarrow\) корень \(\rightarrow\) право» и «лево \(\rightarrow\) право \(\rightarrow\) корень» соответствуют прямому, симметричному и обратному обходам соответственно. Они показывают три разных приоритета обхода, но все они относятся к обходу в глубину.

        2.   Анализ сложности

        -

        Временная сложность: все вершины будут посещены по \(1\) разу, что требует \(O(|V|)\) времени; все ребра будут посещены по \(2\) раза, что требует \(O(2|E|)\) времени; суммарно получается \(O(|V| + |E|)\) .

        +

        Временная сложность: все вершины будут посещены по \(1\) разу, что требует \(O(|V|)\) времени. Все ребра будут посещены по \(2\) раза, что требует \(O(2|E|)\) времени. Суммарно получается \(O(|V| + |E|)\) .

        Пространственная сложность: число вершин в списке res и хеш-множестве visited в худшем случае достигает \(|V|\) , максимальная глубина рекурсии тоже равна \(|V|\) , поэтому требуется \(O(|V|)\) памяти.

        diff --git a/ru/chapter_graph/summary/index.html b/ru/chapter_graph/summary/index.html index 9b8ce3b9f..bd1df2300 100644 --- a/ru/chapter_graph/summary/index.html +++ b/ru/chapter_graph/summary/index.html @@ -4363,22 +4363,22 @@
      4. По сравнению с линейными отношениями (связный список) и отношениями разделения (дерево), сетевые отношения (граф) обладают большей свободой и потому более сложны.
      5. Ребра ориентированного графа имеют направление, в связном графе любые вершины достижимы, а во взвешенном графе каждое ребро содержит переменную веса.
      6. Матрица смежности использует матрицу для представления графа: каждая строка и каждый столбец соответствуют вершине, а элементы матрицы показывают, есть между двумя вершинами ребро или нет. Матрица смежности эффективна в операциях добавления, удаления, поиска и изменения, но расходует больше памяти.
      7. -
      8. Список смежности использует несколько списков для представления графа; \(i\)-й список соответствует вершине \(i\) и хранит все ее смежные вершины. По сравнению с матрицей смежности список смежности экономит пространство, но для поиска ребра в нем приходится обходить список, поэтому по времени он уступает.
      9. +
      10. Список смежности использует несколько списков для представления графа. \(i\)-й список соответствует вершине \(i\) и хранит все ее смежные вершины. По сравнению с матрицей смежности список смежности экономит пространство, но для поиска ребра в нем приходится обходить список, поэтому по времени он уступает.
      11. Когда списки в списке смежности становятся слишком длинными, их можно преобразовать в красно-черное дерево или хеш-таблицу, чтобы повысить эффективность поиска.
      12. -
      13. С точки зрения алгоритмической идеи матрица смежности отражает принцип "обмена пространства на время", а список смежности - принцип "обмена времени на пространство".
      14. +
      15. С точки зрения алгоритмической идеи матрица смежности отражает принцип «обмена пространства на время», а список смежности - принцип «обмена времени на пространство».
      16. Графы можно использовать для моделирования различных реальных систем, таких как социальные сети, линии метро и так далее.
      17. Дерево является частным случаем графа, а обход дерева - частным случаем обхода графа.
      18. Обход графа в ширину представляет собой способ поиска, который расширяется от ближнего к дальнему и обычно реализуется с помощью очереди.
      19. -
      20. Обход графа в глубину представляет собой способ поиска, который сначала идет до самого конца, а затем возвращается назад, когда путь исчерпан; обычно он реализуется на основе рекурсии.
      21. +
      22. Обход графа в глубину представляет собой способ поиска, который сначала идет до самого конца, а затем возвращается назад, когда путь исчерпан. Обычно он реализуется на основе рекурсии.

    2.   Q & A

    Q: Что считается путем: последовательность вершин или последовательность ребер?

    -

    Определение в разных языковых версиях Википедии различается: в английской версии путь определяется как "последовательность ребер", а в русской версии - как "последовательность вершин". В английской версии исходная формулировка выглядит так: In graph theory, a path in a graph is a finite or infinite sequence of edges which joins a sequence of vertices.

    +

    Определение в разных языковых версиях Википедии различается: в английской версии путь определяется как «последовательность ребер», а в русской версии - как «последовательность вершин». В английской версии исходная формулировка выглядит так: In graph theory, a path in a graph is a finite or infinite sequence of edges which joins a sequence of vertices.

    В этой книге путь рассматривается как последовательность ребер, а не как последовательность вершин. Причина в том, что между двумя вершинами может существовать несколько ребер, и в таком случае каждому ребру соответствует свой путь.

    Q: Есть ли в несвязном графе вершины, до которых нельзя дойти?

    В несвязном графе, начиная из некоторой вершины, по крайней мере одна вершина оказывается недостижимой. Чтобы обойти весь несвязный граф, нужно задать несколько стартовых точек и обойти все связные компоненты графа.

    -

    Q: Есть ли требования к порядку вершин в списке "всех вершин, соединенных с данной вершиной" в списке смежности?

    -

    Порядок может быть произвольным. Но на практике может понадобиться сортировка по определенному правилу, например по порядку добавления вершин или по возрастанию значений вершин; это помогает быстро находить вершины с некоторым экстремальным свойством.

    +

    Q: Есть ли требования к порядку вершин в списке «всех вершин, соединенных с данной вершиной» в списке смежности?

    +

    Порядок может быть произвольным. Но на практике может понадобиться сортировка по определенному правилу, например по порядку добавления вершин или по возрастанию значений вершин. Это помогает быстро находить вершины с некоторым экстремальным свойством.

    diff --git a/ru/chapter_greedy/fractional_knapsack_problem/index.html b/ru/chapter_greedy/fractional_knapsack_problem/index.html index edee8df6c..10cb4882c 100644 --- a/ru/chapter_greedy/fractional_knapsack_problem/index.html +++ b/ru/chapter_greedy/fractional_knapsack_problem/index.html @@ -4396,7 +4396,7 @@

    Рисунок 15-4   Ценность предмета на единицу веса

    1.   Определение жадной стратегии

    -

    Максимизация общей ценности предметов в рюкзаке по сути равносильна максимизации ценности на единицу веса. Отсюда естественно выводится следующая жадная стратегия.

    +

    Максимизация общей ценности предметов в рюкзаке по сути равносильна максимизации ценности на единицу веса. Отсюда естественно выводится жадная стратегия, показанная на рисунке 15-5.

    1. Отсортировать предметы по убыванию удельной ценности.
    2. Перебирать все предметы и на каждом шаге жадно выбирать предмет с наибольшей удельной ценностью.
    3. diff --git a/ru/chapter_greedy/greedy_algorithm/index.html b/ru/chapter_greedy/greedy_algorithm/index.html index abb48941c..d7c2d0260 100644 --- a/ru/chapter_greedy/greedy_algorithm/index.html +++ b/ru/chapter_greedy/greedy_algorithm/index.html @@ -4708,7 +4708,7 @@

      У вас может невольно вырваться: «Эврика!» Жадный алгоритм решает задачу размена монет всего примерно десятью строками кода.

      15.1.1   Преимущества и ограничения жадного алгоритма

      Жадный алгоритм не только прост в реализации, но и обычно очень эффективен. В приведенном выше коде обозначим минимальный номинал монеты через \(\min(coins)\), тогда жадный выбор выполняется не более чем \(amt / \min(coins)\) раз, а временная сложность равна \(O(amt / \min(coins))\). Это на порядок меньше, чем временная сложность решения через динамическое программирование \(O(n \times amt)\).

      -

      Однако для некоторых наборов номиналов монет жадный алгоритм не может найти оптимальный ответ. Ниже показаны два примера.

      +

      Однако для некоторых наборов номиналов монет жадный алгоритм не может найти оптимальный ответ. На рисунке 15-2 показаны два примера.

      • Положительный пример \(coins = [1, 5, 10, 20, 50, 100]\): для такого набора монет при любом \(amt\) жадный алгоритм находит оптимальное решение.
      • Отрицательный пример \(coins = [1, 20, 50]\): пусть \(amt = 60\). Жадный алгоритм найдет только комбинацию \(50 + 1 \times 10\), то есть всего \(11\) монет, тогда как динамическое программирование находит оптимум \(20 + 20 + 20\), где требуется лишь \(3\) монеты.
      • diff --git a/ru/chapter_greedy/max_capacity_problem/index.html b/ru/chapter_greedy/max_capacity_problem/index.html index 956cf13a5..1ee8d5031 100644 --- a/ru/chapter_greedy/max_capacity_problem/index.html +++ b/ru/chapter_greedy/max_capacity_problem/index.html @@ -4409,7 +4409,7 @@ cap[i, j] = \min(ht[i], ht[j]) \times (j - i)

        Рисунок 15-10   Состояние после перемещения короткой перегородки внутрь

        Отсюда и выводится жадная стратегия для этой задачи: инициализировать два указателя по краям контейнера и на каждом шаге сдвигать внутрь указатель, соответствующий короткой перегородке, пока указатели не встретятся.

        -

        На рисунках ниже показан процесс выполнения этой жадной стратегии.

        +

        На рисунке 15-11 показан процесс выполнения этой жадной стратегии.

        1. В начальном состоянии указатели \(i\) и \(j\) стоят на двух концах массива.
        2. Вычислить вместимость текущего состояния \(cap[i, j]\) и обновить максимальную вместимость.
        3. diff --git a/ru/chapter_hashing/hash_algorithm/index.html b/ru/chapter_hashing/hash_algorithm/index.html index 649299288..38ed3d2c8 100644 --- a/ru/chapter_hashing/hash_algorithm/index.html +++ b/ru/chapter_hashing/hash_algorithm/index.html @@ -4402,7 +4402,7 @@

          6.3   Алгоритмы хеширования

          В двух предыдущих разделах мы рассмотрели принципы работы хеш-таблицы и способы обработки хеш-коллизий. Однако и открытая адресация, и метод цепочек лишь позволяют хеш-таблице корректно работать при возникновении коллизий, но не уменьшают вероятность появления самих коллизий.

          -

          Если хеш-коллизии происходят слишком часто, производительность хеш-таблицы резко деградирует. Как показано на рисунке 6-8, для хеш-таблицы с методом цепочек в идеальном случае пары ключ-значение равномерно распределены по всем бакетам, и это дает наилучшую эффективность поиска; в худшем же случае все пары ключ-значение оказываются в одном бакете, и временная сложность вырождается до \(O(n)\) .

          +

          Если хеш-коллизии происходят слишком часто, производительность хеш-таблицы резко деградирует. Как показано на рисунке 6-8, для хеш-таблицы с методом цепочек в идеальном случае пары ключ-значение равномерно распределены по всем бакетам, и это дает наилучшую эффективность поиска. В худшем же случае все пары ключ-значение оказываются в одном бакете, и временная сложность вырождается до \(O(n)\) .

          Лучший и худший случаи хеш-коллизий

          Рисунок 6-8   Лучший и худший случаи хеш-коллизий

          @@ -4421,7 +4421,7 @@

          На практике хеш-алгоритмы используются не только для реализации хеш-таблиц, но и во многих других областях.

          • Хранение паролей: чтобы защищать пароли пользователей, система обычно хранит не сами пароли в открытом виде, а их хеш-значения. Когда пользователь вводит пароль, система вычисляет хеш-значение введенного пароля и сравнивает его с сохраненным значением. Если они совпадают, пароль считается правильным.
          • -
          • Проверка целостности данных: отправитель может вычислить хеш-значение данных и отправить его вместе с самими данными; получатель затем вычисляет хеш-значение повторно и сравнивает его с полученным. Если они совпадают, данные считаются целостными.
          • +
          • Проверка целостности данных: отправитель может вычислить хеш-значение данных и отправить его вместе с самими данными. Получатель затем вычисляет хеш-значение повторно и сравнивает его с полученным. Если они совпадают, данные считаются целостными.

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

            @@ -4429,12 +4429,12 @@
          • Устойчивость к коллизиям: должно быть крайне трудно найти два разных входа, имеющих одинаковое хеш-значение.
          • Эффект лавины: даже небольшое изменение во входных данных должно приводить к заметному и непредсказуемому изменению результата.
          -

          Обрати внимание: "равномерное распределение" и "устойчивость к коллизиям" - это два независимых понятия , и выполнение первого не означает автоматического выполнения второго. Например, при случайном распределении входных key хеш-функция key % 100 может выдавать достаточно равномерное распределение. Однако этот хеш-алгоритм слишком прост: все key с одинаковыми двумя последними цифрами будут иметь одинаковый результат, а значит, по хеш-значению можно легко подобрать подходящие key и, например, взломать пароль.

          +

          Обрати внимание: «равномерное распределение» и «устойчивость к коллизиям» - это два независимых понятия , и выполнение первого не означает автоматического выполнения второго. Например, при случайном распределении входных key хеш-функция key % 100 может выдавать достаточно равномерное распределение. Однако этот хеш-алгоритм слишком прост: все key с одинаковыми двумя последними цифрами будут иметь одинаковый результат, а значит, по хеш-значению можно легко подобрать подходящие key и, например, взломать пароль.

          6.3.2   Проектирование хеш-алгоритма

          Разработка хеш-алгоритма - это сложная задача, в которой нужно учитывать множество факторов. Однако для некоторых нетребовательных сценариев мы можем спроектировать и несколько простых хеш-алгоритмов.

          • Аддитивный хеш: складываем ASCII-коды всех символов входной строки и используем полученную сумму как хеш-значение.
          • -
          • Мультипликативный хеш: используем "некоррелированность" умножения; на каждом шаге умножаем текущее значение на константу и добавляем ASCII-код очередного символа.
          • +
          • Мультипликативный хеш: используем «некоррелированность» умножения. На каждом шаге умножаем текущее значение на константу и добавляем ASCII-код очередного символа.
          • XOR-хеш: последовательно накапливаем элементы входных данных в одном хеш-значении через операцию XOR.
          • Ротационный хеш: последовательно накапливаем ASCII-коды символов, причем перед каждым накоплением выполняем циклический сдвиг хеш-значения.
          @@ -5021,7 +5021,7 @@ \text{hash} & = \{ 0, 3, 6, 0, 3, 6, 0, 3, 6, 0, 3, 6,\dots \} \end{aligned} \]
    -

    Если входные key как раз удовлетворяют такому распределению в виде арифметической прогрессии, то хеш-значения начнут скучиваться, а это усугубит хеш-коллизии. Теперь предположим, что мы заменили modulus на простое число \(13\) ; поскольку между key и modulus нет общих делителей, равномерность распределения хеш-значений заметно улучшится.

    +

    Если входные key как раз удовлетворяют такому распределению в виде арифметической прогрессии, то хеш-значения начнут скучиваться, а это усугубит хеш-коллизии. Теперь предположим, что мы заменили modulus на простое число \(13\). Поскольку между key и modulus нет общих делителей, равномерность распределения хеш-значений заметно улучшится.

    \[ \begin{aligned} \text{modulus} & = 13 \newline @@ -5037,7 +5037,7 @@

    На протяжении почти ста лет хеш-алгоритмы непрерывно развивались и оптимизировались. Одни исследователи старались повысить их производительность, а другие исследователи и хакеры сосредоточивались на поиске уязвимостей в их безопасности. В таблице 6-2 приведены распространенные хеш-алгоритмы, которые часто встречаются в реальных приложениях.

    • MD5 и SHA-1 уже многократно были успешно атакованы, поэтому они выведены из большинства сценариев, где требуется безопасность.
    • -
    • SHA-256 из семейства SHA-2 является одним из самых надежных хеш-алгоритмов; на сегодняшний день не известно успешных практических атак, поэтому он широко используется в самых разных протоколах и системах безопасности.
    • +
    • SHA-256 из семейства SHA-2 является одним из самых надежных хеш-алгоритмов. На сегодняшний день не известно успешных практических атак, поэтому он широко используется в самых разных протоколах и системах безопасности.
    • SHA-3 по сравнению с SHA-2 требует меньших затрат на реализацию и обеспечивает более высокую вычислительную эффективность, но на данный момент распространен слабее, чем семейство SHA-2.

    Таблица 6-2   Распространенные хеш-алгоритмы

    @@ -5096,7 +5096,7 @@

    Мы знаем, что key в хеш-таблице могут быть целыми числами, вещественными числами, строками и другими типами данных. Языки программирования обычно предоставляют встроенные хеш-алгоритмы для этих типов, чтобы вычислять индексы бакетов в хеш-таблице. Возьмем Python: в нем можно вызвать функцию hash() , чтобы вычислить хеш-значения для различных типов данных.

    • Хеш-значение целого числа и булева значения совпадает с самим значением.
    • -
    • Вычисление хеш-значений для вещественных чисел и строк устроено сложнее; интересующиеся читатели могут изучить это самостоятельно.
    • +
    • Вычисление хеш-значений для вещественных чисел и строк устроено сложнее. Интересующиеся читатели могут изучить это самостоятельно.
    • Хеш-значение кортежа получается путем хеширования каждого элемента, а затем объединения этих хеш-значений в одно итоговое значение.
    • Хеш-значение объекта обычно строится на основе его адреса в памяти. Если переопределить метод хеширования объекта, можно реализовать вычисление хеша по содержимому.
    diff --git a/ru/chapter_hashing/hash_collision/index.html b/ru/chapter_hashing/hash_collision/index.html index dc09e3e00..a7bb34139 100644 --- a/ru/chapter_hashing/hash_collision/index.html +++ b/ru/chapter_hashing/hash_collision/index.html @@ -5950,13 +5950,13 @@

    Следует отметить, что когда связный список становится очень длинным, эффективность поиска \(O(n)\) оказывается низкой. В этом случае список можно преобразовать в AVL-дерево или красно-черное дерево , чтобы оптимизировать временную сложность поиска до \(O(\log n)\) .

    6.2.2   Открытая адресация

    -

    Открытая адресация (open addressing) не вводит дополнительных структур данных, а обрабатывает хеш-коллизии с помощью многократного пробирования; основные варианты пробирования включают линейное пробирование, квадратичное пробирование и повторное хеширование.

    +

    Открытая адресация (open addressing) не вводит дополнительных структур данных, а обрабатывает хеш-коллизии с помощью многократного пробирования. Основные варианты пробирования включают линейное пробирование, квадратичное пробирование и повторное хеширование.

    Ниже на примере линейного пробирования рассмотрим механизм работы хеш-таблицы с открытой адресацией.

    1.   Линейное пробирование

    Линейное пробирование использует линейный поиск с фиксированным шагом. Его методы работы отличаются от обычной хеш-таблицы.

      -
    • Вставка элемента: по хеш-функции вычисляется индекс бакета; если бакет уже занят, то от места конфликта выполняется линейный обход вперед (шаг обычно равен \(1\) ), пока не будет найден пустой бакет, после чего элемент вставляется туда.
    • -
    • Поиск элемента: если возник конфликт, то с тем же шагом продолжается линейный обход вперед, пока не будет найден целевой элемент и возвращено value ; если встречается пустой бакет, это означает, что искомого элемента в хеш-таблице нет, и возвращается None .
    • +
    • Вставка элемента: по хеш-функции вычисляется индекс бакета. Если бакет уже занят, то от места конфликта выполняется линейный обход вперед (шаг обычно равен \(1\) ), пока не будет найден пустой бакет, после чего элемент вставляется туда.
    • +
    • Поиск элемента: если возник конфликт, то с тем же шагом продолжается линейный обход вперед, пока не будет найден целевой элемент и возвращено value. Если встречается пустой бакет, это означает, что искомого элемента в хеш-таблице нет, и возвращается None .

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

    Распределение пар ключ-значение в хеш-таблице с открытой адресацией (линейное пробирование)

    @@ -5968,7 +5968,7 @@

    Рисунок 6-7   Проблема поиска после удаления элемента в открытой адресации

    Чтобы решить эту проблему, можно использовать механизм ленивого удаления (lazy deletion): он не удаляет элемент из хеш-таблицы напрямую, **а помечает этот бакет специальной константой TOMBSTONE **. В этом механизме и None , и TOMBSTONE означают пустой бакет, и оба могут быть использованы для размещения пары ключ-значение. Но есть важное различие: при линейном пробировании, встретив TOMBSTONE , нужно продолжать обход, потому что ниже него все еще могут существовать пары ключ-значение.

    -

    Однако ленивое удаление может ускорять деградацию производительности хеш-таблицы. Это связано с тем, что каждая операция удаления создает новую метку удаления; по мере роста числа TOMBSTONE время поиска тоже увеличивается, потому что линейное пробирование может быть вынуждено перескакивать через множество TOMBSTONE , прежде чем найдет целевой элемент.

    +

    Однако ленивое удаление может ускорять деградацию производительности хеш-таблицы. Это связано с тем, что каждая операция удаления создает новую метку удаления. По мере роста числа TOMBSTONE время поиска тоже увеличивается, потому что линейное пробирование может быть вынуждено перескакивать через множество TOMBSTONE , прежде чем найдет целевой элемент.

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

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

    @@ -7673,7 +7673,7 @@

    2.   Квадратичное пробирование

    -

    Квадратичное пробирование похоже на линейное пробирование и тоже является одной из распространенных стратегий открытой адресации. При возникновении конфликта оно не пропускает фиксированное число шагов, а переходит на расстояние, равное "квадрату числа попыток", то есть на \(1, 4, 9, \dots\) шагов.

    +

    Квадратичное пробирование похоже на линейное пробирование и тоже является одной из распространенных стратегий открытой адресации. При возникновении конфликта оно не пропускает фиксированное число шагов, а переходит на расстояние, равное «квадрату числа попыток», то есть на \(1, 4, 9, \dots\) шагов.

    Квадратичное пробирование имеет следующие основные преимущества.

    • Квадратичное пробирование пытается смягчить эффект кластеризации линейного пробирования, так как пропускает расстояния, равные квадрату номера попытки.
    • @@ -7688,7 +7688,7 @@

      Как видно из названия, метод повторного хеширования использует для пробирования несколько хеш-функций \(f_1(x)\), \(f_2(x)\), \(f_3(x)\), \(\dots\) .

      • Вставка элемента: если хеш-функция \(f_1(x)\) вызывает конфликт, то пробуем \(f_2(x)\) , и так далее, пока не будет найдено пустое место для вставки элемента.
      • -
      • Поиск элемента: поиск идет в том же порядке хеш-функций, пока не будет найден целевой элемент; если встречается пустая позиция или уже были опробованы все хеш-функции, это означает, что элемента в хеш-таблице нет, и возвращается None .
      • +
      • Поиск элемента: поиск идет в том же порядке хеш-функций, пока не будет найден целевой элемент. Если встречается пустая позиция или уже были опробованы все хеш-функции, это означает, что элемента в хеш-таблице нет, и возвращается None .

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

      @@ -7700,7 +7700,7 @@
      • Python использует открытую адресацию. В словаре dict для пробирования применяются псевдослучайные числа.
      • Java использует метод цепочек. Начиная с JDK 1.8, когда длина массива внутри HashMap достигает 64, а длина списка достигает 8, этот список преобразуется в красно-черное дерево для повышения производительности поиска.
      • -
      • Go использует метод цепочек. В Go установлено, что каждый бакет может хранить не более 8 пар ключ-значение; при переполнении подключается overflow-бакет, а когда таких бакетов становится слишком много, выполняется специальное расширение того же масштаба, чтобы сохранить производительность.
      • +
      • Go использует метод цепочек. В Go установлено, что каждый бакет может хранить не более 8 пар ключ-значение. При переполнении подключается overflow-бакет, а когда таких бакетов становится слишком много, выполняется специальное расширение того же масштаба, чтобы сохранить производительность.
      diff --git a/ru/chapter_hashing/hash_map/index.html b/ru/chapter_hashing/hash_map/index.html index f7d6edd84..af7081a13 100644 --- a/ru/chapter_hashing/hash_map/index.html +++ b/ru/chapter_hashing/hash_map/index.html @@ -4380,7 +4380,7 @@

      6.1   Хеш-таблица

      Хеш-таблица (hash table), также называемая таблицей рассеяния, реализует эффективный поиск элементов за счет установления соответствия между ключом key и значением value . Иначе говоря, если передать в хеш-таблицу ключ key , то можно за \(O(1)\) времени получить соответствующее значение value .

      -

      Как показано на рисунке 6-1, пусть есть \(n\) студентов, и у каждого из них есть два поля данных: имя и номер студенческого билета. Если мы хотим реализовать запрос вида "ввести номер студенческого билета и вернуть соответствующее имя", то для этого можно использовать показанную ниже хеш-таблицу.

      +

      Как показано на рисунке 6-1, пусть есть \(n\) студентов, и у каждого из них есть два поля данных: имя и номер студенческого билета. Если мы хотим реализовать запрос вида «ввести номер студенческого билета и вернуть соответствующее имя», то для этого можно воспользоваться хеш-таблицей, изображенной на рисунке 6-1.

      Абстрактное представление хеш-таблицы

      Рисунок 6-1   Абстрактное представление хеш-таблицы

      @@ -4905,7 +4905,7 @@
      index = hash(key) % capacity
       

      После этого можно использовать index для доступа к соответствующему бакету в хеш-таблице и получения value .

      -

      Пусть длина массива capacity = 100 , а хеш-алгоритм hash(key) = key . Тогда легко получить хеш-функцию key % 100 . На рисунке 6-2 на примере key "номер студенческого билета" и value "имя" показан принцип работы хеш-функции.

      +

      Пусть длина массива capacity = 100 , а хеш-алгоритм hash(key) = key . Тогда легко получить хеш-функцию key % 100 . На рисунке 6-2 на примере key «номер студенческого билета» и value «имя» показан принцип работы хеш-функции.

      Принцип работы хеш-функции

      Рисунок 6-2   Принцип работы хеш-функции

      @@ -6072,7 +6072,7 @@

      6.1.3   Хеш-коллизии и расширение

      -

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

      +

      По сути, хеш-функция отображает входное пространство, состоящее из всех key , в выходное пространство, состоящее из всех индексов массива, а входное пространство обычно значительно больше выходного. Поэтому теоретически неизбежно существование ситуации «несколько входов соответствуют одному выходу».

      Для хеш-функции из приведенного выше примера, если последние две цифры key совпадают, то совпадает и результат хеш-функции. Например, если искать студентов с номерами 12836 и 20336, то получим:

      12836 % 100 = 36
       20336 % 100 = 36
      @@ -6086,7 +6086,7 @@
       

      Расширение хеш-таблицы

      Рисунок 6-4   Расширение хеш-таблицы

      -

      Подобно расширению массива, расширение хеш-таблицы требует перенести все пары ключ-значение из старой таблицы в новую, а это очень затратно по времени; кроме того, поскольку емкость хеш-таблицы capacity изменилась, нам приходится с помощью хеш-функции заново вычислять позиции хранения всех пар ключ-значение, что дополнительно увеличивает вычислительные расходы процесса расширения. Поэтому языки программирования обычно заранее резервируют достаточно большую емкость хеш-таблицы, чтобы избежать частых расширений.

      +

      Подобно расширению массива, расширение хеш-таблицы требует перенести все пары ключ-значение из старой таблицы в новую, а это очень затратно по времени. Кроме того, поскольку емкость хеш-таблицы capacity изменилась, нам приходится с помощью хеш-функции заново вычислять позиции хранения всех пар ключ-значение, что дополнительно увеличивает вычислительные расходы процесса расширения. Поэтому языки программирования обычно заранее резервируют достаточно большую емкость хеш-таблицы, чтобы избежать частых расширений.

      Коэффициент загрузки (load factor) - важное понятие хеш-таблицы. Он определяется как отношение числа элементов в хеш-таблице к числу бакетов и используется для оценки степени серьезности хеш-коллизий, а также часто служит условием срабатывания расширения хеш-таблицы. Например, в Java, когда коэффициент загрузки превышает \(0.75\) , система расширяет хеш-таблицу до \(2\) раз от исходной емкости.

      diff --git a/ru/chapter_hashing/summary/index.html b/ru/chapter_hashing/summary/index.html index cbb8bcaba..62973d7b0 100644 --- a/ru/chapter_hashing/summary/index.html +++ b/ru/chapter_hashing/summary/index.html @@ -4362,7 +4362,7 @@
    • Передав key , мы можем получить value из хеш-таблицы за \(O(1)\) времени, поэтому она очень эффективна.
    • К типичным операциям хеш-таблицы относятся поиск, добавление пары ключ-значение, удаление пары ключ-значение и обход хеш-таблицы.
    • Хеш-функция отображает key в индекс массива, после чего можно обратиться к соответствующему бакету и получить value .
    • -
    • Два разных key после хеш-функции могут дать один и тот же индекс массива, что приводит к ошибочному результату поиска; это явление называется хеш-коллизией.
    • +
    • Два разных key после хеш-функции могут дать один и тот же индекс массива, что приводит к ошибочному результату поиска. Это явление называется хеш-коллизией.
    • Чем больше емкость хеш-таблицы, тем ниже вероятность хеш-коллизий. Поэтому хеш-коллизии можно смягчать путем расширения хеш-таблицы. Как и у массива, операция расширения у хеш-таблицы очень затратна.
    • Коэффициент загрузки определяется как отношение числа элементов в хеш-таблице к числу бакетов, отражает степень серьезности хеш-коллизий и часто используется как условие запуска расширения хеш-таблицы.
    • Метод цепочек превращает одиночный элемент в связный список и хранит все конфликтующие элементы в одном списке. Однако слишком длинный список снижает эффективность поиска, поэтому его можно дополнительно преобразовать в красно-черное дерево.
    • @@ -4382,12 +4382,12 @@

      Во-первых, у хеш-таблицы повышается временная эффективность, но снижается пространственная эффективность. Значительная часть ее памяти остается неиспользованной.

      Во-вторых, она быстрее только в определенных сценариях. Если одну и ту же задачу можно реализовать на массиве или связном списке с той же асимптотикой, то часто такая реализация окажется быстрее, чем хеш-таблица. Причина в том, что вычисление хеш-функции само по себе стоит времени, то есть константа в сложности получается выше.

      Наконец, временная сложность хеш-таблицы тоже может деградировать. Например, при методе цепочек мы все равно выполняем поиск в связном списке или красно-черном дереве, поэтому риск деградации до \(O(n)\) сохраняется.

      -

      Q: Есть ли у повторного хеширования недостаток "нельзя напрямую удалять элементы"? Можно ли повторно использовать место, помеченное как удаленное?

      +

      Q: Есть ли у повторного хеширования недостаток «нельзя напрямую удалять элементы»? Можно ли повторно использовать место, помеченное как удаленное?

      Повторное хеширование - это разновидность открытой адресации, а у всех методов открытой адресации есть недостаток: элементы нельзя удалять напрямую, поэтому приходится использовать метку удаления. Пространство, помеченное как удаленное, можно использовать повторно. Когда новый элемент вставляется в хеш-таблицу и в процессе пробирования попадает на такую отмеченную позицию, эта позиция может быть занята новым элементом. Такой подход сохраняет последовательность пробирования и одновременно поддерживает приемлемую эффективность использования памяти.

      Q: Почему при линейном пробировании во время поиска элемента вообще возникает хеш-коллизия?

      Во время поиска мы через хеш-функцию находим соответствующий бакет и соответствующую пару ключ-значение, но видим, что key не совпадает, а это и означает наличие хеш-коллизии. Поэтому метод линейного пробирования в соответствии с заранее заданным шагом последовательно движется дальше, пока не найдет правильную пару ключ-значение или не убедится, что поиск завершился неудачей.

      Q: Почему расширение хеш-таблицы помогает смягчать хеш-коллизии?

      -

      Последний шаг хеш-функции обычно состоит во взятии по модулю длины массива \(n\) , чтобы результат попадал в диапазон индексов массива; после расширения длина массива \(n\) меняется, а значит, может измениться и индекс, соответствующий данному key . Несколько key , которые раньше попадали в один бакет, после расширения могут распределиться по нескольким бакетам, и тем самым хеш-коллизии будут ослаблены.

      +

      Последний шаг хеш-функции обычно состоит во взятии по модулю длины массива \(n\) , чтобы результат попадал в диапазон индексов массива. После расширения длина массива \(n\) меняется, а значит, может измениться и индекс, соответствующий данному key . Несколько key , которые раньше попадали в один бакет, после расширения могут распределиться по нескольким бакетам, и тем самым хеш-коллизии будут ослаблены.

      Q: Если нам нужен быстрый доступ, почему бы просто не использовать массив?

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

      diff --git a/ru/chapter_heap/build_heap/index.html b/ru/chapter_heap/build_heap/index.html index 0e78236f0..74662eb6c 100644 --- a/ru/chapter_heap/build_heap/index.html +++ b/ru/chapter_heap/build_heap/index.html @@ -4383,11 +4383,11 @@

      8.2.1   Реализация через операцию добавления в кучу

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

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

      -

      Пусть число элементов равно \(n\) ; так как каждая операция добавления требует \(O(\log{n})\) времени, временная сложность такого построения кучи составляет \(O(n \log n)\) .

      +

      Пусть число элементов равно \(n\). Так как каждая операция добавления требует \(O(\log{n})\) времени, временная сложность такого построения кучи составляет \(O(n \log n)\) .

      8.2.2   Реализация через обход и упорядочивание

      На самом деле можно реализовать и более эффективный способ построения кучи, который состоит из двух шагов.

        -
      1. Без изменений добавить все элементы списка в кучу; в этот момент свойства кучи еще не выполняются.
      2. +
      3. Без изменений добавить все элементы списка в кучу. В этот момент свойства кучи еще не выполняются.
      4. Обойти кучу в обратном порядке, то есть в порядке, обратном обходу по уровням, и по очереди выполнить упорядочивание сверху вниз для каждого нелистового узла.

      После того как некоторый узел был упорядочен, поддерево с этим узлом в качестве корня становится корректной подкучей. А поскольку обход выполняется в обратном порядке, куча строится снизу вверх.

      @@ -4678,11 +4678,11 @@
    • В процессе упорядочивания сверху вниз каждый узел в худшем случае может просеяться до листа, поэтому максимальное число итераций равно высоте двоичного дерева \(\log n\) .

    Перемножив эти два значения, можно получить временную сложность построения кучи \(O(n \log n)\) . Но эта оценка неточна, потому что мы не учли свойство двоичного дерева: на нижних уровнях узлов гораздо больше, чем на верхних.

    -

    Далее выполним более точный расчет. Чтобы упростить вычисления, предположим, что дано "идеальное двоичное дерево" высоты \(h\) с числом узлов \(n\) ; это предположение не повлияет на корректность результата.

    +

    Далее выполним более точный расчет. Чтобы упростить вычисления, предположим, что дано «идеальное двоичное дерево» высоты \(h\) с числом узлов \(n\). Это предположение не повлияет на корректность результата.

    Число узлов на каждом уровне идеального двоичного дерева

    Рисунок 8-5   Число узлов на каждом уровне идеального двоичного дерева

    -

    Как показано на рисунке 8-5, максимальное число итераций упорядочивания сверху вниз для некоторого узла равно расстоянию от этого узла до листового узла, а это расстояние как раз и есть высота узла. Поэтому мы можем просуммировать для каждого уровня выражение "число узлов \(\times\) высота узла" и получить суммарное число итераций упорядочивания для всех узлов.

    +

    Как показано на рисунке 8-5, максимальное число итераций упорядочивания сверху вниз для некоторого узла равно расстоянию от этого узла до листового узла, а это расстояние как раз и есть высота узла. Поэтому мы можем просуммировать для каждого уровня выражение «число узлов \(\times\) высота узла» и получить суммарное число итераций упорядочивания для всех узлов.

    \[ T(h) = 2^0h + 2^1(h-1) + 2^2(h-2) + \dots + 2^{(h-1)}\times1 \]
    diff --git a/ru/chapter_heap/heap/index.html b/ru/chapter_heap/heap/index.html index 061e2722e..96abbfcef 100644 --- a/ru/chapter_heap/heap/index.html +++ b/ru/chapter_heap/heap/index.html @@ -4538,7 +4538,7 @@

    В реальных приложениях мы можем напрямую использовать классы кучи, предоставляемые языком программирования, или классы очереди с приоритетом.

    -

    Подобно сортировкам "по возрастанию" и "по убыванию", мы можем переключаться между "минимальной кучей" и "максимальной кучей", изменяя flag или модифицируя Comparator . Код приведен ниже:

    +

    Подобно сортировкам «по возрастанию» и «по убыванию», мы можем переключаться между «минимальной кучей» и «максимальной кучей», изменяя flag или модифицируя Comparator . Код приведен ниже:

    @@ -4897,7 +4897,7 @@

    8.1.2   Реализация кучи

    Ниже реализуется максимальная куча. Чтобы преобразовать ее в минимальную кучу, достаточно инвертировать всю логику сравнений по величине, например заменить \(\geq\) на \(\leq\) . Заинтересованные читатели могут попробовать реализовать это самостоятельно.

    1.   Хранение и представление кучи

    -

    В разделе "Двоичные деревья" мы уже говорили, что полное двоичное дерево очень удобно представлять массивом. Поскольку куча как раз и является полным двоичным деревом, для хранения кучи мы также будем использовать массив.

    +

    В разделе «Двоичные деревья» мы уже говорили, что полное двоичное дерево очень удобно представлять массивом. Поскольку куча как раз и является полным двоичным деревом, для хранения кучи мы также будем использовать массив.

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

    Как показано на рисунке 8-2, для заданного индекса \(i\) индекс левого дочернего узла равен \(2i + 1\) , правого дочернего узла - \(2i + 2\) , а родительского узла - \((i - 1) / 2\) с округлением вниз. Если индекс выходит за допустимые границы, это означает пустой узел или отсутствие узла.

    Представление и хранение кучи

    @@ -5229,8 +5229,8 @@

    3.   Добавление элемента в кучу

    -

    Пусть дан элемент val . Сначала мы помещаем его в основание кучи. После добавления свойства кучи могут нарушиться, потому что val может оказаться больше, чем другие элементы в куче. Поэтому необходимо восстановить порядок на пути от вставленного узла к корню ; эта операция называется упорядочиванием кучи.

    -

    Рассмотрим ситуацию, когда упорядочивание выполняется снизу вверх, начиная от только что вставленного узла. Как показано на рисунках ниже, мы сравниваем значение вставленного узла со значением его родителя; если вставленный узел больше, то меняем их местами. Затем продолжаем выполнять ту же операцию и последовательно восстанавливать корректность всех узлов по пути снизу вверх, пока не выйдем за корень или не встретим узел, для которого обмен не требуется.

    +

    Пусть дан элемент val . Сначала мы помещаем его в основание кучи. После добавления свойства кучи могут нарушиться, потому что val может оказаться больше, чем другие элементы в куче. Поэтому необходимо восстановить порядок на пути от вставленного узла к корню. Эта операция называется упорядочиванием кучи.

    +

    Рассмотрим ситуацию, когда упорядочивание выполняется снизу вверх, начиная от только что вставленного узла. Как показано на рисунке 8-3, мы сравниваем значение вставленного узла со значением его родителя. Если вставленный узел больше, то меняем их местами. Затем продолжаем выполнять ту же операцию и последовательно восстанавливать корректность всех узлов по пути снизу вверх, пока не выйдем за корень или не встретим узел, для которого обмен не требуется.

    @@ -5618,7 +5618,7 @@
  • После обмена удалить основание кучи из списка. Стоит отметить, что, поскольку обмен уже выполнен, фактически удаляется исходный элемент вершины кучи.
  • Начиная от корневого узла, выполнить упорядочивание кучи сверху вниз.
  • -

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

    +

    Как показано на рисунке 8-4, направление операции упорядочивания кучи сверху вниз противоположно операции упорядочивания кучи снизу вверх. Мы сравниваем значение корневого узла со значениями двух дочерних узлов, выбираем больший дочерний узел и меняем его местами с корневым узлом. Затем циклически повторяем ту же операцию, пока не выйдем за листовой узел или не встретим узел, который уже не требует обмена.

    @@ -6149,8 +6149,8 @@

    8.1.3   Типичные применения кучи

      -
    • Очередь с приоритетом: куча обычно является предпочтительной структурой данных для реализации очереди с приоритетом; добавление и извлечение элементов имеют временную сложность \(O(\log n)\) , а построение кучи - \(O(n)\) , и все эти операции выполняются очень эффективно.
    • -
    • Пирамидальная сортировка: для заданного набора данных можно построить кучу, а затем непрерывно извлекать из нее элементы, получая отсортированные данные. Однако на практике мы обычно используем более изящную реализацию пирамидальной сортировки; подробности см. в разделе "Пирамидальная сортировка".
    • +
    • Очередь с приоритетом: куча обычно является предпочтительной структурой данных для реализации очереди с приоритетом. Добавление и извлечение элементов имеют временную сложность \(O(\log n)\) , а построение кучи - \(O(n)\) , и все эти операции выполняются очень эффективно.
    • +
    • Пирамидальная сортировка: для заданного набора данных можно построить кучу, а затем непрерывно извлекать из нее элементы, получая отсортированные данные. Однако на практике мы обычно используем более изящную реализацию пирамидальной сортировки. Подробности см. в разделе «Пирамидальная сортировка».
    • Получение наибольших \(k\) элементов: это классическая алгоритмическая задача и одновременно типичное применение кучи. Например, выбор 10 самых горячих новостей для списка популярных тем или выбор 10 самых продаваемых товаров.
    diff --git a/ru/chapter_heap/summary/index.html b/ru/chapter_heap/summary/index.html index d96b585cb..139ab75e7 100644 --- a/ru/chapter_heap/summary/index.html +++ b/ru/chapter_heap/summary/index.html @@ -4360,7 +4360,7 @@

    1.   Ключевые выводы

    • Куча представляет собой полное двоичное дерево и делится на максимальную кучу и минимальную кучу. Элемент на вершине максимальной (минимальной) кучи является наибольшим (наименьшим).
    • -
    • Очередь с приоритетом определяется как очередь, элементы которой извлекаются в соответствии с приоритетом; обычно ее реализуют с помощью кучи.
    • +
    • Очередь с приоритетом определяется как очередь, элементы которой извлекаются в соответствии с приоритетом. Обычно ее реализуют с помощью кучи.
    • К основным операциям кучи и их временным сложностям относятся: добавление элемента в кучу \(O(\log n)\) , извлечение элемента с вершины кучи \(O(\log n)\) и доступ к вершине кучи \(O(1)\) .
    • Полное двоичное дерево очень удобно представлять массивом, поэтому кучу обычно тоже хранят в массиве.
    • Операция упорядочивания кучи используется для поддержания свойств кучи и применяется как при добавлении элемента, так и при извлечении элемента.
    • @@ -4368,7 +4368,7 @@
    • Top-k - это классическая алгоритмическая задача, которую можно эффективно решать с помощью кучи за \(O(n \log k)\) .

    2.   Q & A

    -

    Q: Является ли "куча" как структура данных тем же самым понятием, что и "куча" в управлении памятью?

    +

    Q: Является ли «куча» как структура данных тем же самым понятием, что и «куча» в управлении памятью?

    Это не одно и то же, просто у них случайно совпало название. Куча в памяти компьютерной системы является частью динамического распределения памяти: во время выполнения программы она используется для хранения данных. Программа может запросить определенный объем памяти в куче для хранения сложных структур, таких как объекты и массивы. Когда эти данные больше не нужны, память нужно освободить, чтобы не допустить утечек. По сравнению со стековой памятью управление памятью в куче требует большей осторожности, а неправильное использование может привести к утечкам памяти и проблемам с указателями.

    diff --git a/ru/chapter_heap/top_k/index.html b/ru/chapter_heap/top_k/index.html index 5bc29b377..2eb35616f 100644 --- a/ru/chapter_heap/top_k/index.html +++ b/ru/chapter_heap/top_k/index.html @@ -4385,23 +4385,23 @@

    Для этой задачи мы сначала покажем два относительно прямолинейных способа решения, а затем более эффективный способ на основе кучи.

    8.3.1   Метод 1: выбор через обход

    -

    Как показано на рисунке 8-6, можно выполнить \(k\) проходов по массиву и на каждом проходе извлекать соответственно \(1\)-й, \(2\)-й, \(\dots\) , \(k\)-й по величине элемент; временная сложность такого подхода равна \(O(nk)\) .

    +

    Как показано на рисунке 8-6, можно выполнить \(k\) проходов по массиву и на каждом проходе извлекать соответственно \(1\)-й, \(2\)-й, \(\dots\) , \(k\)-й по величине элемент. Временная сложность такого подхода равна \(O(nk)\) .

    Этот метод подходит только для случая \(k \ll n\) , потому что когда \(k\) приближается к \(n\) , его временная сложность стремится к \(O(n^2)\) , а это уже очень затратно.

    Поиск наибольших k элементов через обход

    Рисунок 8-6   Поиск наибольших k элементов через обход

    Tip

    -

    Когда \(k = n\) , мы получаем полную упорядоченную последовательность, и в этот момент задача становится эквивалентной алгоритму "сортировка выбором".

    +

    Когда \(k = n\) , мы получаем полную упорядоченную последовательность, и в этот момент задача становится эквивалентной алгоритму «сортировка выбором».

    8.3.2   Метод 2: сортировка

    -

    Как показано на рисунке 8-7, можно сначала отсортировать массив nums , а затем вернуть его крайние правые \(k\) элементов; временная сложность такого метода равна \(O(n \log n)\) .

    +

    Как показано на рисунке 8-7, можно сначала отсортировать массив nums , а затем вернуть его крайние правые \(k\) элементов. Временная сложность такого метода равна \(O(n \log n)\) .

    Очевидно, что этот способ делает слишком много, потому что нам нужно только найти наибольшие \(k\) элементов, а сортировать остальные элементы совсем не обязательно.

    Поиск наибольших k элементов через сортировку

    Рисунок 8-7   Поиск наибольших k элементов через сортировку

    8.3.3   Метод 3: куча

    -

    Задачу Top-k можно решить гораздо эффективнее с помощью кучи, как показано на рисунках ниже.

    +

    Задачу Top-k можно решить гораздо эффективнее с помощью кучи, как показано на рисунке 8-8.

    1. Инициализировать минимальную кучу, у которой вершина содержит наименьший элемент.
    2. Сначала по очереди поместить в кучу первые \(k\) элементов массива.
    3. @@ -4812,7 +4812,7 @@

      -

      Всего выполняется \(n\) операций добавления и извлечения из кучи, а максимальная длина кучи равна \(k\) , поэтому временная сложность равна \(O(n \log k)\) . Этот метод очень эффективен: когда \(k\) мало, временная сложность стремится к \(O(n)\) ; когда \(k\) велико, она все равно не превышает \(O(n \log n)\) .

      +

      Всего выполняется \(n\) операций добавления и извлечения из кучи, а максимальная длина кучи равна \(k\) , поэтому временная сложность равна \(O(n \log k)\) . Этот метод очень эффективен: когда \(k\) мало, временная сложность стремится к \(O(n)\). Когда \(k\) велико, она все равно не превышает \(O(n \log n)\) .

      Кроме того, этот метод подходит и для сценариев с динамическим потоком данных. При непрерывном поступлении новых данных мы можем продолжать поддерживать содержимое кучи, тем самым динамически обновляя наибольшие \(k\) элементов.

      diff --git a/ru/chapter_hello_algo/index.html b/ru/chapter_hello_algo/index.html index 4cacd4be2..394238ba4 100644 --- a/ru/chapter_hello_algo/index.html +++ b/ru/chapter_hello_algo/index.html @@ -4255,19 +4255,19 @@

      Перед началом

      -

      Несколько лет назад я публиковал на LeetCode разборы серии задач "Sword for Offer" и получил поддержку и ободрение от многих читателей. Во время общения с ними мне чаще всего задавали один и тот же вопрос: "как начать изучать алгоритмы?" Постепенно этот вопрос начал меня по-настоящему занимать.

      -

      Слепо бросаться в решение задач кажется самым популярным способом: он прост, прямолинеен и действительно работает. Но решение задач похоже на игру в "Сапера": люди с сильными навыками самообучения способны обезвредить мины одну за другой, а тем, у кого не хватает базы, легко набить себе шишки и шаг за шагом отступить под давлением неудач. Полностью проходить учебники тоже принято часто, но для тех, кто готовится к поиску работы, диплом, резюме, письменные тесты и собеседования уже отнимают большую часть сил, и потому толстые книги нередко превращаются в тяжелое испытание.

      -

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

      -

      Я глубоко согласен со словами профессора Фейнмана: "Knowledge isn't free. You have to pay attention." В этом смысле книга не совсем "бесплатна". Чтобы не подвести то драгоценное "внимание", которое ты ей уделишь, я постараюсь вложить в ее создание максимум собственного "внимания".

      +

      Несколько лет назад я публиковал на LeetCode разборы серии задач «Sword for Offer» и получил поддержку и ободрение от многих читателей. Во время общения с ними мне чаще всего задавали один и тот же вопрос: «как начать изучать алгоритмы?» Постепенно этот вопрос начал меня по-настоящему занимать.

      +

      Слепо бросаться в решение задач кажется самым популярным способом: он прост, прямолинеен и действительно работает. Но решение задач похоже на игру в «Сапера»: люди с сильными навыками самообучения способны обезвредить мины одну за другой, а тем, у кого не хватает базы, легко набить себе шишки и шаг за шагом отступить под давлением неудач. Полностью проходить учебники тоже принято часто, но для тех, кто готовится к поиску работы, диплом, резюме, письменные тесты и собеседования уже отнимают большую часть сил, и потому толстые книги нередко превращаются в тяжелое испытание.

      +

      Если ты тоже сталкиваешься с такими трудностями, то можно сказать, что эта книга сама «нашла» тебя. Она стала моим ответом на этот вопрос: пусть и не идеальным, но как минимум честной и активной попыткой. Эта книга сама по себе не гарантирует предложения о работе, но поможет тебе увидеть «карту знаний» по структурам данных и алгоритмам, понять форму, размер и расположение разных «мин» и освоить разные «способы разминирования». Освоив это, ты сможешь увереннее решать задачи и читать технические материалы, шаг за шагом выстраивая целостную систему знаний.

      +

      Я глубоко согласен со словами профессора Фейнмана: «Knowledge isn't free. You have to pay attention.» В этом смысле книга не совсем «бесплатна». Чтобы не подвести то драгоценное «внимание», которое ты ей уделишь, я постараюсь вложить в ее создание максимум собственного «внимания».

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

      -

      Hello Algo

      +

      Hello Algo

      Hello, алгоритмы!

      Появление компьютеров радикально изменило мир. Благодаря высокой скорости вычислений и отличной программируемости они стали идеальной средой для исполнения алгоритмов и обработки данных. Реалистичная графика в играх, интеллектуальные решения в автономном вождении, впечатляющие партии AlphaGo и естественное взаимодействие ChatGPT: все это изящные проявления алгоритмов на компьютере.

      На самом деле еще до появления компьютеров алгоритмы и структуры данных уже существовали во всех уголках мира. Ранние алгоритмы были сравнительно простыми: например, древние способы счета или последовательности действий при изготовлении инструментов. По мере развития цивилизации алгоритмы становились тоньше и сложнее. За мастерством ремесленников, промышленными продуктами, освобождающими производительные силы, и даже за научными законами движения Вселенной почти всегда стоит изобретательная алгоритмическая мысль.

      -

      Точно так же структуры данных встречаются повсюду: от социальных сетей до схем метро многие системы можно моделировать как "граф"; от государства до семьи основные формы общественной организации обладают свойствами "дерева"; зимняя одежда похожа на "стек", где то, что надевают первым, снимают последним; тубус для бадминтонных воланов похож на "очередь", где элементы добавляются с одного конца и извлекаются с другого; словарь похож на "хеш-таблицу", позволяющую быстро находить нужную статью.

      +

      Точно так же структуры данных встречаются повсюду: от социальных сетей до схем метро многие системы можно моделировать как «граф». От государства до семьи основные формы общественной организации обладают свойствами «дерева». Зимняя одежда похожа на «стек», где то, что надевают первым, снимают последним. Тубус для бадминтонных воланов похож на «очередь», где элементы добавляются с одного конца и извлекаются с другого. Словарь похож на «хеш-таблицу», позволяющую быстро находить нужную статью.

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

      diff --git a/ru/chapter_introduction/algorithms_are_everywhere/index.html b/ru/chapter_introduction/algorithms_are_everywhere/index.html index 848ca1707..94a20f2f0 100644 --- a/ru/chapter_introduction/algorithms_are_everywhere/index.html +++ b/ru/chapter_introduction/algorithms_are_everywhere/index.html @@ -4269,45 +4269,45 @@

      1.1   Алгоритмы повсюду

      Говоря об алгоритмах, естественно вспомнить о математике. Однако на самом деле многие алгоритмы не связаны со сложной математикой, а больше полагаются на базовую логику, которая повсеместно встречается в нашей повседневной жизни.

      Прежде чем углубиться в обсуждение алгоритмов, стоит упомянуть интересный факт: вы уже точно освоили множество алгоритмов и привыкли применять их в повседневной жизни. Далее приведем несколько конкретных примеров, чтобы подтвердить этот факт.

      -

      Пример 1: поиск в словаре. В словаре все слова упорядочены по алфавиту. Предположим, нам нужно найти слово, начинающееся на букву \(r\); обычно для этого нужно выполнить следующие действия.

      +

      Пример 1: поиск в словаре. В словаре все слова упорядочены по алфавиту. Предположим, нам нужно найти слово, начинающееся на букву \(r\). Обычно это делают так, как показано на рисунке 1-1.

        -
      1. Откройте словарь примерно на половине страниц и посмотрите, какая буква является первой на этой странице; предположим, это буква \(m\).
      2. +
      3. Откройте словарь примерно на половине страниц и посмотрите, какая буква является первой на этой странице. Предположим, это буква \(m\).
      4. Поскольку в алфавите буква \(r\) идет после \(m\), исключаем первую половину словаря, и область поиска сужается до второй половины.
      5. Продолжайте повторять шаги 1. и 2. , пока не найдете страницу, где первой буквой слов будет \(r\).
      -

      Этапы поиска в словаре. Шаг 1

      +

      Этапы поиска в словаре

      -

      Этапы поиска в словаре. Шаг 2

      +

      binary_search_dictionary_step2

      -

      Этапы поиска в словаре. Шаг 3

      +

      binary_search_dictionary_step3

      -

      Этапы поиска в словаре. Шаг 4

      +

      binary_search_dictionary_step4

      -

      Этапы поиска в словаре. Шаг 5

      +

      binary_search_dictionary_step5

      -

      Рисунок 1-1   Этапы поиска в словаре. Шаг 1

      +

      Рисунок 1-1   Этапы поиска в словаре

      -

      Навык поиска в словаре, которым владеет каждый школьник, на самом деле является известным алгоритмом двоичного поиска. С точки зрения структуры данных словарь можно рассматривать как отсортированный массив; с точки зрения алгоритма последовательность операций по поиску в словаре можно считать двоичным поиском.

      -

      Пример 2: упорядочивание карт. Во время игры в карты необходимо каждый раз упорядочивать карты в руке от меньшего к большему. Для этого нужно выполнить следующие действия.

      +

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

      +

      Пример 2: упорядочивание карт. Во время игры в карты необходимо каждый раз упорядочивать карты в руке от меньшего к большему. Обычно это делают так, как показано на рисунке 1-2.

      1. Разделите карты на упорядоченную и неупорядоченную части, предполагая, что изначально самая левая карта уже упорядочена.
      2. -
      3. Из неупорядоченной части извлеките одну карту и вставьте ее в правильное место в упорядоченной части; после этого две самые левые карты станут упорядоченными.
      4. +
      5. Из неупорядоченной части извлеките одну карту и вставьте ее в правильное место в упорядоченной части. После этого две самые левые карты станут упорядоченными.
      6. Повторяйте шаг 2. , каждый раз перемещая одну карту из неупорядоченной части в упорядоченную, пока все карты не станут упорядоченными.

      Этапы упорядочивания карт

      Рисунок 1-2   Этапы упорядочивания карт

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

      -

      Пример 3: сдача. Предположим, что в супермаркете мы купили товар стоимостью \(69\) руб. и дали кассиру купюру в \(100\) руб. Кассир должен вернуть нам \(31\) руб. Для этого ему нужно выполнить следующие действия.

      +

      Пример 3: сдача. Предположим, что в супермаркете мы купили товар стоимостью \(69\) руб. и дали кассиру купюру в \(100\) руб. Кассир должен вернуть нам \(31\) руб. Обычно он рассуждает так, как показано на рисунке 1-3.

      1. Варианты выбора - это купюры номиналом меньше \(31\) руб. Пусть у нас имеются номиналы \(1\) , \(5\) , \(10\) и \(20\) руб.
      2. Возьмем самую крупную доступную купюру в \(20\) руб. Остаток сдачи составит \(31 - 20 = 11\) руб.
      3. diff --git a/ru/chapter_introduction/summary/index.html b/ru/chapter_introduction/summary/index.html index 4a7ca7736..8013e9e6a 100644 --- a/ru/chapter_introduction/summary/index.html +++ b/ru/chapter_introduction/summary/index.html @@ -4360,7 +4360,7 @@

        1.   Ключевые выводы

        • Алгоритмы повсеместно присутствуют в нашей повседневной жизни и не являются недосягаемыми сложными знаниями. На самом деле мы уже освоили множество алгоритмов, которые помогают решать различные жизненные задачи.
        • -
        • Принцип поиска в словаре соответствует алгоритму двоичного поиска. Двоичный поиск иллюстрирует важную идею алгоритмов "разделяй и властвуй".
        • +
        • Принцип поиска в словаре соответствует алгоритму двоичного поиска. Двоичный поиск иллюстрирует важную идею алгоритмов «разделяй и властвуй».
        • Процесс сортировки карт в колоде очень похож на алгоритм сортировки вставками, который хорошо подходит для сортировки небольших наборов данных.
        • Процесс размена по своей сути является жадным алгоритмом, в котором на каждом этапе принимается наилучшее на данный момент решение.
        • Алгоритм представляет собой набор инструкций или шагов, предназначенных для решения конкретной задачи в ограниченное время, а структура данных - это способ организации и хранения данных в компьютере.
        • @@ -4369,13 +4369,13 @@

        2.   Q & A

        Q: Я программист и в повседневной работе никогда не использовал алгоритмы для решения задач, поскольку часто используемые алгоритмы уже встроены в языки программирования и ими можно пользоваться напрямую. Значит ли это, что рабочие задачи еще не требуют применения алгоритмов?

        -

        Если сравнить конкретные профессиональные навыки с приемами в боевых искусствах, то базовые дисциплины скорее напоминают "внутреннюю силу".

        +

        Если сравнить конкретные профессиональные навыки с приемами в боевых искусствах, то базовые дисциплины скорее напоминают «внутреннюю силу».

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

        • Если бы мы не изучали структуры данных и алгоритмы, то, получив любые данные, возможно, просто передали бы их этой функции сортировки. Все работает гладко, производительность хорошая, и на первый взгляд проблем нет.
        • -
        • Однако если мы изучили алгоритмы, то знаем, что временная сложность встроенной функции сортировки составляет \(O(n \log n)\) ; если же данные представлены целыми числами фиксированной разрядности, например номерами студентов, то можно использовать более эффективный метод поразрядной сортировки, снизив временную сложность до \(O(nk)\) , где \(k\) - это количество разрядов, а при больших объемах данных выиграть во времени, затратах и пользовательском опыте.
        • +
        • Однако если мы изучили алгоритмы, то знаем, что временная сложность встроенной функции сортировки составляет \(O(n \log n)\). Если же данные представлены целыми числами фиксированной разрядности, например номерами студентов, то можно использовать более эффективный метод поразрядной сортировки, снизив временную сложность до \(O(nk)\) , где \(k\) - это количество разрядов, а при больших объемах данных выиграть во времени, затратах и пользовательском опыте.
        -

        В инженерной практике множество задач трудно решить оптимальным образом, и многие из них решаются "как-то". Сложность задачи зависит как от ее природы, так и от уровня знаний и опыта человека, который ее анализирует. Чем более полными знаниями и большим опытом обладает человек, тем глубже он может проанализировать проблему и тем изящнее может быть ее решение.

        +

        В инженерной практике множество задач трудно решить оптимальным образом, и многие из них решаются «как-то». Сложность задачи зависит как от ее природы, так и от уровня знаний и опыта человека, который ее анализирует. Чем более полными знаниями и большим опытом обладает человек, тем глубже он может проанализировать проблему и тем изящнее может быть ее решение.

        diff --git a/ru/chapter_introduction/what_is_dsa/index.html b/ru/chapter_introduction/what_is_dsa/index.html index 4b35ff2b1..ec23940a0 100644 --- a/ru/chapter_introduction/what_is_dsa/index.html +++ b/ru/chapter_introduction/what_is_dsa/index.html @@ -4446,7 +4446,7 @@

        Стоит отметить, что структуры данных и алгоритмы не зависят от языка программирования. Именно поэтому данная книга предлагает их реализации на различных языках.

        Принятое сокращение

        -

        В реальных обсуждениях выражение "структуры данных и алгоритмы" обычно сокращают до просто "алгоритмы". Например, хорошо известные задачи LeetCode на деле одновременно проверяют знания и по структурам данных, и по алгоритмам.

        +

        В реальных обсуждениях выражение «структуры данных и алгоритмы» обычно сокращают до просто «алгоритмы». Например, хорошо известные задачи LeetCode на деле одновременно проверяют знания и по структурам данных, и по алгоритмам.

        diff --git a/ru/chapter_preface/about_the_book/index.html b/ru/chapter_preface/about_the_book/index.html index b1fc4b09e..e7d5343dd 100644 --- a/ru/chapter_preface/about_the_book/index.html +++ b/ru/chapter_preface/about_the_book/index.html @@ -4398,27 +4398,22 @@
        • Анализ сложности: критерии и методы оценки структур данных и алгоритмов. Методы расчета временной и пространственной сложности, распространенные типы, примеры и т. д.
        • Структуры данных: классификация основных типов данных и структур данных. Определение, преимущества и недостатки, основные операции, распространенные типы, типичные приложения и методы реализации массивов, списков, стеков, очередей, хеш-таблиц, деревьев, куч и графов.
        • -
        • Алгоритмы: определение, преимущества и недостатки, эффективность, области применения, этапы решения и примеры задач для поиска, сортировки, алгоритма "разделяй и властвуй", поиска с возвратом, динамического программирования и жадных алгоритмов.
        • +
        • Алгоритмы: определение, преимущества и недостатки, эффективность, области применения, этапы решения и примеры задач для поиска, сортировки, алгоритма «разделяй и властвуй», поиска с возвратом, динамического программирования и жадных алгоритмов.

        Основное содержание книги

        Рисунок 0-1   Основное содержание книги

        0.1.3   Благодарности

        -

        Эта книга постоянно совершенствуется благодаря совместным усилиям множества участников открытого сообщества. Благодарим каждого автора, вложившего свое время и силы; их имена перечислены в порядке, автоматически сгенерированном GitHub: krahets, coderonion, Gonglja, nuomi1, Reanon, justin-tse, hpstory, danielsss, curtishd, night-cruise, S-N-O-R-L-A-X, rongyi, msk397, gvenusleo, khoaxuantu, rivertwilight, K3v123, gyt95, zhuoqinyue, yuelinxin, Zuoxun, mingXta, Phoenix0415, FangYuan33, GN-Yu, longsizhuo, pengchzn, QiLOL, Cathay-Chen, guowei-gong, xBLACKICEx, IsChristina, JoseHung, qualifier1024, hello-ikun, magentaqin, Guanngxu, thomasq0, sunshinesDL, L-Super, Transmigration-zhou, WSL0809, Slone123c, lhxsm, yuan0221, what-is-me, theNefelibatas, Shyam-Chen, sangxiaai, longranger2, codeberg-user, xiongsp, JeffersonHuang, prinpal, seven1240, Wonderdch, malone6, xiaomiusa87, gaofer, bluebean-cloud, a16su, SamJin98, hongyun-robot, nanlei, XiaChuerwu, yd-j, iron-irax, mgisr, steventimes, junminhong, heshuyue, danny900714, Nigh, Dr-XYZ, MolDuM, XC-Zero, reeswell, PXG-XPG, NI-SW, Horbin-Magician, Enlightenus, YangXuanyi, xjr7670, beatrix-chan, DullSword, qq909244296, iStig, boloboloda, hts0000, gledfish, fbigm, echo1937, jiaxianhua, wenjianmin, keshida, kilikilikid, lclc6, lwbaptx, linyejoe2, liuxjerry, szu17dmy, dshlstarr, Yucao-cy, coderlef, czruby, bongbongbakudan, beintentional, ZongYangL, ZhongYuuu, ZhongGuanbin, hezhizhen, linzeyan, ZJKung, JTCPOWI, KawaiiAsh, luluxia, xb534, ztkuaikuai, yw-1021, ElaBosak233, baagod, zhouLion, yishangzhang, yi427, yanedie, yabo083, weibk, wangwang105, th1nk3r-ing, tao363, 4yDX3906, syd168, sslmj2020, smilelsb, siqyka, selear, sdshaoda, Xi-Row, popozhu, nuquist19, noobcodemaker, XiaoK29, chadyi, lyl625760, lucaswangdev, llql1211, 0130w, shanghai-Jerry, EJackYang, Javesun99, eltociear, lipusheng, KNChiu, BlindTerran, ShiMaRing, lovelock, FreddieLi, FloranceYeh, fanchenggang, gltianwen, goerll, nedchu, curly210102, CuB3y0nd, KraHsu, CarrotDLaw, youshaoXG, bubble9um, Asashishi, Asa0oo0o0o, fanenr, eagleanurag, akshiterate, 52coder, foursevenlove, KorsChen, hopkings2008, yang-le, realwujing, Evilrabbit520, Umer-Jahangir, Turing-1024-Lee, Suremotoo, paoxiaomooo, Chieko-Seren, Senrian, Allen-Scai, 19santosh99, ymmmas, Risuntsy, Richard-Zhang1019, RafaelCaso, qingpeng9802, primexiao, Urbaner3, codetypess, nidhoggfgg, MwumLi, CreatorMetaSky, martinx, ZnYang2018, hugtyftg, logan-qiu, psychelzh, Kunchen-Luo, Keynman и KeiichiKasai.

        +

        Эта книга постоянно совершенствуется благодаря совместным усилиям множества участников открытого сообщества. Благодарим каждого автора, вложившего свое время и силы. Их имена перечислены в порядке, автоматически сгенерированном GitHub: krahets, coderonion, Gonglja, nuomi1, Reanon, justin-tse, hpstory, danielsss, curtishd, night-cruise, S-N-O-R-L-A-X, rongyi, msk397, gvenusleo, khoaxuantu, rivertwilight, K3v123, gyt95, zhuoqinyue, yuelinxin, Zuoxun, mingXta, Phoenix0415, FangYuan33, GN-Yu, longsizhuo, pengchzn, QiLOL, Cathay-Chen, guowei-gong, xBLACKICEx, IsChristina, JoseHung, qualifier1024, hello-ikun, magentaqin, Guanngxu, thomasq0, sunshinesDL, L-Super, Transmigration-zhou, WSL0809, Slone123c, lhxsm, yuan0221, what-is-me, theNefelibatas, Shyam-Chen, sangxiaai, longranger2, codeberg-user, xiongsp, JeffersonHuang, prinpal, seven1240, Wonderdch, malone6, xiaomiusa87, gaofer, bluebean-cloud, a16su, SamJin98, hongyun-robot, nanlei, XiaChuerwu, yd-j, iron-irax, mgisr, steventimes, junminhong, heshuyue, danny900714, Nigh, Dr-XYZ, MolDuM, XC-Zero, reeswell, PXG-XPG, NI-SW, Horbin-Magician, Enlightenus, YangXuanyi, xjr7670, beatrix-chan, DullSword, qq909244296, iStig, boloboloda, hts0000, gledfish, fbigm, echo1937, jiaxianhua, wenjianmin, keshida, kilikilikid, lclc6, lwbaptx, linyejoe2, liuxjerry, szu17dmy, dshlstarr, Yucao-cy, coderlef, czruby, bongbongbakudan, beintentional, ZongYangL, ZhongYuuu, ZhongGuanbin, hezhizhen, linzeyan, ZJKung, JTCPOWI, KawaiiAsh, luluxia, xb534, ztkuaikuai, yw-1021, ElaBosak233, baagod, zhouLion, yishangzhang, yi427, yanedie, yabo083, weibk, wangwang105, th1nk3r-ing, tao363, 4yDX3906, syd168, sslmj2020, smilelsb, siqyka, selear, sdshaoda, Xi-Row, popozhu, nuquist19, noobcodemaker, XiaoK29, chadyi, lyl625760, lucaswangdev, llql1211, 0130w, shanghai-Jerry, EJackYang, Javesun99, eltociear, lipusheng, KNChiu, BlindTerran, ShiMaRing, lovelock, FreddieLi, FloranceYeh, fanchenggang, gltianwen, goerll, nedchu, curly210102, CuB3y0nd, KraHsu, CarrotDLaw, youshaoXG, bubble9um, Asashishi, Asa0oo0o0o, fanenr, eagleanurag, akshiterate, 52coder, foursevenlove, KorsChen, hopkings2008, yang-le, realwujing, Evilrabbit520, Umer-Jahangir, Turing-1024-Lee, Suremotoo, paoxiaomooo, Chieko-Seren, Senrian, Allen-Scai, 19santosh99, ymmmas, Risuntsy, Richard-Zhang1019, RafaelCaso, qingpeng9802, primexiao, Urbaner3, codetypess, nidhoggfgg, MwumLi, CreatorMetaSky, martinx, ZnYang2018, hugtyftg, logan-qiu, psychelzh, Kunchen-Luo, Keynman и KeiichiKasai.

        Рецензирование кода книги выполнили coderonion, curtishd, Gonglja, gvenusleo, hpstory, justin-tse, khoaxuantu, krahets, night-cruise, nuomi1, Reanon и rongyi (в алфавитном порядке). Благодарим их за потраченное время и силы, которые обеспечили стандартизацию и единообразие кода на различных языках.

        -

        Английскую версию книги вычитали yuelinxin, K3v123, magentaqin, QiLOL, Phoenix0415, SamJin98, yanedie, RafaelCaso, pengchzn и thomasq0; японскую версию - eltociear; русскую версию - И. А. Шевкун и Yuyan Huang; традиционную китайскую версию - Shyam-Chen и Dr-XYZ. Именно благодаря их вкладу эта книга может служить более широкому кругу читателей, и мы искренне благодарим их.

        +

        Английскую версию книги вычитали yuelinxin, K3v123, magentaqin, QiLOL, Phoenix0415, SamJin98, yanedie, RafaelCaso, pengchzn и thomasq0. Японскую версию - eltociear. Русскую версию - И. А. Шевкун и Yuyan Huang. Традиционную китайскую версию - Shyam-Chen и Dr-XYZ. Именно благодаря их вкладу эта книга может служить более широкому кругу читателей, и мы искренне благодарим их.

        Инструмент генерации ePub-версии этой книги разработал zhongfq. Благодарим его за вклад, который дал читателям более гибкий способ чтения.

        В процессе создания этой книги мне помогало много людей.

          -
        • Благодарю моего наставника в компании, доктора Ли Си: в одной из бесед вы вдохновили меня быстрее начать, что укрепило мою решимость написать эту книгу;
        • -
        • Благодарю мою девушку Bubble, первого читателя этой книги: с позиции новичка в алгоритмах она дала много ценных советов, благодаря которым книга стала более понятной и доступной;
        • -
        • Благодарю Tengbao, Qibao и Feibao за креативное название книги, которое навевает приятные воспоминания о первой строке кода "Hello World!";
        • -
        • Благодарю Xiaoquan за профессиональную помощь в вопросах интеллектуальной собственности, что сыграло важную роль в совершенствовании этой открытой книги;
        • -
        • Благодарю Sutong за дизайн обложки и логотипа книги, а также за терпение при многочисленных исправлениях по моим просьбам;
        • -
        • Благодарю @squidfunk за советы по оформлению и за разработку открытой темы документации Material-for-MkDocs.
        • +
        • Благодарю моего наставника в компании, доктора Ли Си: в одной из бесед вы вдохновили меня быстрее начать, что укрепило мою решимость написать эту книгу. - Благодарю мою девушку Bubble, первого читателя этой книги: с позиции новичка в алгоритмах она дала много ценных советов, благодаря которым книга стала более понятной и доступной. - Благодарю Tengbao, Qibao и Feibao за креативное название книги, которое навевает приятные воспоминания о первой строке кода «Hello World!». - Благодарю Xiaoquan за профессиональную помощь в вопросах интеллектуальной собственности, что сыграло важную роль в совершенствовании этой открытой книги. - Благодарю Sutong за дизайн обложки и логотипа книги, а также за терпение при многочисленных исправлениях по моим просьбам. - Благодарю @squidfunk за советы по оформлению и за разработку открытой темы документации Material-for-MkDocs.

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

        -

        Эта книга пропагандирует метод обучения, сочетающий умственную и практическую деятельность; в этом отношении на меня сильно повлияла Dive into Deep Learning. Я настоятельно рекомендую эту замечательную работу всем читателям.

        +

        Эта книга пропагандирует метод обучения, сочетающий умственную и практическую деятельность. В этом отношении на меня сильно повлияла Dive into Deep Learning. Я настоятельно рекомендую эту замечательную работу всем читателям.

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

        diff --git a/ru/chapter_preface/suggestions/index.html b/ru/chapter_preface/suggestions/index.html index 830b41e22..b4f94de96 100644 --- a/ru/chapter_preface/suggestions/index.html +++ b/ru/chapter_preface/suggestions/index.html @@ -4432,8 +4432,8 @@
      4. Главы, помеченные * в заголовке, являются дополнительными и содержат более сложный материал. Если времени мало, их можно пропустить.
      5. Профессиональные термины выделяются полужирным шрифтом в печатной и PDF-версии или подчеркиванием в веб-версии, например массив (array). Рекомендуется запоминать их для удобства чтения литературы.
      6. Важные моменты и обобщающие фразы будут выделяться полужирным шрифтом, и на такие тексты следует обращать особое внимание.
      7. -
      8. Слова и выражения со специальным смыслом будут отмечаться "кавычками", чтобы избежать неоднозначности.
      9. -
      10. Когда термины различаются между языками программирования, в качестве стандарта используется Python; например, None применяется для обозначения "пустого" значения.
      11. +
      12. Слова и выражения со специальным смыслом будут отмечаться «кавычками», чтобы избежать неоднозначности.
      13. +
      14. Когда термины различаются между языками программирования, в качестве стандарта используется Python. Например, None применяется для обозначения «пустого» значения.
      15. В некоторых местах книга отходит от стандартов комментирования программного кода ради более компактного оформления. Комментарии в основном делятся на три типа: заголовочные, содержательные и многострочные.
      16. @@ -4586,7 +4586,7 @@

        Рисунок 0-2   Пример анимированной иллюстрации

        0.2.3   Углубление понимания через практику кода

        -

        Сопроводительный код этой книги размещен в репозитории GitHub. Как показано ниже, исходный код содержит тестовые примеры и может быть запущен одним нажатием кнопки.

        +

        Сопроводительный код этой книги размещен в репозитории GitHub. Как показано на рисунке 0-3, исходный код содержит тестовые примеры и может быть запущен одним нажатием кнопки.

        Если позволяет время, рекомендуется самостоятельно набирать код. Если времени на обучение мало, по крайней мере просмотрите и выполните весь код.

        Процесс написания кода приносит больше пользы, чем его чтение. Настоящее обучение - это обучение на практике.

        Пример запуска кода

        @@ -4597,7 +4597,7 @@

        Шаг 2: клонирование или загрузка репозитория кода. Перейдите в репозиторий GitHub. Если у вас уже установлен Git, репозиторий можно клонировать следующей командой:

        git clone https://github.com/krahets/hello-algo.git
         
        -

        Также можно нажать кнопку "Download ZIP" в месте, показанном на рисунке 0-4, напрямую скачать архив с кодом и затем распаковать его локально.

        +

        Также можно нажать кнопку «Download ZIP» в месте, показанном на рисунке 0-4, напрямую скачать архив с кодом и затем распаковать его локально.

        Клонирование репозитория и загрузка кода

        Рисунок 0-4   Клонирование репозитория и загрузка кода

        @@ -4605,7 +4605,7 @@

        Блоки кода и соответствующие исходные файлы

        Рисунок 0-5   Блоки кода и соответствующие исходные файлы

        -

        Помимо локального запуска, веб-версия также поддерживает визуальное выполнение Python-кода (на базе pythontutor). Как показано ниже, можно нажать "Визуализировать выполнение" под блоком кода, чтобы раскрыть окно и наблюдать за выполнением алгоритма; также можно нажать "Полноэкранный режим" для более удобного просмотра.

        +

        Помимо локального запуска, веб-версия также поддерживает визуальное выполнение Python-кода (на базе pythontutor). Как показано на рисунке 0-6, можно нажать «Визуализировать выполнение» под блоком кода, чтобы раскрыть окно и наблюдать за выполнением алгоритма. Также можно нажать «Полноэкранный режим» для более удобного просмотра.

        Визуальный запуск Python-кода

        Рисунок 0-6   Визуальный запуск Python-кода

        @@ -4619,10 +4619,10 @@

        В целом процесс изучения структур данных и алгоритмов можно разделить на три этапа.

        1. Этап 1: введение в алгоритмы. Необходимо познакомиться с особенностями и применением различных структур данных, изучить принципы, процессы, назначение и эффективность различных алгоритмов.
        2. -
        3. Этап 2: решение алгоритмических задач. Рекомендуется начинать с популярных задач и решить не менее 100 из них, чтобы познакомиться с основными алгоритмическими проблемами. При первых попытках "забывание знаний" может стать испытанием, но это нормально. Следуйте при повторении задач "кривой забывания Эббингауза", и обычно после 3-5 циклов повторения материал хорошо запоминается. Рекомендуемые списки задач и планы практики см. в этом репозитории GitHub.
        4. -
        5. Этап 3: построение системы знаний. В процессе обучения можно читать статьи по алгоритмам, изучать каркасы решений и учебники, чтобы постоянно обогащать свою систему знаний. В решении задач можно применять продвинутые стратегии, например классификацию по темам, несколько решений одной задачи или одно решение для нескольких задач; соответствующий опыт можно найти в различных сообществах.
        6. +
        7. Этап 2: решение алгоритмических задач. Рекомендуется начинать с популярных задач и решить не менее 100 из них, чтобы познакомиться с основными алгоритмическими проблемами. При первых попытках «забывание знаний» может стать испытанием, но это нормально. Следуйте при повторении задач «кривой забывания Эббингауза», и обычно после 3-5 циклов повторения материал хорошо запоминается. Рекомендуемые списки задач и планы практики см. в этом репозитории GitHub.
        8. +
        9. Этап 3: построение системы знаний. В процессе обучения можно читать статьи по алгоритмам, изучать каркасы решений и учебники, чтобы постоянно обогащать свою систему знаний. В решении задач можно применять продвинутые стратегии, например классификацию по темам, несколько решений одной задачи или одно решение для нескольких задач. Соответствующий опыт можно найти в различных сообществах.
        -

        Как показано на рисунке 0-8, содержание этой книги в основном охватывает "этап 1" и призвано помочь вам более эффективно перейти к обучению на этапах 2 и 3.

        +

        Как показано на рисунке 0-8, содержание этой книги в основном охватывает «этап 1» и призвано помочь вам более эффективно перейти к обучению на этапах 2 и 3.

        Дорожная карта изучения алгоритмов

        Рисунок 0-8   Дорожная карта изучения алгоритмов

        diff --git a/ru/chapter_reference/index.html b/ru/chapter_reference/index.html index 70d59dfbd..3699119e7 100644 --- a/ru/chapter_reference/index.html +++ b/ru/chapter_reference/index.html @@ -4260,7 +4260,7 @@

        [3] Robert Sedgewick и др. Algorithms (4th Edition).

        [4] Yan Weimin. Data Structures (C Language Edition).

        [5] Deng Junhui. Data Structures (C++ Language Edition, 3rd Edition).

        -

        [6] Mark Allen Weiss; пер. Chen Yue. Data Structures and Algorithm Analysis: Java Description (3rd Edition).

        +

        [6] Mark Allen Weiss. Пер. Chen Yue. Data Structures and Algorithm Analysis: Java Description (3rd Edition).

        [7] Cheng Jie. A Plainspoken Guide to Data Structures.

        [8] Wang Zheng. The Beauty of Data Structures and Algorithms.

        [9] Gayle Laakmann McDowell. Cracking the Coding Interview: 189 Programming Questions and Solutions (6th Edition).

        diff --git a/ru/chapter_searching/binary_search/index.html b/ru/chapter_searching/binary_search/index.html index a8f3a8ecd..688103638 100644 --- a/ru/chapter_searching/binary_search/index.html +++ b/ru/chapter_searching/binary_search/index.html @@ -4357,7 +4357,7 @@

        10.1   Двоичный поиск

        -

        Двоичный поиск (binary search) - это эффективный алгоритм поиска, основанный на стратегии "разделяй и властвуй". Он использует упорядоченность данных, сокращая на каждом шаге область поиска вдвое, пока не будет найден целевой элемент или пока интервал поиска не опустеет.

        +

        Двоичный поиск (binary search) - это эффективный алгоритм поиска, основанный на стратегии «разделяй и властвуй». Он использует упорядоченность данных, сокращая на каждом шаге область поиска вдвое, пока не будет найден целевой элемент или пока интервал поиска не опустеет.

        Question

        Дан массив nums длины \(n\), элементы которого расположены в порядке возрастания и не повторяются. Найдите и верните индекс элемента target в этом массиве. Если массив не содержит этого элемента, верните \(-1\) . Пример показан на рисунке 10-1.

        @@ -4996,7 +4996,7 @@

        Как показано на рисунке 10-3, в этих двух вариантах представления интервала различаются инициализация, условие цикла и операция сужения интервала в алгоритме двоичного поиска.

        -

        Поскольку в записи "двойной замкнутый интервал" обе границы являются закрытыми, операции сужения интервала при помощи указателей \(i\) и \(j\) тоже получаются симметричными. Из-за этого в таком варианте сложнее допустить ошибку, поэтому обычно рекомендуется использовать именно запись "двойной замкнутый интервал".

        +

        Поскольку в записи «двойной замкнутый интервал» обе границы являются закрытыми, операции сужения интервала при помощи указателей \(i\) и \(j\) тоже получаются симметричными. Из-за этого в таком варианте сложнее допустить ошибку, поэтому обычно рекомендуется использовать именно запись «двойной замкнутый интервал».

        Два определения интервалов

        Рисунок 10-3   Два определения интервалов

        @@ -5010,7 +5010,7 @@
        • Двоичный поиск применим только к упорядоченным данным. Если входные данные неупорядочены, специально сортировать их ради двоичного поиска невыгодно. Это связано с тем, что временная сложность алгоритмов сортировки обычно составляет \(O(n \log n)\) , что выше, чем у линейного и двоичного поиска. Если элементы приходится часто вставлять, то для сохранения порядка в массиве их нужно помещать в конкретные позиции, а это требует \(O(n)\) времени и тоже обходится дорого.
        • Двоичный поиск применим только к массивам. Для него нужен скачкообразный доступ к элементам, а в связном списке такой доступ малоэффективен, поэтому двоичный поиск не подходит для списков и структур данных, построенных на их основе.
        • -
        • При малом объеме данных линейный поиск работает лучше. В линейном поиске на каждом шаге нужна всего одна операция сравнения; в двоичном поиске требуется 1 сложение, 1 деление, от 1 до 3 сравнений и еще 1 сложение или вычитание, то есть всего от 4 до 6 элементарных операций. Поэтому при небольшом \(n\) линейный поиск может оказаться быстрее двоичного.
        • +
        • При малом объеме данных линейный поиск работает лучше. В линейном поиске на каждом шаге нужна всего одна операция сравнения. В двоичном поиске требуется 1 сложение, 1 деление, от 1 до 3 сравнений и еще 1 сложение или вычитание, то есть всего от 4 до 6 элементарных операций. Поэтому при небольшом \(n\) линейный поиск может оказаться быстрее двоичного.
        diff --git a/ru/chapter_searching/binary_search_insertion/index.html b/ru/chapter_searching/binary_search_insertion/index.html index b7997bc96..c66699acf 100644 --- a/ru/chapter_searching/binary_search_insertion/index.html +++ b/ru/chapter_searching/binary_search_insertion/index.html @@ -4964,9 +4964,9 @@

        Tip

        -

        Код в этом разделе записан в стиле "двойного замкнутого интервала". При желании можно самостоятельно реализовать вариант "слева закрыт, справа открыт".

        +

        Код в этом разделе записан в стиле «двойного замкнутого интервала». При желании можно самостоятельно реализовать вариант «слева закрыт, справа открыт».

        -

        Если смотреть в целом, суть двоичного поиска сводится к тому, что для указателей \(i\) и \(j\) заранее задаются ориентиры поиска; целью может быть конкретный элемент, например target , а может быть и диапазон элементов, например все элементы, меньшие target .

        +

        Если смотреть в целом, суть двоичного поиска сводится к тому, что для указателей \(i\) и \(j\) заранее задаются ориентиры поиска. Целью может быть конкретный элемент, например target , а может быть и диапазон элементов, например все элементы, меньшие target .

        В ходе непрерывного двоичного деления указатели \(i\) и \(j\) постепенно приближаются к заранее заданной цели. В конце они либо успешно находят ответ, либо останавливаются после выхода за границы.

        diff --git a/ru/chapter_searching/replace_linear_by_hashing/index.html b/ru/chapter_searching/replace_linear_by_hashing/index.html index e7caaa13f..da49fa6f7 100644 --- a/ru/chapter_searching/replace_linear_by_hashing/index.html +++ b/ru/chapter_searching/replace_linear_by_hashing/index.html @@ -4363,7 +4363,7 @@

        Дан массив целых чисел nums и целевой элемент target . Найдите в массиве два элемента, сумма которых равна target , и верните их индексы. Подойдет любой корректный ответ.

        10.4.1   Линейный поиск: обмен времени на пространство

        -

        Рассмотрим прямой перебор всех возможных комбинаций. Как показано на рисунке 10-9, мы запускаем два вложенных цикла и на каждом шаге проверяем, равна ли сумма двух целых чисел target ; если да, то возвращаем их индексы.

        +

        Рассмотрим прямой перебор всех возможных комбинаций. Как показано на рисунке 10-9, мы запускаем два вложенных цикла и на каждом шаге проверяем, равна ли сумма двух целых чисел target. Если да, то возвращаем их индексы.

        Линейный поиск для задачи о двух суммах

        Рисунок 10-9   Линейный поиск для задачи о двух суммах

        @@ -4576,7 +4576,7 @@

        10.4.2   Хеш-поиск: обмен пространства на время

        Рассмотрим вариант с использованием хеш-таблицы, где ключами и значениями будут элементы массива и их индексы. При циклическом обходе массива на каждом шаге выполняются действия, показанные на рисунке 10-10.

          -
        1. Проверить, находится ли число target - nums[i] в хеш-таблице; если да, то сразу вернуть индексы этих двух элементов.
        2. +
        3. Проверить, находится ли число target - nums[i] в хеш-таблице. Если да, то сразу вернуть индексы этих двух элементов.
        4. Добавить в хеш-таблицу пару из ключа nums[i] и индекса i .
        diff --git a/ru/chapter_searching/searching_algorithm_revisited/index.html b/ru/chapter_searching/searching_algorithm_revisited/index.html index 4fbbcd73d..f79c80206 100644 --- a/ru/chapter_searching/searching_algorithm_revisited/index.html +++ b/ru/chapter_searching/searching_algorithm_revisited/index.html @@ -4389,17 +4389,17 @@

        10.5.1   Полный перебор

        Полный перебор заключается в том, что мы обходим каждый элемент структуры данных, чтобы найти целевой элемент.

          -
        • "Линейный поиск" применяется к линейным структурам данных, таким как массивы и списки. Он начинается с одного конца структуры данных и последовательно проверяет элементы, пока не найдет целевой элемент или пока не достигнет другого конца структуры данных.
        • -
        • "Обход в ширину" и "обход в глубину" - это две стратегии обхода графов и деревьев. Обход в ширину стартует из начального узла и исследует все узлы текущего уровня, прежде чем переходить к следующему. Обход в глубину стартует из начального узла, проходит один путь до конца, затем возвращается назад и пробует другие пути, пока не будет полностью пройдена вся структура данных.
        • +
        • «Линейный поиск» применяется к линейным структурам данных, таким как массивы и списки. Он начинается с одного конца структуры данных и последовательно проверяет элементы, пока не найдет целевой элемент или пока не достигнет другого конца структуры данных.
        • +
        • «Обход в ширину» и «обход в глубину» - это две стратегии обхода графов и деревьев. Обход в ширину стартует из начального узла и исследует все узлы текущего уровня, прежде чем переходить к следующему. Обход в глубину стартует из начального узла, проходит один путь до конца, затем возвращается назад и пробует другие пути, пока не будет полностью пройдена вся структура данных.

        Преимущество полного перебора состоит в его простоте и универсальности, поскольку он не требует предварительной обработки данных и использования дополнительных структур данных.

        Однако временная сложность таких алгоритмов равна \(O(n)\) , где \(n\) - число элементов, поэтому при больших объемах данных их производительность невысока.

        10.5.2   Адаптивный поиск

        Адаптивный поиск использует специфические свойства данных (например, упорядоченность), чтобы оптимизировать процесс поиска и тем самым эффективнее находить целевой элемент.

          -
        • "Двоичный поиск" использует упорядоченность данных для эффективного поиска и применим только к массивам.
        • -
        • "Хеш-поиск" использует хеш-таблицу для построения отображения между поисковыми данными и целевыми данными, благодаря чему запросы выполняются эффективно.
        • -
        • "Поиск в дереве" ведется в конкретной древовидной структуре (например, в двоичном дереве поиска) и позволяет быстро отсекать узлы на основе сравнения значений, чтобы найти цель.
        • +
        • «Двоичный поиск» использует упорядоченность данных для эффективного поиска и применим только к массивам.
        • +
        • «Хеш-поиск» использует хеш-таблицу для построения отображения между поисковыми данными и целевыми данными, благодаря чему запросы выполняются эффективно.
        • +
        • «Поиск в дереве» ведется в конкретной древовидной структуре (например, в двоичном дереве поиска) и позволяет быстро отсекать узлы на основе сравнения значений, чтобы найти цель.

        Преимущество этих алгоритмов заключается в высокой эффективности: их временная сложность может достигать \(O(\log n)\) и даже \(O(1)\) .

        Однако для использования таких алгоритмов обычно требуется предварительная обработка данных. Например, для двоичного поиска нужно заранее отсортировать массив, а хеш-поиск и поиск в дереве требуют дополнительных структур данных, поддержание которых тоже отнимает время и память.

        @@ -4481,13 +4481,13 @@

        Двоичный поиск

          -
        • Подходит для больших наборов данных и демонстрирует стабильную эффективность; его худшая временная сложность равна \(O(\log n)\) .
        • +
        • Подходит для больших наборов данных и демонстрирует стабильную эффективность. Его худшая временная сложность равна \(O(\log n)\) .
        • Объем данных не должен быть слишком большим, потому что массив требует непрерывного участка памяти.
        • Не подходит для сценариев с частыми вставками и удалениями данных, так как поддержание массива в отсортированном виде требует больших затрат.

        Хеш-поиск

          -
        • Подходит для сценариев, в которых требования к скорости запросов очень высоки; средняя временная сложность равна \(O(1)\) .
        • +
        • Подходит для сценариев, в которых требования к скорости запросов очень высоки. Средняя временная сложность равна \(O(1)\) .
        • Не подходит для сценариев, где требуется упорядоченность данных или поиск по диапазону, потому что хеш-таблица не умеет поддерживать порядок данных.
        • Сильно зависит от хеш-функции и стратегии обработки коллизий, поэтому риск деградации производительности сравнительно велик.
        • Не подходит для слишком больших объемов данных, так как хеш-таблице требуется дополнительное пространство, чтобы максимально снизить число коллизий и обеспечить хорошую производительность поиска.
        • diff --git a/ru/chapter_searching/summary/index.html b/ru/chapter_searching/summary/index.html index f43872d76..0cc944ca3 100644 --- a/ru/chapter_searching/summary/index.html +++ b/ru/chapter_searching/summary/index.html @@ -4341,7 +4341,7 @@
        • Полный перебор находит данные путем обхода структуры данных. Линейный поиск подходит для массивов и списков, а обход в ширину и обход в глубину подходят для графов и деревьев. Эти алгоритмы универсальны и не требуют предварительной обработки данных, но их временная сложность \(O(n)\) сравнительно велика.
        • Хеш-поиск, поиск в дереве и двоичный поиск относятся к эффективным методам поиска и позволяют быстро находить целевой элемент в конкретных структурах данных. Такие алгоритмы обладают высокой эффективностью, их временная сложность может достигать \(O(\log n)\) и даже \(O(1)\) , но обычно им нужны дополнительные структуры данных.
        • На практике нужно анализировать размер данных, требования к производительности поиска, а также частоту запросов и обновлений данных, чтобы выбрать подходящий метод поиска.
        • -
        • Линейный поиск подходит для небольших или часто обновляемых наборов данных; двоичный поиск - для больших отсортированных данных; хеш-поиск - для сценариев с высокими требованиями к скорости запросов и без необходимости поиска по диапазону; поиск в дереве - для больших динамических данных, где нужно поддерживать порядок и выполнять диапазонные запросы.
        • +
        • Линейный поиск подходит для небольших или часто обновляемых наборов данных. Двоичный поиск - для больших отсортированных данных. Хеш-поиск - для сценариев с высокими требованиями к скорости запросов и без необходимости поиска по диапазону. Поиск в дереве - для больших динамических данных, где нужно поддерживать порядок и выполнять диапазонные запросы.
        • Замена линейного поиска на хеш-поиск - это распространенная стратегия ускорения, которая позволяет снизить временную сложность с \(O(n)\) до \(O(1)\) .
        diff --git a/ru/chapter_sorting/bubble_sort/index.html b/ru/chapter_sorting/bubble_sort/index.html index ef7da6218..7f0b35e10 100644 --- a/ru/chapter_sorting/bubble_sort/index.html +++ b/ru/chapter_sorting/bubble_sort/index.html @@ -4380,7 +4380,7 @@

        11.3   Сортировка пузырьком

        Сортировка пузырьком (bubble sort) реализует сортировку путем последовательного сравнения и обмена соседних элементов. Этот процесс напоминает всплытие пузырьков снизу вверх, откуда и произошло название алгоритма.

        -

        Как показано на рисунке 11-4, процесс "всплытия" можно смоделировать через операцию обмена элементов: начиная от левого края массива и двигаясь вправо, мы последовательно сравниваем соседние элементы и, если "левый элемент > правый элемент", меняем их местами. После завершения прохода максимальный элемент будет перемещен в самый правый конец массива.

        +

        Как показано на рисунке 11-4, процесс «всплытия» можно смоделировать через операцию обмена элементов: начиная от левого края массива и двигаясь вправо, мы последовательно сравниваем соседние элементы и, если «левый элемент > правый элемент», меняем их местами. После завершения прохода максимальный элемент будет перемещен в самый правый конец массива.

        @@ -4409,11 +4409,11 @@

        Рисунок 11-4   Моделирование пузырька через обмен элементов

        11.3.1   Алгоритм

        -

        Пусть длина массива равна \(n\) ; тогда шаги сортировки пузырьком показаны на рисунке 11-5.

        +

        Пусть длина массива равна \(n\). Тогда шаги сортировки пузырьком показаны на рисунке 11-5.

          -
        1. Сначала выполнить один проход "всплытия" по \(n\) элементам, переместив максимальный элемент массива на правильную позицию.
        2. -
        3. Затем выполнить "всплытие" по оставшимся \(n - 1\) элементам, переместив второй по величине элемент на правильную позицию.
        4. -
        5. Продолжать по аналогии; после \(n - 1\) раундов "всплытия" первые \(n - 1\) по величине элементы окажутся на правильных позициях.
        6. +
        7. Сначала выполнить один проход «всплытия» по \(n\) элементам, переместив максимальный элемент массива на правильную позицию.
        8. +
        9. Затем выполнить «всплытие» по оставшимся \(n - 1\) элементам, переместив второй по величине элемент на правильную позицию.
        10. +
        11. Продолжать по аналогии. После \(n - 1\) раундов «всплытия» первые \(n - 1\) по величине элементы окажутся на правильных позициях.
        12. Оставшийся единственный элемент обязательно является минимальным, сортировать его уже не нужно, поэтому сортировка завершена.

        Процесс сортировки пузырьком

        @@ -4648,8 +4648,8 @@

        11.3.2   Оптимизация эффективности

        -

        Если в каком-либо раунде "всплытия" не произошло ни одного обмена, значит, массив уже отсортирован и можно сразу вернуть результат. Поэтому можно добавить флаг flag для отслеживания этой ситуации и немедленного выхода.

        -

        После такой оптимизации худшая и средняя временные сложности сортировки пузырьком по-прежнему равны \(O(n^2)\) ; однако если входной массив уже полностью упорядочен, достигается лучшая временная сложность \(O(n)\) .

        +

        Если в каком-либо раунде «всплытия» не произошло ни одного обмена, значит, массив уже отсортирован и можно сразу вернуть результат. Поэтому можно добавить флаг flag для отслеживания этой ситуации и немедленного выхода.

        +

        После такой оптимизации худшая и средняя временные сложности сортировки пузырьком по-прежнему равны \(O(n^2)\). Однако если входной массив уже полностью упорядочен, достигается лучшая временная сложность \(O(n)\) .

        @@ -4944,9 +4944,9 @@

        11.3.3   Характеристики алгоритма

          -
        • Временная сложность равна \(O(n^2)\), алгоритм адаптивен: длины диапазонов, проходящих "всплытие" в разных раундах, последовательно равны \(n - 1\), \(n - 2\), \(\dots\), \(2\), \(1\) , а их сумма равна \((n - 1) n / 2\) . После добавления оптимизации с flag лучшая временная сложность может достигать \(O(n)\) .
        • +
        • Временная сложность равна \(O(n^2)\), алгоритм адаптивен: длины диапазонов, проходящих «всплытие» в разных раундах, последовательно равны \(n - 1\), \(n - 2\), \(\dots\), \(2\), \(1\) , а их сумма равна \((n - 1) n / 2\) . После добавления оптимизации с flag лучшая временная сложность может достигать \(O(n)\) .
        • Пространственная сложность равна \(O(1)\), сортировка выполняется на месте: указатели \(i\) и \(j\) используют константный объем дополнительной памяти.
        • -
        • Стабильная сортировка: поскольку при "всплытии" равные элементы не обмениваются местами.
        • +
        • Стабильная сортировка: поскольку при «всплытии» равные элементы не обмениваются местами.
        diff --git a/ru/chapter_sorting/bucket_sort/index.html b/ru/chapter_sorting/bucket_sort/index.html index cc1b92d83..1d3a36452 100644 --- a/ru/chapter_sorting/bucket_sort/index.html +++ b/ru/chapter_sorting/bucket_sort/index.html @@ -4379,8 +4379,8 @@

        11.8   Блочная сортировка

        -

        Рассмотренные выше алгоритмы сортировки относятся к "сортировкам на основе сравнений": они упорядочивают данные, сравнивая элементы друг с другом. Временная сложность таких алгоритмов не может быть лучше \(O(n \log n)\) . Далее мы рассмотрим несколько "сортировок без сравнений", чья временная сложность может достигать линейного порядка.

        -

        Блочная сортировка (bucket sort) является типичным применением стратегии "разделяй и властвуй". Она создает набор упорядоченных по величине блоков, где каждый блок соответствует определенному диапазону данных; затем элементы равномерно распределяются по этим блокам, внутри каждого блока отдельно выполняется сортировка, а в конце результаты объединяются в порядке блоков.

        +

        Рассмотренные выше алгоритмы сортировки относятся к «сортировкам на основе сравнений»: они упорядочивают данные, сравнивая элементы друг с другом. Временная сложность таких алгоритмов не может быть лучше \(O(n \log n)\) . Далее мы рассмотрим несколько «сортировок без сравнений», чья временная сложность может достигать линейного порядка.

        +

        Блочная сортировка (bucket sort) является типичным применением стратегии «разделяй и властвуй». Она создает набор упорядоченных по величине блоков, где каждый блок соответствует определенному диапазону данных. Затем элементы равномерно распределяются по этим блокам, внутри каждого блока отдельно выполняется сортировка, а в конце результаты объединяются в порядке блоков.

        11.8.1   Алгоритм

        Рассмотрим массив длины \(n\), элементы которого являются числами с плавающей запятой из диапазона \([0, 1)\) . Процесс блочной сортировки показан на рисунке 11-13.

          @@ -4798,9 +4798,9 @@
        1. Является ли блочная сортировка стабильной, зависит от того, стабилен ли алгоритм сортировки внутри каждого блока.
        2. 11.8.3   Как добиться равномерного распределения

          -

          Теоретически временная сложность блочной сортировки может достигать \(O(n)\) ; ключ к этому - как можно более равномерно распределить элементы по блокам. На практике данные часто распределены неравномерно. Например, если нужно распределить все товары на маркетплейсе по 10 ценовым блокам, количество товаров дешевле 100 рублей может быть очень большим, а товаров дороже 1000 рублей - очень маленьким. Если просто разбить диапазон цен на 10 равных частей, число товаров в каждом блоке будет сильно различаться.

          +

          Теоретически временная сложность блочной сортировки может достигать \(O(n)\). Ключ к этому - как можно более равномерно распределить элементы по блокам. На практике данные часто распределены неравномерно. Например, если нужно распределить все товары на маркетплейсе по 10 ценовым блокам, количество товаров дешевле 100 рублей может быть очень большим, а товаров дороже 1000 рублей - очень маленьким. Если просто разбить диапазон цен на 10 равных частей, число товаров в каждом блоке будет сильно различаться.

          Чтобы добиться более равномерного распределения, можно сначала задать грубую линию раздела и приблизительно распределить данные по 3 блокам. После этого блоки с большим числом товаров можно снова делить на 3 блока и продолжать процесс до тех пор, пока число элементов в каждом блоке не станет примерно одинаковым.

          -

          Как показано на рисунке 11-14, по сути этот метод строит рекурсивное дерево, цель которого - сделать значения в листьях как можно более равномерными. Конечно, совсем не обязательно каждый раз делить данные именно на 3 блока; конкретную схему разбиения можно выбирать в зависимости от свойств данных.

          +

          Как показано на рисунке 11-14, по сути этот метод строит рекурсивное дерево, цель которого - сделать значения в листьях как можно более равномерными. Конечно, совсем не обязательно каждый раз делить данные именно на 3 блока. Конкретную схему разбиения можно выбирать в зависимости от свойств данных.

          Рекурсивное разбиение по блокам

          Рисунок 11-14   Рекурсивное разбиение по блокам

          diff --git a/ru/chapter_sorting/counting_sort/index.html b/ru/chapter_sorting/counting_sort/index.html index 52a9e0312..4dd4b1d78 100644 --- a/ru/chapter_sorting/counting_sort/index.html +++ b/ru/chapter_sorting/counting_sort/index.html @@ -4403,10 +4403,10 @@

          11.9   Сортировка подсчетом

          Сортировка подсчетом (counting sort) реализует сортировку за счет подсчета количества вхождений элементов и обычно используется для массивов целых чисел.

          11.9.1   Простая реализация

          -

          Сначала рассмотрим простой пример. Дан массив nums длины \(n\) , элементы которого являются "неотрицательными целыми числами". Общий процесс сортировки подсчетом показан на рисунке 11-16.

          +

          Сначала рассмотрим простой пример. Дан массив nums длины \(n\) , элементы которого являются «неотрицательными целыми числами». Общий процесс сортировки подсчетом показан на рисунке 11-16.

          1. Пройти по массиву, найти в нем максимальное число, обозначить его как \(m\) , а затем создать вспомогательный массив counter длины \(m + 1\) .
          2. -
          3. С помощью counter подсчитать, сколько раз каждое число встречается в nums; при этом counter[num] хранит число вхождений значения num . Делается это просто: достаточно пройти по nums (пусть текущее число равно num ) и на каждом шаге увеличить counter[num] на \(1\) .
          4. +
          5. С помощью counter подсчитать, сколько раз каждое число встречается в nums. При этом counter[num] хранит число вхождений значения num . Делается это просто: достаточно пройти по nums (пусть текущее число равно num ) и на каждом шаге увеличить counter[num] на \(1\) .
          6. Поскольку индексы массива counter изначально упорядочены, можно считать, что все числа уже отсортированы. Далее остается пройти по counter и в соответствии с числом вхождений записать значения обратно в nums в порядке возрастания.

          Процесс сортировки подсчетом

          @@ -4743,7 +4743,7 @@

        11.9.2   Полная реализация

        Внимательный читатель мог заметить, что если входные данные представлены объектами, то описанный выше шаг 3. перестает работать. Например, если входными данными являются объекты товаров и мы хотим отсортировать их по цене (полю класса), то описанный алгоритм сможет выдать только отсортированный ряд цен, но не исходные объекты в нужном порядке.

        -

        Как же получить корректный порядок исходных данных? Сначала вычислим "префиксную сумму" массива counter . Как следует из названия, префиксная сумма в индексе i , обозначаемая как prefix[i] , равна сумме первых i элементов массива:

        +

        Как же получить корректный порядок исходных данных? Сначала вычислим «префиксную сумму» массива counter . Как следует из названия, префиксная сумма в индексе i , обозначаемая как prefix[i] , равна сумме первых i элементов массива:

        \[ \text{prefix}[i] = \sum_{j=0}^i \text{counter[j]} \]
        @@ -4752,7 +4752,7 @@
      17. Записать num в массив res по индексу prefix[num] - 1 .
      18. Уменьшить префиксную сумму prefix[num] на \(1\) , чтобы получить индекс следующего размещения элемента num .
      -

      После завершения прохода массив res будет содержать отсортированный результат; остается только переписать res обратно в nums . Полный процесс сортировки подсчетом показан на рисунке 11-17.

      +

      После завершения прохода массив res будет содержать отсортированный результат. Остается только переписать res обратно в nums . Полный процесс сортировки подсчетом показан на рисунке 11-17.

      @@ -5233,7 +5233,7 @@
      • Временная сложность равна \(O(n + m)\), алгоритм не является адаптивным : необходимо пройти по nums и по counter , а оба этих прохода занимают линейное время. Обычно выполняется \(n \gg m\) , поэтому временная сложность стремится к \(O(n)\) .
      • Пространственная сложность равна \(O(n + m)\), сортировка не выполняется на месте: используются массивы res и counter длины \(n\) и \(m\) соответственно.
      • -
      • Стабильная сортировка: порядок заполнения res идет "справа налево", поэтому обратный проход по nums позволяет сохранить относительный порядок равных элементов и тем самым реализовать стабильную сортировку. Вообще говоря, прямой проход по nums тоже даст правильный результат сортировки, но он будет нестабильным.
      • +
      • Стабильная сортировка: порядок заполнения res идет «справа налево», поэтому обратный проход по nums позволяет сохранить относительный порядок равных элементов и тем самым реализовать стабильную сортировку. Вообще говоря, прямой проход по nums тоже даст правильный результат сортировки, но он будет нестабильным.

      11.9.4   Ограничения

      На этом этапе сортировка подсчетом может показаться очень изящной: она позволяет эффективно сортировать данные, опираясь только на подсчет числа вхождений. Однако условия ее применения довольно строгие.

      diff --git a/ru/chapter_sorting/heap_sort/index.html b/ru/chapter_sorting/heap_sort/index.html index afcc7d23d..19d8a1525 100644 --- a/ru/chapter_sorting/heap_sort/index.html +++ b/ru/chapter_sorting/heap_sort/index.html @@ -4359,16 +4359,16 @@

      11.7   Пирамидальная сортировка

      Tip

      -

      Перед чтением этого раздела убедитесь, что вы уже изучили главу "Куча".

      +

      Перед чтением этого раздела убедитесь, что вы уже изучили главу «Куча».

      -

      Пирамидальная сортировка (heap sort) - это эффективный алгоритм сортировки, основанный на структуре данных "куча". Для его реализации можно использовать уже изученные нами "построение кучи" и "извлечение элементов из кучи".

      +

      Пирамидальная сортировка (heap sort) - это эффективный алгоритм сортировки, основанный на структуре данных «куча». Для его реализации можно использовать уже изученные нами «построение кучи» и «извлечение элементов из кучи».

        -
      1. Подать на вход массив и построить из него мин-кучу; в этот момент минимальный элемент будет находиться в вершине кучи.
      2. +
      3. Подать на вход массив и построить из него мин-кучу. В этот момент минимальный элемент будет находиться в вершине кучи.
      4. Непрерывно выполнять извлечение из кучи и по порядку записывать извлеченные элементы - так получится последовательность, отсортированная по возрастанию.

      Хотя этот метод и работоспособен, он требует дополнительного массива для хранения извлеченных элементов и потому расходует лишнюю память. На практике обычно используют более изящную реализацию.

      11.7.1   Алгоритм

      -

      Пусть длина массива равна \(n\) ; тогда процесс пирамидальной сортировки показан на рисунке 11-12.

      +

      Пусть длина массива равна \(n\). Тогда процесс пирамидальной сортировки показан на рисунке 11-12.

      1. Подать на вход массив и построить из него макс-кучу. После этого максимальный элемент окажется в вершине кучи.
      2. Обменять элемент в вершине кучи (первый элемент) с элементом внизу кучи (последний элемент). После обмена длина кучи уменьшается на \(1\) , а число уже отсортированных элементов увеличивается на \(1\) .
      3. @@ -4421,7 +4421,7 @@

      Рисунок 11-12   Шаги пирамидальной сортировки

      -

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

      +

      В коде используется та же функция просеивания сверху вниз sift_down(), что и в главе «Куча». Важно помнить, что длина кучи уменьшается по мере извлечения максимального элемента, поэтому функции sift_down() нужно передавать параметр длины \(n\) , чтобы указать текущую действительную длину кучи. Код приведен ниже:

      diff --git a/ru/chapter_sorting/insertion_sort/index.html b/ru/chapter_sorting/insertion_sort/index.html index 6657514ab..7e13b1837 100644 --- a/ru/chapter_sorting/insertion_sort/index.html +++ b/ru/chapter_sorting/insertion_sort/index.html @@ -4381,7 +4381,7 @@

      11.4   Сортировка вставками

      Сортировка вставками (insertion sort) - это простой алгоритм сортировки, принцип которого очень похож на ручную сортировку карт в колоде.

      Точнее говоря, в неотсортированном диапазоне выбирается опорный элемент, после чего он сравнивается с элементами слева в уже отсортированном диапазоне и вставляется в правильную позицию.

      -

      На рисунке 11-6 показан процесс вставки элемента в массив. Пусть опорный элемент обозначен как base ; нам нужно сдвинуть все элементы от целевого индекса до base на одну позицию вправо, а затем записать base в целевой индекс.

      +

      На рисунке 11-6 показан процесс вставки элемента в массив. Пусть опорный элемент обозначен как base. Нам нужно сдвинуть все элементы от целевого индекса до base на одну позицию вправо, а затем записать base в целевой индекс.

      Одна операция вставки

      Рисунок 11-6   Одна операция вставки

      @@ -4389,9 +4389,9 @@

      Общий процесс сортировки вставками показан на рисунке 11-7.

      1. В начальном состоянии отсортирован только первый элемент массива.
      2. -
      3. Выбрать второй элемент массива как base ; после вставки в правильную позицию первые два элемента массива окажутся отсортированными.
      4. -
      5. Выбрать третий элемент как base ; после вставки в правильную позицию первые три элемента массива окажутся отсортированными.
      6. -
      7. Продолжать по аналогии; в последнем раунде в качестве base берется последний элемент, и после его вставки все элементы массива будут отсортированы.
      8. +
      9. Выбрать второй элемент массива как base. После вставки в правильную позицию первые два элемента массива окажутся отсортированными.
      10. +
      11. Выбрать третий элемент как base. После вставки в правильную позицию первые три элемента массива окажутся отсортированными.
      12. +
      13. Продолжать по аналогии. В последнем раунде в качестве base берется последний элемент, и после его вставки все элементы массива будут отсортированы.

      Процесс сортировки вставками

      Рисунок 11-7   Процесс сортировки вставками

      @@ -4629,11 +4629,11 @@

      11.4.3   Преимущества сортировки вставками

      Временная сложность сортировки вставками равна \(O(n^2)\) , а у быстрой сортировки, которую мы скоро изучим, временная сложность равна \(O(n \log n)\) . Несмотря на более высокую асимптотическую сложность, на малых объемах данных сортировка вставками обычно работает быстрее.

      -

      Этот вывод похож на сравнение линейного и двоичного поиска. Алгоритмы уровня \(O(n \log n)\) , такие как быстрая сортировка, относятся к алгоритмам на основе стратегии "разделяй и властвуй" и обычно включают больше элементарных вычислений. Когда объем данных мал, значения \(n^2\) и \(n \log n\) близки друг к другу, поэтому асимптотика не доминирует, а решающим становится число элементарных операций в каждом раунде.

      -

      На практике встроенные функции сортировки во многих языках программирования (например, в Java) используют сортировку вставками. Общая идея такова: для длинных массивов применять алгоритмы сортировки на основе стратегии "разделяй и властвуй", например быструю сортировку; для коротких массивов сразу использовать сортировку вставками.

      +

      Этот вывод похож на сравнение линейного и двоичного поиска. Алгоритмы уровня \(O(n \log n)\) , такие как быстрая сортировка, относятся к алгоритмам на основе стратегии «разделяй и властвуй» и обычно включают больше элементарных вычислений. Когда объем данных мал, значения \(n^2\) и \(n \log n\) близки друг к другу, поэтому асимптотика не доминирует, а решающим становится число элементарных операций в каждом раунде.

      +

      На практике встроенные функции сортировки во многих языках программирования (например, в Java) используют сортировку вставками. Общая идея такова: для длинных массивов применять алгоритмы сортировки на основе стратегии «разделяй и властвуй», например быструю сортировку. Для коротких массивов сразу использовать сортировку вставками.

      Хотя сортировка пузырьком, выбором и вставками имеют одинаковую временную сложность \(O(n^2)\) , в реальных задачах сортировка вставками используется заметно чаще, чем сортировка пузырьком и сортировка выбором. Основные причины таковы.

        -
      • Сортировка пузырьком основана на обмене элементов, для чего нужна временная переменная и суммарно выполняются 3 элементарные операции; сортировка вставками основана на присваивании элементов и требует всего 1 элементарной операции. Поэтому вычислительные затраты сортировки пузырьком обычно выше, чем у сортировки вставками.
      • +
      • Сортировка пузырьком основана на обмене элементов, для чего нужна временная переменная и суммарно выполняются 3 элементарные операции. Сортировка вставками основана на присваивании элементов и требует всего 1 элементарной операции. Поэтому вычислительные затраты сортировки пузырьком обычно выше, чем у сортировки вставками.
      • Временная сложность сортировки выбором в любом случае равна \(O(n^2)\) . Если входные данные уже частично упорядочены, сортировка вставками обычно эффективнее сортировки выбором.
      • Сортировка выбором нестабильна, поэтому ее нельзя использовать для многоуровневой сортировки.
      diff --git a/ru/chapter_sorting/merge_sort/index.html b/ru/chapter_sorting/merge_sort/index.html index ad34ac380..3aeda4843 100644 --- a/ru/chapter_sorting/merge_sort/index.html +++ b/ru/chapter_sorting/merge_sort/index.html @@ -4379,21 +4379,21 @@

      11.6   Сортировка слиянием

      -

      Сортировка слиянием (merge sort) - это алгоритм сортировки, основанный на стратегии "разделяй и властвуй", который включает этапы "разделения" и "слияния", показанные на рисунке 11-10.

      +

      Сортировка слиянием (merge sort) - это алгоритм сортировки, основанный на стратегии «разделяй и властвуй», который включает этапы «разделения» и «слияния», показанные на рисунке 11-10.

      1. Этап разделения: массив рекурсивно делится пополам, и задача сортировки длинного массива превращается в задачи сортировки более коротких массивов.
      2. -
      3. Этап слияния: когда длина подмассива становится равной 1, разделение завершается и начинается слияние; два коротких упорядоченных массива непрерывно объединяются в один более длинный упорядоченный массив, пока процесс не завершится.
      4. +
      5. Этап слияния: когда длина подмассива становится равной 1, разделение завершается и начинается слияние. Два коротких упорядоченных массива непрерывно объединяются в один более длинный упорядоченный массив, пока процесс не завершится.

      Этапы разделения и слияния в сортировке слиянием

      Рисунок 11-10   Этапы разделения и слияния в сортировке слиянием

      11.6.1   Алгоритм

      -

      Как показано на рисунке 11-11, на этапе "разделения" массив рекурсивно разбивается сверху вниз по середине на два подмассива.

      +

      Как показано на рисунке 11-11, на этапе «разделения» массив рекурсивно разбивается сверху вниз по середине на два подмассива.

      1. Вычислить середину массива mid и рекурсивно разделить левый подмассив (интервал [left, mid] ) и правый подмассив (интервал [mid + 1, right] ).
      2. Рекурсивно повторять шаг 1. , пока длина подмассива не станет равной 1.
      -

      Этап "слияния" снизу вверх объединяет левый и правый подмассивы в один упорядоченный массив. Следует заметить, что начиная с подмассивов длины 1, каждый подмассив в фазе слияния уже является упорядоченным.

      +

      Этап «слияния» снизу вверх объединяет левый и правый подмассивы в один упорядоченный массив. Следует заметить, что начиная с подмассивов длины 1, каждый подмассив в фазе слияния уже является упорядоченным.

      @@ -5046,10 +5046,10 @@

      11.6.3   Сортировка связного списка

      Для связных списков сортировка слиянием имеет заметное преимущество перед другими алгоритмами сортировки: пространственную сложность задачи сортировки списка можно оптимизировать до \(O(1)\).

        -
      • Этап разделения: работу по разбиению списка можно реализовать с помощью "итерации" вместо "рекурсии", тем самым устранив расход памяти на стек вызовов.
      • +
      • Этап разделения: работу по разбиению списка можно реализовать с помощью «итерации» вместо «рекурсии», тем самым устранив расход памяти на стек вызовов.
      • Этап слияния: в связном списке добавление и удаление узлов требует только изменения ссылок (указателей), поэтому при слиянии двух коротких упорядоченных списков в один длинный упорядоченный список не нужно создавать дополнительный список.
      -

      Детали реализации достаточно сложны; заинтересованные читатели могут обратиться к соответствующим материалам самостоятельно.

      +

      Детали реализации достаточно сложны. Заинтересованные читатели могут обратиться к соответствующим материалам самостоятельно.

      diff --git a/ru/chapter_sorting/quick_sort/index.html b/ru/chapter_sorting/quick_sort/index.html index 616179029..b35d06f0e 100644 --- a/ru/chapter_sorting/quick_sort/index.html +++ b/ru/chapter_sorting/quick_sort/index.html @@ -4423,8 +4423,8 @@

      11.5   Быстрая сортировка

      -

      Быстрая сортировка (quick sort) - это алгоритм сортировки, основанный на стратегии "разделяй и властвуй"; он работает эффективно и применяется очень широко.

      -

      Ключевая операция быстрой сортировки - это "разделение с опорным элементом". Ее цель такова: выбрать некоторый элемент массива в качестве "опорного" и переместить все элементы меньше опорного влево от него, а все элементы больше опорного - вправо. Конкретный процесс показан на рисунке 11-8.

      +

      Быстрая сортировка (quick sort) - это алгоритм сортировки, основанный на стратегии «разделяй и властвуй». Он работает эффективно и применяется очень широко.

      +

      Ключевая операция быстрой сортировки - это «разделение с опорным элементом». Ее цель такова: выбрать некоторый элемент массива в качестве «опорного» и переместить все элементы меньше опорного влево от него, а все элементы больше опорного - вправо. Конкретный процесс показан на рисунке 11-8.

      1. Выбрать самый левый элемент массива как опорный и инициализировать два указателя i и j , направленные на левую и правую границы массива.
      2. Запустить цикл, в котором i и j ищут соответственно первый элемент, больший опорного, и первый элемент, меньший опорного, после чего эти два элемента меняются местами.
      3. @@ -4463,7 +4463,7 @@

      Рисунок 11-8   Шаги разделения с опорным элементом

      -

      После завершения разделения исходный массив разбивается на три части: левый подмассив, опорный элемент и правый подмассив; при этом выполняется условие "любой элемент левого подмассива \(\leq\) опорный элемент \(\leq\) любой элемент правого подмассива". Следовательно, далее нам нужно лишь отсортировать эти два подмассива.

      +

      После завершения разделения исходный массив разбивается на три части: левый подмассив, опорный элемент и правый подмассив. При этом выполняется условие «любой элемент левого подмассива \(\leq\) опорный элемент \(\leq\) любой элемент правого подмассива». Следовательно, далее нам нужно лишь отсортировать эти два подмассива.

      Стратегия разделяй и властвуй в быстрой сортировке

      Иными словами, разделение с опорным элементом сводит задачу сортировки длинного массива к двум задачам сортировки более коротких массивов.

      @@ -4773,9 +4773,9 @@

      11.5.1   Алгоритм

      Общий процесс быстрой сортировки показан на рисунке 11-9.

        -
      1. Сначала выполнить "разделение с опорным элементом" для исходного массива и получить неотсортированные левый и правый подмассивы.
      2. -
      3. Затем рекурсивно выполнить "разделение с опорным элементом" для левого и правого подмассивов.
      4. -
      5. Продолжать рекурсию до тех пор, пока длина подмассива не станет равной 1; после этого сортировка всего массива будет завершена.
      6. +
      7. Сначала выполнить «разделение с опорным элементом» для исходного массива и получить неотсортированные левый и правый подмассивы.
      8. +
      9. Затем рекурсивно выполнить «разделение с опорным элементом» для левого и правого подмассивов.
      10. +
      11. Продолжать рекурсию до тех пор, пока длина подмассива не станет равной 1. После этого сортировка всего массива будет завершена.

      Процесс быстрой сортировки

      Рисунок 11-9   Процесс быстрой сортировки

      @@ -4975,22 +4975,22 @@

      11.5.2   Характеристики алгоритма

        -
      • Временная сложность равна \(O(n \log n)\), алгоритм не является адаптивным: в среднем глубина рекурсии при разделении равна \(\log n\) , а суммарное число циклов на каждом уровне равно \(n\) , поэтому общая сложность составляет \(O(n \log n)\) . В худшем случае каждое разделение делит массив длины \(n\) на подмассивы длины \(0\) и \(n - 1\) ; тогда глубина рекурсии достигает \(n\) , на каждом уровне выполняется \(n\) операций, и общая временная сложность вырождается в \(O(n^2)\) .
      • +
      • Временная сложность равна \(O(n \log n)\), алгоритм не является адаптивным: в среднем глубина рекурсии при разделении равна \(\log n\) , а суммарное число циклов на каждом уровне равно \(n\) , поэтому общая сложность составляет \(O(n \log n)\) . В худшем случае каждое разделение делит массив длины \(n\) на подмассивы длины \(0\) и \(n - 1\). Тогда глубина рекурсии достигает \(n\) , на каждом уровне выполняется \(n\) операций, и общая временная сложность вырождается в \(O(n^2)\) .
      • Пространственная сложность равна \(O(n)\), сортировка выполняется на месте: если входной массив полностью отсортирован в обратном порядке, глубина рекурсии достигает худшего случая \(n\) , что требует \(O(n)\) памяти под стек вызовов. При этом сама сортировка выполняется в исходном массиве без дополнительного массива.
      • Нестабильная сортировка: на последнем шаге разделения опорный элемент может быть обменян вправо от равного ему элемента.

      11.5.3   Почему быстрая сортировка быстрая

      -

      Уже по названию понятно, что быстрая сортировка должна иметь преимущества по эффективности. Хотя ее средняя временная сложность совпадает со сложностью "сортировки слиянием" и "пирамидальной сортировки", на практике быстрая сортировка обычно работает быстрее. Основные причины таковы.

      +

      Уже по названию понятно, что быстрая сортировка должна иметь преимущества по эффективности. Хотя ее средняя временная сложность совпадает со сложностью «сортировки слиянием» и «пирамидальной сортировки», на практике быстрая сортировка обычно работает быстрее. Основные причины таковы.

      • Вероятность худшего случая очень мала: хотя худшая временная сложность быстрой сортировки равна \(O(n^2)\) и она не так стабильна, как сортировка слиянием, в подавляющем большинстве случаев она работает за \(O(n \log n)\) .
      • -
      • Высокая эффективность использования кэша: при выполнении разделения система может загрузить весь подмассив в кэш, поэтому доступ к элементам оказывается быстрым. Алгоритмы вроде "пирамидальной сортировки" требуют скачкообразного доступа к элементам и таким свойством не обладают.
      • -
      • Небольшой константный множитель в сложности: среди трех перечисленных алгоритмов у быстрой сортировки обычно меньше всего сравнений, присваиваний и обменов. Это похоже на причину, по которой "сортировка вставками" часто быстрее "сортировки пузырьком".
      • +
      • Высокая эффективность использования кэша: при выполнении разделения система может загрузить весь подмассив в кэш, поэтому доступ к элементам оказывается быстрым. Алгоритмы вроде «пирамидальной сортировки» требуют скачкообразного доступа к элементам и таким свойством не обладают.
      • +
      • Небольшой константный множитель в сложности: среди трех перечисленных алгоритмов у быстрой сортировки обычно меньше всего сравнений, присваиваний и обменов. Это похоже на причину, по которой «сортировка вставками» часто быстрее «сортировки пузырьком».

      11.5.4   Оптимизация выбора опорного элемента

      -

      На некоторых входных данных временная эффективность быстрой сортировки может ухудшаться. Рассмотрим крайний случай: входной массив полностью отсортирован в обратном порядке. Поскольку в качестве опорного мы выбираем самый левый элемент, после разделения он будет обменян в самый правый конец массива, из-за чего длина левого подмассива станет \(n - 1\) , а длина правого - \(0\) . Если рекурсия будет продолжаться таким образом, то после каждого разделения один из подмассивов будет иметь длину \(0\) , стратегия "разделяй и властвуй" потеряет смысл, а быстрая сортировка выродится в нечто близкое к "сортировке пузырьком".

      +

      На некоторых входных данных временная эффективность быстрой сортировки может ухудшаться. Рассмотрим крайний случай: входной массив полностью отсортирован в обратном порядке. Поскольку в качестве опорного мы выбираем самый левый элемент, после разделения он будет обменян в самый правый конец массива, из-за чего длина левого подмассива станет \(n - 1\) , а длина правого - \(0\) . Если рекурсия будет продолжаться таким образом, то после каждого разделения один из подмассивов будет иметь длину \(0\) , стратегия «разделяй и властвуй» потеряет смысл, а быстрая сортировка выродится в нечто близкое к «сортировке пузырьком».

      Чтобы по возможности избежать такого сценария, можно улучшить стратегию выбора опорного элемента в процедуре разделения. Например, можно выбирать случайный элемент массива как опорный. Однако если не повезет и каждый раз будет выбираться неудачный опорный элемент, производительность все равно останется неудовлетворительной.

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

      -

      Чтобы улучшить ситуацию, можно взять три кандидата (обычно первый, последний и средний элементы массива) и использовать медиану этих трех значений как опорный элемент. Благодаря этому вероятность того, что опорный элемент окажется "не слишком маленьким и не слишком большим", заметно возрастает. Конечно, можно брать и большее число кандидатов, чтобы еще сильнее повысить устойчивость алгоритма. После этого вероятность деградации временной сложности до \(O(n^2)\) существенно уменьшается.

      +

      Чтобы улучшить ситуацию, можно взять три кандидата (обычно первый, последний и средний элементы массива) и использовать медиану этих трех значений как опорный элемент. Благодаря этому вероятность того, что опорный элемент окажется «не слишком маленьким и не слишком большим», заметно возрастает. Конечно, можно брать и большее число кандидатов, чтобы еще сильнее повысить устойчивость алгоритма. После этого вероятность деградации временной сложности до \(O(n^2)\) существенно уменьшается.

      Пример кода:

      @@ -5445,7 +5445,7 @@

      11.5.5   Оптимизация глубины рекурсии

      -

      На некоторых входных данных быстрая сортировка может занимать слишком много памяти. Рассмотрим полностью отсортированный входной массив. Пусть длина текущего подмассива в рекурсии равна \(m\) ; тогда после каждого разделения будут получаться левый подмассив длины \(0\) и правый подмассив длины \(m - 1\) . Это означает, что на каждом уровне размер задачи уменьшается совсем немного (лишь на один элемент), а высота дерева рекурсии достигает \(n - 1\) , поэтому требуется \(O(n)\) памяти под стек вызовов.

      +

      На некоторых входных данных быстрая сортировка может занимать слишком много памяти. Рассмотрим полностью отсортированный входной массив. Пусть длина текущего подмассива в рекурсии равна \(m\). Тогда после каждого разделения будут получаться левый подмассив длины \(0\) и правый подмассив длины \(m - 1\) . Это означает, что на каждом уровне размер задачи уменьшается совсем немного (лишь на один элемент), а высота дерева рекурсии достигает \(n - 1\) , поэтому требуется \(O(n)\) памяти под стек вызовов.

      Чтобы избежать накопления стековых кадров, после каждого разделения можно сравнивать длины двух подмассивов и рекурсивно обрабатывать только более короткий из них. Поскольку длина короткого подмассива не превысит \(n / 2\) , такой подход гарантирует, что глубина рекурсии не превысит \(\log n\) , а худшая пространственная сложность будет оптимизирована до \(O(\log n)\) . Код приведен ниже:

      diff --git a/ru/chapter_sorting/radix_sort/index.html b/ru/chapter_sorting/radix_sort/index.html index b23b44fe8..63e5e00f1 100644 --- a/ru/chapter_sorting/radix_sort/index.html +++ b/ru/chapter_sorting/radix_sort/index.html @@ -4357,13 +4357,13 @@

      11.10   Поразрядная сортировка

      -

      В предыдущем разделе была рассмотрена сортировка подсчетом: она хорошо подходит для случаев, когда объем данных \(n\) велик, а диапазон значений \(m\) сравнительно мал. Предположим теперь, что нужно отсортировать \(n = 10^6\) номеров студентов, причем каждый номер представляет собой \(8\)-значное число. Тогда диапазон данных \(m = 10^8\) оказывается очень большим; сортировка подсчетом потребует огромного объема памяти, а поразрядная сортировка позволяет этого избежать.

      +

      В предыдущем разделе была рассмотрена сортировка подсчетом: она хорошо подходит для случаев, когда объем данных \(n\) велик, а диапазон значений \(m\) сравнительно мал. Предположим теперь, что нужно отсортировать \(n = 10^6\) номеров студентов, причем каждый номер представляет собой \(8\)-значное число. Тогда диапазон данных \(m = 10^8\) оказывается очень большим. Сортировка подсчетом потребует огромного объема памяти, а поразрядная сортировка позволяет этого избежать.

      Поразрядная сортировка (radix sort) по своей основной идее совпадает с сортировкой подсчетом и тоже реализует сортировку через подсчет количества. При этом поразрядная сортировка использует соотношение между разрядами числа и последовательно сортирует данные по каждому разряду, получая итоговый упорядоченный результат.

      11.10.1   Алгоритм

      Рассмотрим пример со студенческими номерами: будем считать, что младший разряд имеет номер \(1\) , а старший - номер \(8\) . Тогда процесс поразрядной сортировки показан на рисунке 11-18.

      1. Инициализировать номер разряда \(k = 1\) .
      2. -
      3. Выполнить "сортировку подсчетом" по \(k\)-му разряду студенческого номера. После этого данные будут упорядочены по \(k\)-му разряду по возрастанию.
      4. +
      5. Выполнить «сортировку подсчетом» по \(k\)-му разряду студенческого номера. После этого данные будут упорядочены по \(k\)-му разряду по возрастанию.
      6. Увеличить \(k\) на \(1\) и вернуться к шагу 2. , продолжая процесс, пока сортировка не будет выполнена для всех разрядов.

      Процесс поразрядной сортировки

      @@ -5059,7 +5059,7 @@ x_k = \lfloor\frac{x}{d^{k-1}}\rfloor \bmod d
      • Временная сложность равна \(O(nk)\), алгоритм не является адаптивным: пусть объем данных равен \(n\) , числа записаны в системе счисления с основанием \(d\) , а максимальное число разрядов равно \(k\) . Тогда выполнение сортировки подсчетом для одного разряда требует \(O(n + d)\) времени, а сортировка по всем \(k\) разрядам требует \(O((n + d)k)\) времени. Обычно \(d\) и \(k\) сравнительно малы, поэтому временная сложность стремится к \(O(n)\) .
      • Пространственная сложность равна \(O(n + d)\), сортировка не выполняется на месте: как и в сортировке подсчетом, здесь требуются массивы res и counter длины \(n\) и \(d\) .
      • -
      • Стабильная сортировка: если сортировка подсчетом стабильна, то и поразрядная сортировка стабильна; если же сортировка подсчетом нестабильна, поразрядная сортировка не может гарантировать корректный результат.
      • +
      • Стабильная сортировка: если сортировка подсчетом стабильна, то и поразрядная сортировка стабильна. Если же сортировка подсчетом нестабильна, поразрядная сортировка не может гарантировать корректный результат.
      diff --git a/ru/chapter_sorting/selection_sort/index.html b/ru/chapter_sorting/selection_sort/index.html index 223b4d899..4b909eacc 100644 --- a/ru/chapter_sorting/selection_sort/index.html +++ b/ru/chapter_sorting/selection_sort/index.html @@ -4336,7 +4336,7 @@

      11.2   Сортировка выбором

      Сортировка выбором (selection sort) работает очень просто: запускается цикл, и на каждом шаге из неотсортированного диапазона выбирается минимальный элемент, после чего он переносится в конец уже отсортированного диапазона.

      -

      Пусть длина массива равна \(n\) ; тогда процесс сортировки выбором выглядит так, как показано на рисунке 11-2.

      +

      Пусть длина массива равна \(n\). Тогда процесс сортировки выбором выглядит так, как показано на рисунке 11-2.

      1. В начальном состоянии все элементы не отсортированы, то есть неотсортированный диапазон индексов равен \([0, n-1]\) .
      2. Выбрать минимальный элемент из диапазона \([0, n-1]\) и поменять его местами с элементом в позиции \(0\) . После этого первый элемент массива отсортирован.
      3. @@ -4642,7 +4642,7 @@

        11.2.1   Характеристики алгоритма

          -
        • Временная сложность равна \(O(n^2)\), сортировка не является адаптивной: внешний цикл выполняется \(n - 1\) раз; в первом раунде длина неотсортированного диапазона равна \(n\) , а в последнем - \(2\) , то есть отдельные раунды содержат \(n\), \(n - 1\), \(\dots\), \(3\), \(2\) проходов внутреннего цикла, их сумма равна \(\frac{(n - 1)(n + 2)}{2}\) .
        • +
        • Временная сложность равна \(O(n^2)\), сортировка не является адаптивной: внешний цикл выполняется \(n - 1\) раз. В первом раунде длина неотсортированного диапазона равна \(n\) , а в последнем - \(2\) , то есть отдельные раунды содержат \(n\), \(n - 1\), \(\dots\), \(3\), \(2\) проходов внутреннего цикла, их сумма равна \(\frac{(n - 1)(n + 2)}{2}\) .
        • Пространственная сложность равна \(O(1)\), сортировка выполняется на месте: указатели \(i\) и \(j\) используют константный объем дополнительной памяти.
        • Нестабильная сортировка: как показано на рисунке 11-3, элемент nums[i] может быть переставлен вправо от другого равного ему элемента, из-за чего их относительный порядок изменится.
        diff --git a/ru/chapter_sorting/sorting_algorithm/index.html b/ru/chapter_sorting/sorting_algorithm/index.html index 23382f9eb..4d3bf64c7 100644 --- a/ru/chapter_sorting/sorting_algorithm/index.html +++ b/ru/chapter_sorting/sorting_algorithm/index.html @@ -4385,7 +4385,7 @@ ('E', 23)

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

      -

      Основанность на сравнении: сортировка на основе сравнений использует операторы сравнения (\(<\), \(=\), \(>\)), чтобы определить относительный порядок элементов и отсортировать массив; ее теоретически лучшая временная сложность равна \(O(n \log n)\) . А вот сортировка без сравнений не опирается на операторы сравнения, поэтому может достигать \(O(n)\) , но универсальность у нее ниже.

      +

      Основанность на сравнении: сортировка на основе сравнений использует операторы сравнения (\(<\), \(=\), \(>\)), чтобы определить относительный порядок элементов и отсортировать массив. Ее теоретически лучшая временная сложность равна \(O(n \log n)\) . А вот сортировка без сравнений не опирается на операторы сравнения, поэтому может достигать \(O(n)\) , но универсальность у нее ниже.

      11.1.2   Идеальный алгоритм сортировки

      Быстрый, выполняющийся на месте, стабильный, адаптивный и универсальный. Очевидно, что на сегодняшний день не существует алгоритма сортировки, который одновременно обладал бы всеми этими свойствами. Поэтому при выборе алгоритма сортировки нужно исходить из конкретных особенностей данных и требований задачи.

      Далее мы последовательно изучим разные алгоритмы сортировки и на основании приведенных выше критериев разберем их преимущества и недостатки.

      diff --git a/ru/chapter_sorting/summary/index.html b/ru/chapter_sorting/summary/index.html index e37309195..775db2cdf 100644 --- a/ru/chapter_sorting/summary/index.html +++ b/ru/chapter_sorting/summary/index.html @@ -4362,9 +4362,9 @@
    4. Сортировка пузырьком выполняет сортировку за счет обмена соседних элементов. Если добавить флаг для досрочного выхода, лучшую временную сложность пузырьковой сортировки можно оптимизировать до \(O(n)\) .
    5. Сортировка вставками на каждом раунде вставляет элемент из неотсортированного диапазона в правильную позицию внутри отсортированного диапазона. Хотя ее временная сложность равна \(O(n^2)\) , она очень популярна для задач сортировки небольших массивов, поскольку число элементарных операций у нее сравнительно невелико.
    6. Быстрая сортировка основана на операции разделения с опорным элементом. При неудачном выборе опорного элемента на каждом раунде ее временная сложность может деградировать до \(O(n^2)\) . Использование медианы трех элементов или случайного опорного элемента уменьшает вероятность этой деградации. Если всегда рекурсивно обрабатывать более короткий поддиапазон первым, можно эффективно уменьшить глубину рекурсии и оптимизировать пространственную сложность до \(O(\log n)\) .
    7. -
    8. Сортировка слиянием включает этапы разделения и слияния и служит типичным проявлением стратегии "разделяй и властвуй". Для сортировки массива ей требуется вспомогательный массив, поэтому пространственная сложность равна \(O(n)\) ; однако при сортировке связного списка пространственную сложность можно оптимизировать до \(O(1)\) .
    9. -
    10. Блочная сортировка включает три этапа: распределение данных по блокам, сортировку внутри блоков и объединение результатов. Она тоже отражает стратегию "разделяй и властвуй" и подходит для очень больших объемов данных. Ключ к эффективности блочной сортировки - равномерное распределение данных.
    11. -
    12. Сортировка подсчетом является частным случаем блочной сортировки; она реализует сортировку через подсчет числа вхождений данных. Сортировка подсчетом подходит для случаев, когда объем данных велик, но диапазон значений ограничен, и при этом данные можно преобразовать в положительные целые числа.
    13. +
    14. Сортировка слиянием включает этапы разделения и слияния и служит типичным проявлением стратегии «разделяй и властвуй». Для сортировки массива ей требуется вспомогательный массив, поэтому пространственная сложность равна \(O(n)\). Однако при сортировке связного списка пространственную сложность можно оптимизировать до \(O(1)\) .
    15. +
    16. Блочная сортировка включает три этапа: распределение данных по блокам, сортировку внутри блоков и объединение результатов. Она тоже отражает стратегию «разделяй и властвуй» и подходит для очень больших объемов данных. Ключ к эффективности блочной сортировки - равномерное распределение данных.
    17. +
    18. Сортировка подсчетом является частным случаем блочной сортировки. Она реализует сортировку через подсчет числа вхождений данных. Сортировка подсчетом подходит для случаев, когда объем данных велик, но диапазон значений ограничен, и при этом данные можно преобразовать в положительные целые числа.
    19. Поразрядная сортировка выполняет сортировку данных путем последовательной сортировки по каждому разряду и требует, чтобы данные можно было представить в виде чисел фиксированной разрядности.
    20. В общем случае нам хотелось бы найти алгоритм сортировки, который одновременно обладал бы высокой эффективностью, стабильностью, выполнением на месте и адаптивностью. Но, как и в других разделах алгоритмов и структур данных, не существует одного алгоритма сортировки, способного удовлетворить всем этим требованиям одновременно. На практике приходится выбирать подходящий алгоритм в зависимости от свойств данных.
    21. На рисунке 11-19 сравниваются эффективность, стабильность, выполнение на месте и адаптивность основных алгоритмов сортировки.
    22. @@ -4376,14 +4376,14 @@

      В: В каких случаях стабильность алгоритма сортировки является обязательной?

      В реальных задачах нам может понадобиться сортировать объекты по некоторому атрибуту. Например, у студентов есть два атрибута: имя и рост. Мы хотим выполнить многоуровневую сортировку: сначала отсортировать по имени и получить (A, 180) (B, 185) (C, 170) (D, 170) , а затем отсортировать по росту. Если используемый алгоритм сортировки нестабилен, то мы можем получить (D, 170) (C, 170) (A, 180) (B, 185) .

      Нетрудно увидеть, что в этом случае студенты D и C поменялись местами, порядок по имени разрушился, а именно этого мы и не хотим.

      -

      В: Можно ли поменять местами порядок "поиска справа налево" и "поиска слева направо" в разделении с опорным элементом?

      -

      Нет. Если в качестве опорного элемента выбирается самый левый элемент, необходимо сначала выполнять "поиск справа налево", а уже затем - "поиск слева направо". Этот вывод кажется немного неочевидным, поэтому разберем его подробнее.

      -

      Последний шаг partition() - это обмен nums[left] и nums[i] . После обмена все элементы слева от опорного должны быть <= опорного, а значит, перед этим обменом должно выполняться условие nums[left] >= nums[i]. Если сначала выполнять "поиск слева направо", то в случае, когда не удается найти элемент больше опорного, цикл завершится в состоянии i == j , и при этом может оказаться, что nums[j] == nums[i] > nums[left]. Иными словами, на последнем шаге обмена элемент, больший опорного, будет помещен в начало массива, из-за чего разделение завершится неверно.

      -

      Например, для массива [0, 0, 0, 0, 1] , если сначала выполнять "поиск слева направо", после разделения получится [1, 0, 0, 0, 0] , а это неправильный результат.

      -

      Если же выбрать nums[right] в качестве опорного элемента, то ситуация станет противоположной, и тогда сначала нужно выполнять "поиск слева направо".

      +

      В: Можно ли поменять местами порядок «поиска справа налево» и «поиска слева направо» в разделении с опорным элементом?

      +

      Нет. Если в качестве опорного элемента выбирается самый левый элемент, необходимо сначала выполнять «поиск справа налево», а уже затем - «поиск слева направо». Этот вывод кажется немного неочевидным, поэтому разберем его подробнее.

      +

      Последний шаг partition() - это обмен nums[left] и nums[i] . После обмена все элементы слева от опорного должны быть <= опорного, а значит, перед этим обменом должно выполняться условие nums[left] >= nums[i]. Если сначала выполнять «поиск слева направо», то в случае, когда не удается найти элемент больше опорного, цикл завершится в состоянии i == j , и при этом может оказаться, что nums[j] == nums[i] > nums[left]. Иными словами, на последнем шаге обмена элемент, больший опорного, будет помещен в начало массива, из-за чего разделение завершится неверно.

      +

      Например, для массива [0, 0, 0, 0, 1] , если сначала выполнять «поиск слева направо», после разделения получится [1, 0, 0, 0, 0] , а это неправильный результат.

      +

      Если же выбрать nums[right] в качестве опорного элемента, то ситуация станет противоположной, и тогда сначала нужно выполнять «поиск слева направо».

      В: Почему при оптимизации глубины рекурсии в быстрой сортировке выбор короткого массива гарантирует, что глубина рекурсии не превысит \(\log n\) ?

      Глубина рекурсии - это число текущих рекурсивных вызовов, которые еще не завершились. На каждом раунде разделения исходный массив разбивается на два подмассива. После оптимизации глубины рекурсии длина подмассива, в который мы продолжаем рекурсивный спуск, не превышает половины длины исходного массива. Если рассматривать худший случай, когда длина каждый раз становится ровно вдвое меньше, итоговая глубина рекурсии и будет равна \(\log n\) .

      -

      В исходной версии быстрой сортировки может происходить последовательный рекурсивный вызов для более длинных массивов; в худшем случае это будут длины \(n\) , \(n - 1\) , \(\dots\) , \(2\) , \(1\) , а глубина рекурсии окажется равной \(n\) . Оптимизация глубины рекурсии как раз и позволяет избежать такого сценария.

      +

      В исходной версии быстрой сортировки может происходить последовательный рекурсивный вызов для более длинных массивов. В худшем случае это будут длины \(n\) , \(n - 1\) , \(\dots\) , \(2\) , \(1\) , а глубина рекурсии окажется равной \(n\) . Оптимизация глубины рекурсии как раз и позволяет избежать такого сценария.

      В: Если все элементы массива равны, будет ли временная сложность быстрой сортировки равна \(O(n^2)\) ? Как справиться с таким вырождением?

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

      В: Почему худшая временная сложность блочной сортировки равна \(O(n^2)\) ?

      diff --git a/ru/chapter_stack_and_queue/deque/index.html b/ru/chapter_stack_and_queue/deque/index.html index d6257d231..262f05355 100644 --- a/ru/chapter_stack_and_queue/deque/index.html +++ b/ru/chapter_stack_and_queue/deque/index.html @@ -4836,7 +4836,7 @@

      1.   Реализация на основе двусвязного списка

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

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

      -

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

      +

      Как показано на рисунке 5-8, мы рассматриваем головной и хвостовой узлы двусвязного списка как голову и хвост двусторонней очереди и одновременно реализуем функции добавления и удаления узлов с обеих сторон.

      @@ -6563,7 +6563,7 @@

      2.   Реализация на основе массива

      -

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

      +

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

      @@ -7991,7 +7991,7 @@

      5.3.3   Применение двусторонней очереди

      Двусторонняя очередь сочетает в себе логику стека и очереди, поэтому она может покрыть все сценарии применения обеих структур и при этом предоставляет более высокую степень свободы.

      -

      Мы знаем, что функция "undo" в программном обеспечении обычно реализуется с помощью стека: система помещает каждое изменение в стек с помощью push , а затем использует pop для отмены. Однако, учитывая ограниченность системных ресурсов, программы обычно ограничивают число шагов отмены, например разрешают хранить только \(50\) шагов. Когда длина стека превышает этот предел, программе нужно удалить элемент с дна стека, то есть с головы очереди. Но стек не может реализовать такую операцию, и в этом случае его приходится заменять двусторонней очередью. Обрати внимание: основная логика "undo" по-прежнему следует стековому правилу LIFO, просто двусторонняя очередь позволяет более гибко реализовать некоторые дополнительные механизмы.

      +

      Мы знаем, что функция «undo» в программном обеспечении обычно реализуется с помощью стека: система помещает каждое изменение в стек с помощью push , а затем использует pop для отмены. Однако, учитывая ограниченность системных ресурсов, программы обычно ограничивают число шагов отмены, например разрешают хранить только \(50\) шагов. Когда длина стека превышает этот предел, программе нужно удалить элемент с дна стека, то есть с головы очереди. Но стек не может реализовать такую операцию, и в этом случае его приходится заменять двусторонней очередью. Обрати внимание: основная логика «undo» по-прежнему следует стековому правилу LIFO, просто двусторонняя очередь позволяет более гибко реализовать некоторые дополнительные механизмы.

      diff --git a/ru/chapter_stack_and_queue/index.html b/ru/chapter_stack_and_queue/index.html index fecae27c6..82b0b6a96 100644 --- a/ru/chapter_stack_and_queue/index.html +++ b/ru/chapter_stack_and_queue/index.html @@ -4280,7 +4280,7 @@

      Abstract

      Стек и очередь - две базовые линейные структуры данных.

      -

      Они соответственно воплощают принципы "последним пришел - первым вышел" и "первым пришел - первым вышел".

      +

      Они соответственно воплощают принципы «последним пришел - первым вышел» и «первым пришел - первым вышел».

      Содержание главы

        diff --git a/ru/chapter_stack_and_queue/queue/index.html b/ru/chapter_stack_and_queue/queue/index.html index 13ad4950e..de22c828f 100644 --- a/ru/chapter_stack_and_queue/queue/index.html +++ b/ru/chapter_stack_and_queue/queue/index.html @@ -4435,8 +4435,8 @@

        5.2   Очередь

        -

        Очередь (queue) - это линейная структура данных, подчиняющаяся правилу "первым пришел - первым вышел". Как видно из названия, очередь моделирует обычную ситуацию ожидания: новые люди непрерывно присоединяются к хвосту очереди, а стоящие в начале по одному уходят.

        -

        Как показано на рисунке 5-4, начало очереди называется головой очереди, а конец - хвостом очереди; операцию добавления элемента в хвост называют enqueue, а операцию удаления элемента из головы - dequeue.

        +

        Очередь (queue) - это линейная структура данных, подчиняющаяся правилу «первым пришел - первым вышел». Как видно из названия, очередь моделирует обычную ситуацию ожидания: новые люди непрерывно присоединяются к хвосту очереди, а стоящие в начале по одному уходят.

        +

        Как показано на рисунке 5-4, начало очереди называется головой очереди, а конец - хвостом очереди. Операцию добавления элемента в хвост называют enqueue, а операцию удаления элемента из головы - dequeue.

        Правило FIFO для очереди

        Рисунок 5-4   Правило FIFO для очереди

        @@ -4792,7 +4792,7 @@

        https://pythontutor.com/render.html#code=from%20collections%20import%20deque%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C%0A%20%20%20%20%23%20%D0%92%20Python%20%D0%B4%D0%B2%D1%83%D1%81%D1%82%D0%BE%D1%80%D0%BE%D0%BD%D0%BD%D1%8E%D1%8E%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C%20deque%20%D0%BE%D0%B1%D1%8B%D1%87%D0%BD%D0%BE%20%D0%B8%D1%81%D0%BF%D0%BE%D0%BB%D1%8C%D0%B7%D1%83%D1%8E%D1%82%20%D0%BA%D0%B0%D0%BA%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C%0A%20%20%20%20%23%20%D0%A5%D0%BE%D1%82%D1%8F%20queue.Queue%28%29%20%D1%8F%D0%B2%D0%BB%D1%8F%D0%B5%D1%82%D1%81%D1%8F%20%D0%BD%D0%B0%D1%81%D1%82%D0%BE%D1%8F%D1%89%D0%B8%D0%BC%20%D0%BA%D0%BB%D0%B0%D1%81%D1%81%D0%BE%D0%BC%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%2C%20%D0%BF%D0%BE%D0%BB%D1%8C%D0%B7%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%D1%81%D1%8F%20%D0%B8%D0%BC%20%D0%BD%D0%B5%20%D1%81%D0%BB%D0%B8%D1%88%D0%BA%D0%BE%D0%BC%20%D1%83%D0%B4%D0%BE%D0%B1%D0%BD%D0%BE%0A%20%20%20%20que%20%3D%20deque%28%29%0A%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BC%D0%B5%D1%81%D1%82%D0%B8%D1%82%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B2%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C%0A%20%20%20%20que.append%281%29%0A%20%20%20%20que.append%283%29%0A%20%20%20%20que.append%282%29%0A%20%20%20%20que.append%285%29%0A%20%20%20%20que.append%284%29%0A%20%20%20%20print%28%22%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C%20que%20%3D%22%2C%20que%29%0A%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B8%D1%82%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B2%20%D0%BD%D0%B0%D1%87%D0%B0%D0%BB%D0%B5%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%0A%20%20%20%20front%20%3D%20que%5B0%5D%0A%20%20%20%20print%28%22%D0%AD%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B2%20%D0%BD%D0%B0%D1%87%D0%B0%D0%BB%D0%B5%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%20front%20%3D%22%2C%20front%29%0A%0A%20%20%20%20%23%20%D0%98%D0%B7%D0%B2%D0%BB%D0%B5%D1%87%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B8%D0%B7%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%0A%20%20%20%20pop%20%3D%20que.popleft%28%29%0A%20%20%20%20print%28%22%D0%98%D0%B7%D0%B2%D0%BB%D0%B5%D1%87%D0%B5%D0%BD%D0%BD%D1%8B%D0%B9%20%D0%B8%D0%B7%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20pop%20%3D%22%2C%20pop%29%0A%20%20%20%20print%28%22que%20%D0%BF%D0%BE%D1%81%D0%BB%D0%B5%20%D0%B8%D0%B7%D0%B2%D0%BB%D0%B5%D1%87%D0%B5%D0%BD%D0%B8%D1%8F%20%3D%22%2C%20que%29%0A%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B8%D1%82%D1%8C%20%D0%B4%D0%BB%D0%B8%D0%BD%D1%83%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%0A%20%20%20%20size%20%3D%20len%28que%29%0A%20%20%20%20print%28%22%D0%94%D0%BB%D0%B8%D0%BD%D0%B0%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%20size%20%3D%22%2C%20size%29%0A%0A%20%20%20%20%23%20%D0%9F%D1%80%D0%BE%D0%B2%D0%B5%D1%80%D0%B8%D1%82%D1%8C%2C%20%D0%BF%D1%83%D1%81%D1%82%D0%B0%20%D0%BB%D0%B8%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C%0A%20%20%20%20is_empty%20%3D%20len%28que%29%20%3D%3D%200%0A%20%20%20%20print%28%22%D0%9F%D1%83%D1%81%D1%82%D0%B0%20%D0%BB%D0%B8%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C%20%3D%22%2C%20is_empty%29&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

        5.2.2   Реализация очереди

        -

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

        +

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

        1.   Реализация на основе связного списка

        Как показано на рисунке 5-5, мы можем рассматривать головной узел и хвостовой узел связного списка как голову очереди и хвост очереди соответственно, договорившись, что добавлять узлы можно только в хвост, а удалять - только из головы.

        @@ -5715,7 +5715,7 @@

        2.   Реализация на основе массива

        Удаление первого элемента из массива имеет временную сложность \(O(n)\) , из-за чего операция dequeue оказывается неэффективной. Однако этого можно избежать с помощью следующего приема.

        -

        Мы можем использовать переменную front , указывающую на индекс элемента в голове очереди, и поддерживать переменную size , которая хранит длину очереди. Определим rear = front + size ; эта формула дает позицию rear, указывающую на ячейку сразу после хвоста очереди.

        +

        Мы можем использовать переменную front , указывающую на индекс элемента в голове очереди, и поддерживать переменную size , которая хранит длину очереди. Определим rear = front + size. Эта формула дает позицию rear, указывающую на ячейку сразу после хвоста очереди.

        Исходя из этого, эффективный диапазон элементов массива равен [front, rear - 1], а различные операции реализуются, как показано на рисунке 5-6.

        • Операция enqueue: записать входной элемент по индексу rear и увеличить size на 1.
        • @@ -6669,7 +6669,7 @@

          5.2.3   Типичные применения очереди

          • Очереди заказов. После оформления заказа покупателем заказ попадает в очередь, а затем система обрабатывает заказы по порядку. Во время крупных распродаж за короткое время возникает огромный поток заказов, и высокая конкурентная нагрузка становится ключевой инженерной проблемой.
          • -
          • Различные отложенные задачи. Любой сценарий, где нужно реализовать принцип "кто раньше пришел, тот раньше обслуживается", например очередь заданий принтера или очередь блюд на кухне ресторана, хорошо моделируется очередью, которая эффективно поддерживает нужный порядок обработки.
          • +
          • Различные отложенные задачи. Любой сценарий, где нужно реализовать принцип «кто раньше пришел, тот раньше обслуживается», например очередь заданий принтера или очередь блюд на кухне ресторана, хорошо моделируется очередью, которая эффективно поддерживает нужный порядок обработки.
          diff --git a/ru/chapter_stack_and_queue/stack/index.html b/ru/chapter_stack_and_queue/stack/index.html index 7a2276737..ab6e24434 100644 --- a/ru/chapter_stack_and_queue/stack/index.html +++ b/ru/chapter_stack_and_queue/stack/index.html @@ -4457,8 +4457,8 @@

          5.1   Стек

          -

          Стек (stack) - это линейная структура данных, подчиняющаяся логике "последним пришел - первым вышел".

          -

          Стек можно сравнить со стопкой тарелок на столе. Если разрешено перемещать только одну тарелку за раз, то, чтобы достать тарелку снизу, сначала придется по одной убрать все тарелки сверху. Если заменить тарелки различными элементами, например целыми числами, символами, объектами и т.д., получится структура данных "стек".

          +

          Стек (stack) - это линейная структура данных, подчиняющаяся логике «последним пришел - первым вышел».

          +

          Стек можно сравнить со стопкой тарелок на столе. Если разрешено перемещать только одну тарелку за раз, то, чтобы достать тарелку снизу, сначала придется по одной убрать все тарелки сверху. Если заменить тарелки различными элементами, например целыми числами, символами, объектами и т.д., получится структура данных «стек».

          Как показано на рисунке 5-1, верхнюю часть стопки элементов мы называем вершиной стека, а нижнюю - основанием стека. Операция добавления элемента на вершину называется push, а операция удаления верхнего элемента - pop.

          Правило LIFO для стека

          Рисунок 5-1   Правило LIFO для стека

          @@ -6216,10 +6216,10 @@

          Пространственная эффективность

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

          Однако, поскольку узлы связного списка должны дополнительно хранить указатели, узлы списка сами по себе занимают больше пространства.

          -

          В итоге нельзя просто сказать, какая из реализаций более экономна по памяти; это нужно анализировать в контексте конкретной задачи.

          +

          В итоге нельзя просто сказать, какая из реализаций более экономна по памяти. Это нужно анализировать в контексте конкретной задачи.

          5.1.4   Типичные применения стека

            -
          • Кнопки "назад" и "вперед" в браузере, undo и redo в программах. Каждый раз, когда мы открываем новую страницу, браузер помещает предыдущую страницу в стек, чтобы по операции "назад" можно было вернуться к ней. Операция "назад" по сути является pop . Если нужно одновременно поддерживать и "назад", и "вперед", то обычно используются два стека.
          • +
          • Кнопки «назад» и «вперед» в браузере, undo и redo в программах. Каждый раз, когда мы открываем новую страницу, браузер помещает предыдущую страницу в стек, чтобы по операции «назад» можно было вернуться к ней. Операция «назад» по сути является pop . Если нужно одновременно поддерживать и «назад», и «вперед», то обычно используются два стека.
          • Управление памятью программы. Каждый раз при вызове функции система помещает на вершину стека стековый кадр, в котором хранится контекст функции. В рекурсивной функции на этапе углубления рекурсии непрерывно выполняются операции push , а на этапе возврата - операции pop .
          diff --git a/ru/chapter_stack_and_queue/summary/index.html b/ru/chapter_stack_and_queue/summary/index.html index 67f2e834e..205f6b50e 100644 --- a/ru/chapter_stack_and_queue/summary/index.html +++ b/ru/chapter_stack_and_queue/summary/index.html @@ -4359,25 +4359,25 @@

          5.4   Резюме

          1.   Основные выводы

            -
          • Стек - это структура данных, следующая правилу "последним пришел - первым вышел", и его можно реализовать с помощью массива или связного списка.
          • +
          • Стек - это структура данных, следующая правилу «последним пришел - первым вышел», и его можно реализовать с помощью массива или связного списка.
          • С точки зрения временной эффективности реализация стека на массиве обычно работает быстрее в среднем, но во время расширения емкости временная сложность отдельной операции push может ухудшаться до \(O(n)\) . Напротив, реализация стека на связном списке дает более стабильные характеристики.
          • С точки зрения использования памяти реализация стека на массиве может приводить к некоторой потере пространства. Однако следует учитывать, что узлы связного списка занимают больше памяти, чем элементы массива.
          • -
          • Очередь - это структура данных, следующая правилу "первым пришел - первым вышел", и ее также можно реализовать с помощью массива или связного списка. Сравнение временной и пространственной эффективности для очереди в целом приводит к тем же выводам, что и для стека.
          • +
          • Очередь - это структура данных, следующая правилу «первым пришел - первым вышел», и ее также можно реализовать с помощью массива или связного списка. Сравнение временной и пространственной эффективности для очереди в целом приводит к тем же выводам, что и для стека.
          • Двусторонняя очередь - это очередь с более высокой степенью свободы, которая позволяет добавлять и удалять элементы с обоих концов.

          2.   Q & A

          -

          Q: Реализованы ли кнопки "вперед" и "назад" в браузере с помощью двусвязного списка?

          -

          По сути, функция переходов "вперед/назад" в браузере отражает логику стека. Когда пользователь открывает новую страницу, она помещается на вершину стека; когда пользователь нажимает кнопку "назад", эта страница снимается с вершины стека. Двусторонняя очередь позволяет удобно реализовать некоторые дополнительные операции, об этом уже упоминалось в разделе "Двусторонняя очередь".

          +

          Q: Реализованы ли кнопки «вперед» и «назад» в браузере с помощью двусвязного списка?

          +

          По сути, функция переходов «вперед/назад» в браузере отражает логику стека. Когда пользователь открывает новую страницу, она помещается на вершину стека. Когда пользователь нажимает кнопку «назад», эта страница снимается с вершины стека. Двусторонняя очередь позволяет удобно реализовать некоторые дополнительные операции, об этом уже упоминалось в разделе «Двусторонняя очередь».

          Q: Нужно ли освобождать память узла после извлечения его из стека?

          -

          Если извлеченный узел еще понадобится, память освобождать не нужно. Если он больше не нужен, то в языках Java и Python есть автоматический сборщик мусора, поэтому ручное освобождение памяти не требуется; в C и C++ память нужно освобождать вручную.

          +

          Если извлеченный узел еще понадобится, память освобождать не нужно. Если он больше не нужен, то в языках Java и Python есть автоматический сборщик мусора, поэтому ручное освобождение памяти не требуется. В C и C++ память нужно освобождать вручную.

          Q: Двусторонняя очередь выглядит как два соединенных стека. Для чего она нужна?

          Двусторонняя очередь похожа на комбинацию стека и очереди или на два соединенных стека. Она объединяет логику обеих структур, поэтому может покрыть все их применения и при этом остается более гибкой.

          Q: Как именно реализуются отмена (undo) и повтор (redo)?

          Используются два стека: стек A для отмены и стек B для повтора.

          1. Каждый раз, когда пользователь выполняет действие, это действие помещается в стек A , а стек B очищается.
          2. -
          3. Когда пользователь выполняет "undo", последнее действие извлекается из стека A и помещается в стек B .
          4. -
          5. Когда пользователь выполняет "redo", последнее действие извлекается из стека B и помещается обратно в стек A .
          6. +
          7. Когда пользователь выполняет «undo», последнее действие извлекается из стека A и помещается в стек B .
          8. +
          9. Когда пользователь выполняет «redo», последнее действие извлекается из стека B и помещается обратно в стек A .
          diff --git a/ru/chapter_tree/array_representation_of_tree/index.html b/ru/chapter_tree/array_representation_of_tree/index.html index 0e4bd0241..770f4ee71 100644 --- a/ru/chapter_tree/array_representation_of_tree/index.html +++ b/ru/chapter_tree/array_representation_of_tree/index.html @@ -4389,7 +4389,7 @@

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

          7.3.2   Представление произвольного двоичного дерева

          -

          Идеальное двоичное дерево - лишь частный случай; в обычной двоичной структуре на промежуточных уровнях часто существует множество None . Поскольку последовательность обхода по уровням не содержит этих None , мы не можем по одной лишь этой последовательности определить их количество и расположение. Это означает, что одному и тому же обходу по уровням может соответствовать сразу несколько различных структур двоичного дерева.

          +

          Идеальное двоичное дерево - лишь частный случай. В обычной двоичной структуре на промежуточных уровнях часто существует множество None . Поскольку последовательность обхода по уровням не содержит этих None , мы не можем по одной лишь этой последовательности определить их количество и расположение. Это означает, что одному и тому же обходу по уровням может соответствовать сразу несколько различных структур двоичного дерева.

          Как показано на рисунке 7-13, для неполной двоичной структуры описанный выше способ представления массивом уже перестает работать.

          Одной последовательности обхода по уровням соответствуют разные двоичные структуры

          Рисунок 7-13   Одной последовательности обхода по уровням соответствуют разные двоичные структуры

          diff --git a/ru/chapter_tree/avl_tree/index.html b/ru/chapter_tree/avl_tree/index.html index d0b1f00ae..a9ce5d587 100644 --- a/ru/chapter_tree/avl_tree/index.html +++ b/ru/chapter_tree/avl_tree/index.html @@ -4657,7 +4657,7 @@

          7.5   AVL-дерево *

          -

          В разделе "Двоичное дерево поиска" мы упоминали, что после многократных операций вставки и удаления узлов двоичное дерево поиска может выродиться в связный список. В таком случае временная сложность всех операций ухудшается с \(O(\log n)\) до \(O(n)\) .

          +

          В разделе «Двоичное дерево поиска» мы упоминали, что после многократных операций вставки и удаления узлов двоичное дерево поиска может выродиться в связный список. В таком случае временная сложность всех операций ухудшается с \(O(\log n)\) до \(O(n)\) .

          Как показано на рисунке 7-24, после двух операций удаления узлов это двоичное дерево поиска вырождается в связный список.

          Деградация AVL-дерева после удаления узлов

          Рисунок 7-24   Деградация AVL-дерева после удаления узлов

          @@ -4666,7 +4666,7 @@

          Деградация AVL-дерева после вставки узлов

          Рисунок 7-25   Деградация AVL-дерева после вставки узлов

          -

          В 1962 году Г. М. Adelson-Velsky и Е. М. Landis в статье "An algorithm for the organization of information" предложили AVL-дерево. В статье подробно описан набор операций, гарантирующий, что при непрерывном добавлении и удалении узлов AVL-дерево не вырождается, благодаря чему временная сложность различных операций сохраняется на уровне \(O(\log n)\) . Иначе говоря, в сценариях, где часто выполняются вставка, удаление, поиск и изменение, AVL-дерево всегда поддерживает эффективную работу с данными и потому имеет высокую практическую ценность.

          +

          В 1962 году Г. М. Adelson-Velsky и Е. М. Landis в статье «An algorithm for the organization of information» предложили AVL-дерево. В статье подробно описан набор операций, гарантирующий, что при непрерывном добавлении и удалении узлов AVL-дерево не вырождается, благодаря чему временная сложность различных операций сохраняется на уровне \(O(\log n)\) . Иначе говоря, в сценариях, где часто выполняются вставка, удаление, поиск и изменение, AVL-дерево всегда поддерживает эффективную работу с данными и потому имеет высокую практическую ценность.

          7.5.1   Распространенные термины AVL-дерева

          AVL-дерево одновременно является и двоичным деревом поиска, и сбалансированным двоичным деревом, то есть одновременно удовлетворяет всем свойствам обеих этих структур. Поэтому AVL-дерево является разновидностью сбалансированного двоичного дерева поиска (balanced binary search tree).

          1.   Высота узла

          @@ -4857,7 +4857,7 @@
      -

      "Высота узла" означает расстояние от этого узла до самого удаленного листового узла, то есть число пройденных "ребер". Особенно важно помнить, что высота листового узла равна \(0\) , а высота пустого узла равна \(-1\) . Мы создадим две вспомогательные функции: одну для получения высоты узла, другую для ее обновления:

      +

      «Высота узла» означает расстояние от этого узла до самого удаленного листового узла, то есть число пройденных «ребер». Особенно важно помнить, что высота листового узла равна \(0\) , а высота пустого узла равна \(-1\) . Мы создадим две вспомогательные функции: одну для получения высоты узла, другую для ее обновления:

      @@ -5074,7 +5074,7 @@

      2.   Баланс-фактор узла

      -

      Баланс-фактор (balance factor) узла определяется как высота левого поддерева минус высота правого поддерева; при этом баланс-фактор пустого узла считается равным \(0\) . Мы также инкапсулируем получение баланс-фактора в отдельную функцию, чтобы потом было удобнее ее использовать:

      +

      Баланс-фактор (balance factor) узла определяется как высота левого поддерева минус высота правого поддерева. При этом баланс-фактор пустого узла считается равным \(0\) . Мы также инкапсулируем получение баланс-фактора в отдельную функцию, чтобы потом было удобнее ее использовать:

      @@ -5222,13 +5222,13 @@

      Tip

      -

      Пусть баланс-фактор равен \(f\) ; тогда для любого узла AVL-дерева выполняется \(-1 \le f \le 1\) .

      +

      Пусть баланс-фактор равен \(f\). Тогда для любого узла AVL-дерева выполняется \(-1 \le f \le 1\) .

      7.5.2   Вращения AVL-дерева

      -

      Особенность AVL-дерева заключается в операции "вращения", которая позволяет заново сбалансировать разбалансированный узел, не нарушая последовательность симметричного обхода двоичного дерева. Иначе говоря, операция вращения одновременно сохраняет свойство "двоичного дерева поиска" и возвращает дерево в состояние "сбалансированного двоичного дерева".

      -

      Узлы, для которых абсолютное значение баланс-фактора больше \(1\) , мы называем "разбалансированными узлами". В зависимости от вида разбаланса вращения делятся на четыре типа: правое вращение, левое вращение, сначала левое затем правое, и сначала правое затем левое. Ниже разберем их подробно.

      +

      Особенность AVL-дерева заключается в операции «вращения», которая позволяет заново сбалансировать разбалансированный узел, не нарушая последовательность симметричного обхода двоичного дерева. Иначе говоря, операция вращения одновременно сохраняет свойство «двоичного дерева поиска» и возвращает дерево в состояние «сбалансированного двоичного дерева».

      +

      Узлы, для которых абсолютное значение баланс-фактора больше \(1\) , мы называем «разбалансированными узлами». В зависимости от вида разбаланса вращения делятся на четыре типа: правое вращение, левое вращение, сначала левое затем правое, и сначала правое затем левое. Ниже разберем их подробно.

      1.   Правое вращение

      -

      Как показано на рисунках ниже, под узлом указан его баланс-фактор. Если двигаться снизу вверх, то первым разбалансированным узлом в двоичном дереве будет "узел 3". Рассмотрим поддерево с этим узлом в качестве корня, обозначим данный узел как node , его левого дочернего узла как child и выполним "правое вращение". После завершения правого вращения поддерево снова станет сбалансированным и при этом сохранит свойство двоичного дерева поиска.

      +

      Как показано на рисунке 7-26, под узлом указан его баланс-фактор. Если двигаться снизу вверх, то первым разбалансированным узлом в двоичном дереве будет «узел 3». Рассмотрим поддерево с этим узлом в качестве корня, обозначим данный узел как node , его левого дочернего узла как child и выполним «правое вращение». После завершения правого вращения поддерево снова станет сбалансированным и при этом сохранит свойство двоичного дерева поиска.

      @@ -5251,7 +5251,7 @@

      Правое вращение при наличии grand_child

      Рисунок 7-27   Правое вращение при наличии grand_child

      -

      "Поворот вправо" - это лишь образное описание; в реальности он реализуется через изменение указателей узлов. Код приведен ниже:

      +

      «Поворот вправо» - это лишь образное описание. В реальности он реализуется через изменение указателей узлов. Код приведен ниже:

      @@ -5470,11 +5470,11 @@

      2.   Левое вращение

      -

      Соответственно, если рассмотреть "зеркальную" версию приведенного выше разбалансированного двоичного дерева, то понадобится выполнить "левое вращение", показанное на рисунке 7-28.

      +

      Соответственно, если рассмотреть «зеркальную» версию приведенного выше разбалансированного двоичного дерева, то понадобится выполнить «левое вращение», показанное на рисунке 7-28.

      Левое вращение

      Рисунок 7-28   Левое вращение

      -

      По той же причине, когда у узла child есть левый дочерний узел, который обозначим как grand_child , в левое вращение также требуется добавить шаг: сделать grand_child правым дочерним узлом node .

      +

      Аналогичная ситуация показана на рисунке 7-29. Если у узла child есть левый дочерний узел, который обозначим как grand_child , то в левое вращение также требуется добавить шаг: сделать grand_child правым дочерним узлом node .

      Левое вращение при наличии grand_child

      Рисунок 7-29   Левое вращение при наличии grand_child

      @@ -5697,21 +5697,21 @@

      3.   Сначала левое, затем правое вращение

      -

      Для разбалансированного узла 3 на рисунке 7-30 ни одно лишь левое вращение, ни одно лишь правое вращение не способны вернуть поддерево в баланс. В этом случае нужно сначала выполнить "левое вращение" для child , а затем выполнить "правое вращение" для node .

      +

      Для разбалансированного узла 3 на рисунке 7-30 ни одно лишь левое вращение, ни одно лишь правое вращение не способны вернуть поддерево в баланс. В этом случае нужно сначала выполнить «левое вращение» для child , а затем выполнить «правое вращение» для node .

      Сначала левое, затем правое вращение

      Рисунок 7-30   Сначала левое, затем правое вращение

      4.   Сначала правое, затем левое вращение

      -

      Как показано на рисунке 7-31, для зеркальной ситуации предыдущего разбалансированного двоичного дерева нужно сначала выполнить "правое вращение" для child , а затем "левое вращение" для node .

      +

      Как показано на рисунке 7-31, для зеркальной ситуации предыдущего разбалансированного двоичного дерева нужно сначала выполнить «правое вращение» для child , а затем «левое вращение» для node .

      Сначала правое, затем левое вращение

      Рисунок 7-31   Сначала правое, затем левое вращение

      5.   Выбор вращения

      -

      Четыре вида разбаланса, показанные на рисунке 7-32, по одному соответствуют рассмотренным выше случаям; для них соответственно требуются правое вращение, сначала левое затем правое, сначала правое затем левое и левое вращение.

      +

      Четыре вида разбаланса, показанные на рисунке 7-32, по одному соответствуют рассмотренным выше случаям. Для них соответственно требуются правое вращение, сначала левое затем правое, сначала правое затем левое и левое вращение.

      Четыре случая вращений AVL-дерева

      Рисунок 7-32   Четыре случая вращений AVL-дерева

      -

      Как показано в таблице 7-3, мы определяем, какому из этих четырех случаев соответствует разбалансированный узел, по знаку баланс-фактора самого разбалансированного узла и по знаку баланс-фактора дочернего узла на более высокой стороне.

      +

      Как показано в таблице 7-3, мы определяем, какому из случаев на рисунке 7-32 соответствует разбалансированный узел, по знаку баланс-фактора самого разбалансированного узла и по знаку баланс-фактора дочернего узла на более высокой стороне.

      Таблица 7-3   Условия выбора для четырех случаев вращений

      diff --git a/ru/chapter_tree/binary_search_tree/index.html b/ru/chapter_tree/binary_search_tree/index.html index 196abc86f..cdbf7161f 100644 --- a/ru/chapter_tree/binary_search_tree/index.html +++ b/ru/chapter_tree/binary_search_tree/index.html @@ -4490,7 +4490,7 @@

      7.4.1   Операции с двоичным деревом поиска

      Мы инкапсулируем двоичное дерево поиска в класс BinarySearchTree и объявляем переменную-член root , которая указывает на корневой узел дерева.

      1.   Поиск узла

      -

      Для заданного целевого значения узла num можно выполнить поиск, опираясь на свойства двоичного дерева поиска. Как показано на рисунках ниже, мы объявляем узел cur , стартуем от корня дерева root и циклически сравниваем значения cur.val и num .

      +

      Для заданного целевого значения узла num можно выполнить поиск, опираясь на свойства двоичного дерева поиска. Как показано на рисунке 7-17, мы объявляем узел cur , стартуем от корня дерева root и циклически сравниваем значения cur.val и num .

      • Если cur.val < num , это означает, что целевой узел находится в правом поддереве cur , поэтому выполняем cur = cur.right .
      • Если cur.val > num , это означает, что целевой узел находится в левом поддереве cur , поэтому выполняем cur = cur.left .
      • @@ -4796,7 +4796,7 @@

        2.   Вставка узла

        -

        Пусть дан элемент num , который нужно вставить. Чтобы сохранить свойство двоичного дерева поиска "левое поддерево < корень < правое поддерево", процесс вставки выглядит следующим образом.

        +

        Пусть дан элемент num , который нужно вставить. Чтобы сохранить свойство двоичного дерева поиска «левое поддерево < корень < правое поддерево», процесс вставки показан на рисунке 7-18.

        1. Найти позицию для вставки: как и в операции поиска, начиная от корня, мы циклически спускаемся вниз в зависимости от соотношения между текущим значением узла и num , пока не выйдем за листовой узел (то есть не дойдем до None ).
        2. Вставить узел в найденную позицию: инициализировать узел num и поставить его на место этого None .
        3. @@ -5237,7 +5237,7 @@

          Как и поиск узла, вставка узла требует \(O(\log n)\) времени.

          3.   Удаление узла

          -

          Сначала нужно найти в двоичном дереве целевой узел, а затем удалить его. Как и при вставке, после удаления необходимо сохранить свойство двоичного дерева поиска: "левое поддерево < корень < правое поддерево". Поэтому в зависимости от числа дочерних узлов у удаляемого узла, то есть для случаев со степенью 0, 1 и 2, выполняются разные операции удаления.

          +

          Сначала нужно найти в двоичном дереве целевой узел, а затем удалить его. Как и при вставке, после удаления необходимо сохранить свойство двоичного дерева поиска: «левое поддерево < корень < правое поддерево». Поэтому в зависимости от числа дочерних узлов у удаляемого узла, то есть для случаев со степенью 0, 1 и 2, выполняются разные операции удаления.

          Как показано на рисунке 7-19, когда степень удаляемого узла равна \(0\) , это значит, что узел является листом и может быть удален напрямую.

          Удаление узла в двоичном дереве поиска (степень 0)

          Рисунок 7-19   Удаление узла в двоичном дереве поиска (степень 0)

          @@ -5246,10 +5246,10 @@

          Удаление узла в двоичном дереве поиска (степень 1)

          Рисунок 7-20   Удаление узла в двоичном дереве поиска (степень 1)

          -

          Когда степень удаляемого узла равна \(2\) , мы уже не можем удалить его напрямую и должны использовать для замены другой узел. Чтобы сохранить свойство двоичного дерева поиска "левое поддерево \(<\) корень \(<\) правое поддерево", этим узлом может быть минимальный узел правого поддерева или максимальный узел левого поддерева.

          -

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

          +

          Когда степень удаляемого узла равна \(2\) , мы уже не можем удалить его напрямую и должны использовать для замены другой узел. Чтобы сохранить свойство двоичного дерева поиска «левое поддерево \(<\) корень \(<\) правое поддерево», этим узлом может быть минимальный узел правого поддерева или максимальный узел левого поддерева.

          +

          Предположим, мы выбираем минимальный узел правого поддерева, то есть следующий узел в симметричном обходе. Тогда процесс удаления показан на рисунке 7-21.

            -
          1. Найти следующий узел в "последовательности симметричного обхода" для удаляемого узла и обозначить его как tmp .
          2. +
          3. Найти следующий узел в «последовательности симметричного обхода» для удаляемого узла и обозначить его как tmp .
          4. Значением tmp перезаписать значение удаляемого узла, а затем рекурсивно удалить узел tmp из дерева.
          @@ -5995,14 +5995,14 @@

          4.   Упорядоченность симметричного обхода

          -

          Как показано на рисунке 7-22, симметричный обход двоичного дерева следует порядку "лево \(\rightarrow\) корень \(\rightarrow\) право", а двоичное дерево поиска удовлетворяет соотношению "левый дочерний узел \(<\) корень \(<\) правый дочерний узел".

          +

          Как показано на рисунке 7-22, симметричный обход двоичного дерева следует порядку «лево \(\rightarrow\) корень \(\rightarrow\) право», а двоичное дерево поиска удовлетворяет соотношению «левый дочерний узел \(<\) корень \(<\) правый дочерний узел».

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

          Используя это свойство возрастающей последовательности симметричного обхода, мы можем получить отсортированные данные из двоичного дерева поиска всего за \(O(n)\) времени, без дополнительной сортировки, что очень эффективно.

          Последовательность симметричного обхода двоичного дерева поиска

          Рисунок 7-22   Последовательность симметричного обхода двоичного дерева поиска

          7.4.2   Эффективность двоичного дерева поиска

          -

          Для заданного набора данных можно рассмотреть хранение либо в массиве, либо в двоичном дереве поиска. Из таблицы ниже видно, что временная сложность операций двоичного дерева поиска имеет логарифмический порядок и обеспечивает стабильную высокую производительность. Только в сценариях с очень частыми вставками и редкими поисками и удалениями массив может быть эффективнее, чем двоичное дерево поиска.

          +

          Для заданного набора данных можно рассмотреть хранение либо в массиве, либо в двоичном дереве поиска. Как видно по данным в таблице 7-2, временная сложность операций двоичного дерева поиска имеет логарифмический порядок и обеспечивает стабильную высокую производительность. Только в сценариях с очень частыми вставками и редкими поисками и удалениями массив может быть эффективнее, чем двоичное дерево поиска.

          Таблица 7-2   Сравнение эффективности массива и дерева поиска

          @@ -6033,7 +6033,7 @@
          -

          В идеальном случае двоичное дерево поиска является "сбалансированным", и тогда любой узел можно найти за \(\log n\) итераций.

          +

          В идеальном случае двоичное дерево поиска является «сбалансированным», и тогда любой узел можно найти за \(\log n\) итераций.

          Однако если в двоичное дерево поиска непрерывно вставлять и удалять узлы, оно может выродиться в связный список, как показано на рисунке 7-23. Тогда временная сложность различных операций тоже вырождается до \(O(n)\) .

          Деградация двоичного дерева поиска

          Рисунок 7-23   Деградация двоичного дерева поиска

          diff --git a/ru/chapter_tree/binary_tree/index.html b/ru/chapter_tree/binary_tree/index.html index db33ee37b..3ff6ffcd1 100644 --- a/ru/chapter_tree/binary_tree/index.html +++ b/ru/chapter_tree/binary_tree/index.html @@ -4557,7 +4557,7 @@

          7.1   Двоичное дерево

          -

          Двоичное дерево (binary tree) - это нелинейная структура данных, представляющая отношения между "предками" и "потомками" и отражающая логику "разделяй и властвуй". Подобно связному списку, базовой единицей двоичного дерева является узел; каждый узел содержит значение, ссылку на левого дочернего узла и ссылку на правого дочернего узла.

          +

          Двоичное дерево (binary tree) - это нелинейная структура данных, представляющая отношения между «предками» и «потомками» и отражающая логику «разделяй и властвуй». Подобно связному списку, базовой единицей двоичного дерева является узел. Каждый узел содержит значение, ссылку на левого дочернего узла и ссылку на правого дочернего узла.

          @@ -4735,8 +4735,8 @@
          -

          Каждый узел имеет две ссылки (указателя), которые соответственно указывают на левого дочернего узла (left-child node) и правого дочернего узла (right-child node); данный узел называется родительским узлом (parent node) для этих двух дочерних узлов. Если задан некоторый узел двоичного дерева, то дерево, образованное его левым дочерним узлом и всеми узлами ниже него, называется левым поддеревом (left subtree) этого узла; аналогично определяется правое поддерево (right subtree).

          -

          Узлы, не имеющие дочерних узлов, называют листьями, а все остальные узлы содержат дочерние узлы и непустые поддеревья. Как показано на рисунке 7-1, если рассматривать "узел 2" как родительский, то его левым и правым дочерними узлами будут "узел 4" и "узел 5"; левое поддерево - это "узел 4 и дерево ниже него", а правое поддерево - это "узел 5 и дерево ниже него".

          +

          Каждый узел имеет две ссылки (указателя), которые соответственно указывают на левого дочернего узла (left-child node) и правого дочернего узла (right-child node). Данный узел называется родительским узлом (parent node) для этих двух дочерних узлов. Если задан некоторый узел двоичного дерева, то дерево, образованное его левым дочерним узлом и всеми узлами ниже него, называется левым поддеревом (left subtree) этого узла. Аналогично определяется правое поддерево (right subtree).

          +

          Узлы, не имеющие дочерних узлов, называют листьями, а все остальные узлы содержат дочерние узлы и непустые поддеревья. Как показано на рисунке 7-1, если рассматривать «узел 2» как родительский, то его левым и правым дочерними узлами будут «узел 4» и «узел 5». Левое поддерево - это «узел 4 и дерево ниже него», а правое поддерево - это «узел 5 и дерево ниже него».

          Родительский узел, дочерние узлы и поддеревья

          Рисунок 7-1   Родительский узел, дочерние узлы и поддеревья

          @@ -4744,9 +4744,9 @@

          Распространенные термины двоичного дерева показаны на рисунке 7-2.

          • Корневой узел (root node): узел, расположенный на верхнем уровне двоичного дерева и не имеющий родительского узла.
          • -
          • Листовой узел (leaf node): узел без дочерних узлов; оба его указателя направлены на None .
          • +
          • Листовой узел (leaf node): узел без дочерних узлов. Оба его указателя направлены на None .
          • Ребро (edge): отрезок, соединяющий два узла, то есть ссылка (указатель) между узлами.
          • -
          • Уровень (level) узла: увеличивается сверху вниз; уровень корневого узла равен 1 .
          • +
          • Уровень (level) узла: увеличивается сверху вниз. Уровень корневого узла равен 1 .
          • Степень (degree) узла: число дочерних узлов данного узла. В двоичном дереве возможны степени 0, 1, 2 .
          • Высота (height) двоичного дерева: число ребер от корневого узла до самого удаленного листового узла.
          • Глубина (depth) узла: число ребер от корневого узла до данного узла.
          • @@ -4757,7 +4757,7 @@

            Tip

            -

            Обычно под "высотой" и "глубиной" понимают "число пройденных ребер", но в некоторых задачах или учебниках их могут определять как "число пройденных узлов". В таком случае и высоту, и глубину нужно увеличить на 1 .

            +

            Обычно под «высотой» и «глубиной» понимают «число пройденных ребер», но в некоторых задачах или учебниках их могут определять как «число пройденных узлов». В таком случае и высоту, и глубину нужно увеличить на 1 .

            7.1.2   Базовые операции двоичного дерева

            1.   Инициализация двоичного дерева

            @@ -5110,7 +5110,7 @@

          7.1.3   Распространенные типы двоичных деревьев

          1.   Идеальное двоичное дерево

          -

          Как показано на рисунке 7-4, идеальное двоичное дерево (perfect binary tree) полностью заполнено на всех уровнях. В идеальном двоичном дереве степень листовых узлов равна \(0\) , а у всех остальных узлов степень равна \(2\) ; если высота дерева равна \(h\) , то общее число узлов равно \(2^{h+1} - 1\) , что образует стандартную экспоненциальную зависимость и отражает часто встречающееся в природе явление клеточного деления.

          +

          Как показано на рисунке 7-4, идеальное двоичное дерево (perfect binary tree) полностью заполнено на всех уровнях. В идеальном двоичном дереве степень листовых узлов равна \(0\) , а у всех остальных узлов степень равна \(2\). Если высота дерева равна \(h\) , то общее число узлов равно \(2^{h+1} - 1\) , что образует стандартную экспоненциальную зависимость и отражает часто встречающееся в природе явление клеточного деления.

          Tip

          В китайскоязычном сообществе идеальное двоичное дерево часто называют полностью заполненным двоичным деревом.

          @@ -5134,9 +5134,9 @@

          Рисунок 7-7   Сбалансированное двоичное дерево

          7.1.4   Вырождение двоичного дерева

          -

          На рисунке 7-8 показаны идеальная структура двоичного дерева и вырожденная структура. Когда каждый уровень двоичного дерева полностью заполнен узлами, мы получаем "идеальное двоичное дерево"; когда же все узлы смещаются к одной стороне, двоичное дерево вырождается в "связный список".

          +

          На рисунке 7-8 показаны идеальная структура двоичного дерева и вырожденная структура. Когда каждый уровень двоичного дерева полностью заполнен узлами, мы получаем «идеальное двоичное дерево». Когда же все узлы смещаются к одной стороне, двоичное дерево вырождается в «связный список».

            -
          • Идеальное двоичное дерево соответствует лучшему случаю и позволяет в полной мере раскрыть преимущества подхода "разделяй и властвуй".
          • +
          • Идеальное двоичное дерево соответствует лучшему случаю и позволяет в полной мере раскрыть преимущества подхода «разделяй и властвуй».
          • Связный список представляет противоположную крайность: все операции становятся линейными, а временная сложность деградирует до \(O(n)\) .

          Лучший и худший случаи структуры двоичного дерева

          diff --git a/ru/chapter_tree/binary_tree_traversal/index.html b/ru/chapter_tree/binary_tree_traversal/index.html index 97463aa04..809a56f10 100644 --- a/ru/chapter_tree/binary_tree_traversal/index.html +++ b/ru/chapter_tree/binary_tree_traversal/index.html @@ -4473,12 +4473,12 @@

          К распространенным способам обхода двоичного дерева относятся обход по уровням, прямой обход, симметричный обход и обратный обход.

          7.2.1   Обход по уровням

          Как показано на рисунке 7-9, обход по уровням (level-order traversal) проходит двоичное дерево сверху вниз по уровням и на каждом уровне посещает узлы слева направо.

          -

          По своей сути обход по уровням относится к обходу в ширину (breadth-first traversal), также называемому поиском в ширину (breadth-first search, BFS); он отражает идею "расширяться от центра к периферии слой за слоем".

          +

          По своей сути обход по уровням относится к обходу в ширину (breadth-first traversal), также называемому поиском в ширину (breadth-first search, BFS). Он отражает идею «расширяться от центра к периферии слой за слоем».

          Обход двоичного дерева по уровням

          Рисунок 7-9   Обход двоичного дерева по уровням

          1.   Код реализации

          -

          Обход в ширину обычно реализуется с помощью "очереди". Очередь подчиняется правилу "первым пришел - первым вышел", а обход в ширину подчиняется правилу "продвигаться по уровням", поэтому стоящая за ними идея согласована. Код реализации приведен ниже:

          +

          Обход в ширину обычно реализуется с помощью «очереди». Очередь подчиняется правилу «первым пришел - первым вышел», а обход в ширину подчиняется правилу «продвигаться по уровням», поэтому стоящая за ними идея согласована. Код реализации приведен ниже:

          @@ -4780,7 +4780,7 @@
        4. Пространственная сложность равна \(O(n)\) : в худшем случае, то есть для полной двоичной деревообразной структуры, до достижения самого нижнего уровня в очереди одновременно может находиться до \((n + 1) / 2\) узлов, что требует \(O(n)\) памяти.

      7.2.2   Прямой, симметричный и обратный обходы

      -

      Соответственно, прямой, симметричный и обратный обходы относятся к обходу в глубину (depth-first traversal), также называемому поиском в глубину (depth-first search, DFS); он отражает идею "сначала идти до конца, затем возвращаться и продолжать".

      +

      Соответственно, прямой, симметричный и обратный обходы относятся к обходу в глубину (depth-first traversal), также называемому поиском в глубину (depth-first search, DFS). Он отражает идею «сначала идти до конца, затем возвращаться и продолжать».

      На рисунке 7-10 показан принцип работы обхода двоичного дерева в глубину. Обход в глубину можно представить как обход всей двоичной структуры по внешнему контуру , и у каждого узла встречаются три позиции, соответствующие прямому, симметричному и обратному обходам.

      Прямой, симметричный и обратный обходы двоичного дерева поиска

      Рисунок 7-10   Прямой, симметричный и обратный обходы двоичного дерева поиска

      @@ -5233,12 +5233,12 @@

      Tip

      -

      Поиск в глубину можно реализовать и итеративно; заинтересованные читатели могут изучить это самостоятельно.

      +

      Поиск в глубину можно реализовать и итеративно. Заинтересованные читатели могут изучить это самостоятельно.

      -

      На рисунках ниже показан рекурсивный процесс прямого обхода двоичного дерева. Его можно разделить на две противоположные части: "вход в рекурсию" и "возврат".

      +

      На рисунке 7-11 показан рекурсивный процесс прямого обхода двоичного дерева. Его можно разделить на две противоположные части: «вход в рекурсию» и «возврат».

        -
      1. "Вход в рекурсию" означает запуск нового вызова функции; в этом процессе программа переходит к следующему узлу.
      2. -
      3. "Возврат" означает завершение вызова функции и возврат назад, то есть текущий узел уже полностью обработан.
      4. +
      5. «Вход в рекурсию» означает запуск нового вызова функции. В этом процессе программа переходит к следующему узлу.
      6. +
      7. «Возврат» означает завершение вызова функции и возврат назад, то есть текущий узел уже полностью обработан.
      diff --git a/ru/chapter_tree/index.html b/ru/chapter_tree/index.html index 4f208a4e8..a971de198 100644 --- a/ru/chapter_tree/index.html +++ b/ru/chapter_tree/index.html @@ -4280,7 +4280,7 @@

      Abstract

      Высокое дерево полно жизни: мощные корни, густая листва и раскидистые ветви.

      -

      Оно наглядно показывает нам живую форму данных, построенную на принципе "разделяй и властвуй".

      +

      Оно наглядно показывает нам живую форму данных, построенную на принципе «разделяй и властвуй».

      Содержание главы

        diff --git a/ru/chapter_tree/summary/index.html b/ru/chapter_tree/summary/index.html index 9f723c914..8d886d023 100644 --- a/ru/chapter_tree/summary/index.html +++ b/ru/chapter_tree/summary/index.html @@ -4359,25 +4359,25 @@

        7.6   Краткие итоги

        1.   Основные моменты

          -
        • Двоичное дерево - это нелинейная структура данных, отражающая логику "разделяй и властвуй". Каждый узел двоичного дерева содержит значение и два указателя, которые соответственно ведут к левому и правому дочерним узлам.
        • +
        • Двоичное дерево - это нелинейная структура данных, отражающая логику «разделяй и властвуй». Каждый узел двоичного дерева содержит значение и два указателя, которые соответственно ведут к левому и правому дочерним узлам.
        • Для любого узла двоичного дерева дерево, образованное его левым (правым) дочерним узлом и всеми нижележащими узлами, называется левым (правым) поддеревом этого узла.
        • К связанным с двоичным деревом терминам относятся корневой узел, листовой узел, уровень, степень, ребро, высота, глубина и так далее.
        • Инициализация двоичного дерева, вставка узлов и удаление узлов аналогичны операциям со связным списком.
        • К распространенным видам двоичного дерева относятся идеальное двоичное дерево, полное двоичное дерево, строгое двоичное дерево и сбалансированное двоичное дерево. Идеальное двоичное дерево - наиболее желательное состояние, а связный список - худший случай после вырождения.
        • Двоичное дерево можно представить массивом: значения узлов и пустые позиции располагаются в порядке обхода по уровням, а связи между родителем и детьми реализуются через индексацию.
        • -
        • Обход двоичного дерева по уровням является методом поиска в ширину; он отражает идею "расширяться от центра к периферии слой за слоем" и обычно реализуется через очередь.
        • -
        • Прямой, симметричный и обратный обходы относятся к поиску в глубину; они отражают идею "сначала дойти до конца, затем вернуться и продолжить" и обычно реализуются рекурсивно.
        • -
        • Двоичное дерево поиска - это эффективная структура данных для поиска элементов; его поиск, вставка и удаление имеют временную сложность \(O(\log n)\) . Когда двоичное дерево поиска вырождается в связный список, все эти сложности деградируют до \(O(n)\) .
        • +
        • Обход двоичного дерева по уровням является методом поиска в ширину. Он отражает идею «расширяться от центра к периферии слой за слоем» и обычно реализуется через очередь.
        • +
        • Прямой, симметричный и обратный обходы относятся к поиску в глубину. Они отражают идею «сначала дойти до конца, затем вернуться и продолжить» и обычно реализуются рекурсивно.
        • +
        • Двоичное дерево поиска - это эффективная структура данных для поиска элементов. Его поиск, вставка и удаление имеют временную сложность \(O(\log n)\) . Когда двоичное дерево поиска вырождается в связный список, все эти сложности деградируют до \(O(n)\) .
        • AVL-дерево, также называемое сбалансированным двоичным деревом поиска, с помощью вращений гарантирует, что после постоянных вставок и удалений узлов дерево остается сбалансированным.
        • Вращения AVL-дерева включают правое вращение, левое вращение, сначала правое затем левое и сначала левое затем правое. После вставки или удаления узла AVL-дерево выполняет вращения снизу вверх, чтобы снова восстановить баланс.

        2.   Q & A

        Q: Для двоичного дерева, состоящего из одного узла, высота дерева и глубина корня обе равны \(0\) ?

        -

        Да, потому что высота и глубина обычно определяются как "число пройденных ребер".

        -

        Q: Вставка и удаление в двоичном дереве обычно выполняются в составе набора операций. Что именно означает этот "набор операций"? Можно ли понимать это как освобождение ресурсов у дочерних узлов ресурса?

        +

        Да, потому что высота и глубина обычно определяются как «число пройденных ребер».

        +

        Q: Вставка и удаление в двоичном дереве обычно выполняются в составе набора операций. Что именно означает этот «набор операций»? Можно ли понимать это как освобождение ресурсов у дочерних узлов ресурса?

        Возьмем в качестве примера двоичное дерево поиска: операция удаления узла делится на три случая, и каждый из этих случаев требует нескольких последовательных шагов работы с узлами.

        Q: Почему у DFS для двоичного дерева есть три порядка: прямой, симметричный и обратный? Для чего они нужны?

        -

        Подобно прямому и обратному обходу массива, прямой, симметричный и обратный обходы - это три способа обхода двоичного дерева, с помощью которых можно получить результаты в определенном порядке. Например, в двоичном дереве поиска, где соблюдается отношение значение левого дочернего узла < значение корня < значение правого дочернего узла , если обходить дерево с приоритетом "лево \(\rightarrow\) корень \(\rightarrow\) право", то получится упорядоченная последовательность узлов.

        +

        Подобно прямому и обратному обходу массива, прямой, симметричный и обратный обходы - это три способа обхода двоичного дерева, с помощью которых можно получить результаты в определенном порядке. Например, в двоичном дереве поиска, где соблюдается отношение значение левого дочернего узла < значение корня < значение правого дочернего узла , если обходить дерево с приоритетом «лево \(\rightarrow\) корень \(\rightarrow\) право», то получится упорядоченная последовательность узлов.

        Q: Правое вращение работает с отношениями между node , child и grand_child . А связь между node и его исходным родителем разве не нужно поддерживать? После правого вращения она ведь не оборвется?

        На это нужно смотреть с точки зрения рекурсии. В правое вращение right_rotate(root) передается корень поддерева, а затем через return child возвращается корень этого поддерева уже после вращения. Соединение между новым корнем поддерева и его родителем восстанавливается после возврата функции и не входит в обязанности самой операции правого вращения.

        Q: В C++ функции делятся на private и public . Какая логика стоит за этим? Почему height() и updateHeight() помещают в разные области видимости?

        diff --git a/ru/javascripts/animation_player.js b/ru/javascripts/animation_player.js index 17fd25a90..33d3e5f59 100644 --- a/ru/javascripts/animation_player.js +++ b/ru/javascripts/animation_player.js @@ -251,4 +251,4 @@ initAutoSlide(); } })(); -/*! update cache: 20260410225948 */ +/*! update cache: 20260414173637 */ diff --git a/ru/javascripts/katex.js b/ru/javascripts/katex.js index a6aaaed8e..c3b173ba1 100644 --- a/ru/javascripts/katex.js +++ b/ru/javascripts/katex.js @@ -8,4 +8,4 @@ document$.subscribe(({ body }) => { ], }); }); -/*! update cache: 20260410225948 */ +/*! update cache: 20260414173637 */ diff --git a/ru/javascripts/mathjax.js b/ru/javascripts/mathjax.js index 0a435ee78..98111f29c 100644 --- a/ru/javascripts/mathjax.js +++ b/ru/javascripts/mathjax.js @@ -15,4 +15,4 @@ window.MathJax = { document$.subscribe(() => { MathJax.typesetPromise(); }); -/*! update cache: 20260410225948 */ +/*! update cache: 20260414173637 */ diff --git a/ru/javascripts/starfield.js b/ru/javascripts/starfield.js index 4718eb5b8..9a2d2b7a2 100644 --- a/ru/javascripts/starfield.js +++ b/ru/javascripts/starfield.js @@ -469,4 +469,4 @@ return Starfield; }); -/*! update cache: 20260410225948 */ +/*! update cache: 20260414173637 */ diff --git a/ru/search.json b/ru/search.json index ff1484614..1e91e0d97 100644 --- a/ru/search.json +++ b/ru/search.json @@ -1 +1 @@ -{"config":{"separator":"[\\s\\-_,:!=\\[\\]()\\\\\"`/]+|\\.(?!\\d)"},"items":[{"location":"chapter_appendix/","level":1,"title":"Глава 16.   Приложение","text":"","path":["Глава 16. Приложение","Глава 16.   Приложение"],"tags":[]},{"location":"chapter_appendix/#_1","level":2,"title":"Содержание главы","text":"
        • 16.1   Установка среды программирования
        • 16.2   Присоединяйтесь к созданию книги
        • 16.3   Глоссарий
        ","path":["Глава 16. Приложение","Глава 16.   Приложение"],"tags":[]},{"location":"chapter_appendix/contribution/","level":1,"title":"16.2   Присоединяйтесь к созданию книги","text":"

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

        Все GitHub ID авторов будут указаны на главных страницах репозитория книги, веб-версии и PDF-версии в знак благодарности за их бескорыстный вклад в сообщество открытого исходного кода.

        Сила открытого исходного кода

        Интервал между двумя тиражами бумажной книги обычно довольно велик, поэтому обновлять содержание очень неудобно.

        В этой же открытой книге цикл обновления содержания сокращается до нескольких дней, а иногда даже до нескольких часов.

        ","path":["Глава 16. Приложение","16.2   Присоединяйтесь к созданию книги"],"tags":[]},{"location":"chapter_appendix/contribution/#1","level":3,"title":"1.   Небольшие правки содержания","text":"

        Как показано на рисунке 16-3, в правом верхнем углу каждой страницы есть \"значок редактирования\". Текст или код можно изменить следующим образом.

        1. Нажмите на \"значок редактирования\". Если появится сообщение \"You need to fork this repository\", согласитесь с этим действием.
        2. Измените содержимое исходного Markdown-файла, проверьте корректность правок и постарайтесь сохранить единый стиль оформления.
        3. Внизу страницы заполните описание изменений, затем нажмите кнопку \"Propose file change\". После перехода на следующую страницу нажмите кнопку \"Create pull request\", чтобы отправить pull request.

        Рисунок 16-3   Кнопка редактирования страницы

        Изображения нельзя изменить напрямую, поэтому проблему с ними нужно описывать через новый Issue или комментарий. Мы постараемся как можно быстрее исправить и обновить изображение.

        ","path":["Глава 16. Приложение","16.2   Присоединяйтесь к созданию книги"],"tags":[]},{"location":"chapter_appendix/contribution/#2","level":3,"title":"2.   Создание содержания","text":"

        Если вам интересно участвовать в этом проекте с открытым исходным кодом, например переводить код на другие языки программирования или расширять содержание статей, то следует придерживаться следующего процесса Pull Request.

        1. Войдите в GitHub и сделайте Fork репозитория книги в свой личный аккаунт.
        2. Перейдите на страницу своего Fork-репозитория и с помощью команды git clone клонируйте репозиторий локально.
        3. Создавайте и редактируйте содержание локально, затем проведите полное тестирование и проверьте корректность кода.
        4. Зафиксируйте локальные изменения, после чего выполните Push в удаленный репозиторий.
        5. Обновите страницу репозитория и нажмите кнопку \"Create pull request\", чтобы инициировать pull request.
        ","path":["Глава 16. Приложение","16.2   Присоединяйтесь к созданию книги"],"tags":[]},{"location":"chapter_appendix/contribution/#3-docker","level":3,"title":"3.   Развертывание Docker","text":"

        В корневом каталоге hello-algo выполните следующий Docker-скрипт, после чего проект станет доступен по адресу http://localhost:8000:

        docker-compose up -d\n

        Удалить развертывание можно следующей командой:

        docker-compose down\n
        ","path":["Глава 16. Приложение","16.2   Присоединяйтесь к созданию книги"],"tags":[]},{"location":"chapter_appendix/installation/","level":1,"title":"16.1   Установка среды программирования","text":"","path":["Глава 16. Приложение","16.1   Установка среды программирования"],"tags":[]},{"location":"chapter_appendix/installation/#1611-ide","level":2,"title":"16.1.1   Установка IDE","text":"

        В качестве локальной интегрированной среды разработки (IDE) рекомендуется использовать открытую и быструю VS Code. Перейдите на официальный сайт VS Code, выберите версию для своей операционной системы и установите ее.

        Рисунок 16-1   Загрузка VS Code с официального сайта

        VS Code обладает мощной экосистемой расширений и поддерживает выполнение и отладку большинства языков программирования. Например, после установки расширения \"Python Extension Pack\" можно отлаживать код на Python. Процесс установки показан на рисунке 16-2.

        Рисунок 16-2   Установка расширений VS Code

        ","path":["Глава 16. Приложение","16.1   Установка среды программирования"],"tags":[]},{"location":"chapter_appendix/installation/#1612","level":2,"title":"16.1.2   Установка языковой среды","text":"","path":["Глава 16. Приложение","16.1   Установка среды программирования"],"tags":[]},{"location":"chapter_appendix/installation/#1-python","level":3,"title":"1.   Среда Python","text":"
        1. Загрузите и установите Miniconda3, требуется Python 3.10 или более поздняя версия.
        2. В магазине расширений VS Code найдите python и установите Python Extension Pack.
        3. (Необязательно) Введите в командной строке pip install black, чтобы установить инструмент форматирования кода.
        ","path":["Глава 16. Приложение","16.1   Установка среды программирования"],"tags":[]},{"location":"chapter_appendix/installation/#2-cc","level":3,"title":"2.   Среда C/C++","text":"
        1. В Windows требуется установить MinGW (руководство по настройке); в macOS компилятор Clang уже установлен по умолчанию.
        2. В магазине расширений VS Code найдите c++ и установите C/C++ Extension Pack.
        3. (Необязательно) Откройте страницу Settings, найдите параметр форматирования Clang_format_fallback Style и задайте значение { BasedOnStyle: Microsoft, BreakBeforeBraces: Attach }.
        ","path":["Глава 16. Приложение","16.1   Установка среды программирования"],"tags":[]},{"location":"chapter_appendix/installation/#3-java","level":3,"title":"3.   Среда Java","text":"
        1. Загрузите и установите OpenJDK (требуемая версия: > JDK 9).
        2. В магазине расширений VS Code найдите java и установите Extension Pack for Java.
        ","path":["Глава 16. Приложение","16.1   Установка среды программирования"],"tags":[]},{"location":"chapter_appendix/installation/#4-c","level":3,"title":"4.   Среда C","text":"
        1. Загрузите и установите .Net 8.0.
        2. В магазине расширений VS Code найдите C# Dev Kit и установите C# Dev Kit (руководство по настройке).
        3. Также можно использовать Visual Studio (руководство по установке).
        ","path":["Глава 16. Приложение","16.1   Установка среды программирования"],"tags":[]},{"location":"chapter_appendix/installation/#5-go","level":3,"title":"5.   Среда Go","text":"
        1. Загрузите и установите go.
        2. В магазине расширений VS Code найдите go и установите Go.
        3. Нажмите Ctrl + Shift + P, чтобы открыть командную палитру, введите go, выберите Go: Install/Update Tools, отметьте все инструменты и установите их.
        ","path":["Глава 16. Приложение","16.1   Установка среды программирования"],"tags":[]},{"location":"chapter_appendix/installation/#6-swift","level":3,"title":"6.   Среда Swift","text":"
        1. Загрузите и установите Swift.
        2. В магазине расширений VS Code найдите swift и установите Swift for Visual Studio Code.
        ","path":["Глава 16. Приложение","16.1   Установка среды программирования"],"tags":[]},{"location":"chapter_appendix/installation/#7-javascript","level":3,"title":"7.   Среда JavaScript","text":"
        1. Загрузите и установите Node.js.
        2. (Необязательно) В магазине расширений VS Code найдите Prettier и установите инструмент форматирования кода.
        ","path":["Глава 16. Приложение","16.1   Установка среды программирования"],"tags":[]},{"location":"chapter_appendix/installation/#8-typescript","level":3,"title":"8.   Среда TypeScript","text":"
        1. Выполните те же шаги, что и для среды JavaScript.
        2. Установите TypeScript Execute (tsx).
        3. В магазине расширений VS Code найдите typescript и установите Pretty TypeScript Errors.
        ","path":["Глава 16. Приложение","16.1   Установка среды программирования"],"tags":[]},{"location":"chapter_appendix/installation/#9-dart","level":3,"title":"9.   Среда Dart","text":"
        1. Загрузите и установите Dart.
        2. В магазине расширений VS Code найдите dart и установите Dart.
        ","path":["Глава 16. Приложение","16.1   Установка среды программирования"],"tags":[]},{"location":"chapter_appendix/installation/#10-rust","level":3,"title":"10.   Среда Rust","text":"
        1. Загрузите и установите Rust.
        2. В магазине расширений VS Code найдите rust и установите rust-analyzer.
        ","path":["Глава 16. Приложение","16.1   Установка среды программирования"],"tags":[]},{"location":"chapter_appendix/terminology/","level":1,"title":"16.3   Глоссарий","text":"

        В таблице 16-1 приведены важные термины, встречающиеся в книге. Рекомендуем запомнить английские названия терминов, чтобы легче читать англоязычные материалы.

        Таблица 16-1   Важные термины по структурам данных и алгоритмам

        English Русский algorithm алгоритм data structure структура данных code код file файл function функция method метод variable переменная asymptotic complexity analysis асимптотический анализ сложности time complexity временная сложность space complexity пространственная сложность loop цикл iteration итерация recursion рекурсия tail recursion хвостовая рекурсия recursion tree дерево рекурсии big-\\(O\\) notation нотация big-\\(O\\) asymptotic upper bound асимптотическая верхняя граница sign-magnitude прямой код 1’s complement обратный код 2’s complement дополнительный код array массив index индекс linked list связный список linked list node, list node узел связного списка head node головной узел tail node хвостовой узел list список dynamic array динамический массив hard disk жесткий диск random-access memory (RAM) оперативная память cache memory кеш-память cache miss промах кеша cache hit rate коэффициент попадания в кеш stack стек top of the stack вершина стека bottom of the stack основание стека queue очередь double-ended queue двусторонняя очередь front of the queue голова очереди rear of the queue хвост очереди hash table хеш-таблица hash set хеш-набор bucket бакет hash function хеш-функция hash collision хеш-коллизия load factor коэффициент заполнения separate chaining цепная адресация open addressing открытая адресация linear probing линейное зондирование lazy deletion ленивое удаление binary tree двоичное дерево tree node узел дерева left-child node левый дочерний узел right-child node правый дочерний узел parent node родительский узел left subtree левое поддерево right subtree правое поддерево root node корневой узел leaf node листовой узел edge ребро level уровень degree степень height высота depth глубина perfect binary tree идеальное двоичное дерево complete binary tree полное двоичное дерево full binary tree строгое двоичное дерево balanced binary tree сбалансированное двоичное дерево binary search tree двоичное дерево поиска AVL tree АВЛ-дерево red-black tree красно-черное дерево level-order traversal обход по уровням breadth-first traversal обход в ширину depth-first traversal обход в глубину pre-order traversal прямой обход in-order traversal симметричный обход post-order traversal обратный обход balanced binary search tree сбалансированное двоичное дерево поиска balance factor фактор баланса heap куча max heap максимальная куча min heap минимальная куча priority queue приоритетная очередь heapify упорядочивание кучи top-\\(k\\) problem поиск \\(k\\) наибольших элементов graph граф vertex вершина undirected graph неориентированный граф directed graph ориентированный граф connected graph связный граф disconnected graph несвязный граф weighted graph взвешенный граф adjacency смежность path путь in-degree входящая степень out-degree исходящая степень adjacency matrix матрица смежности adjacency list список смежности breadth-first search поиск в ширину depth-first search поиск в глубину binary search двоичный поиск searching algorithm алгоритм поиска sorting algorithm алгоритм сортировки selection sort сортировка выбором bubble sort сортировка пузырьком insertion sort сортировка вставкой quick sort быстрая сортировка merge sort сортировка слиянием heap sort пирамидальная сортировка bucket sort блочная сортировка counting sort сортировка подсчетом radix sort поразрядная сортировка divide and conquer разделяй и властвуй hanota problem задача о Ханойской башне backtracking algorithm алгоритм поиска с возвратом constraint ограничение solution решение state состояние pruning отсечение permutations problem задача о перестановках subset-sum problem задача о сумме подмножеств \\(n\\)-queens problem задача о \\(n\\) ферзях dynamic programming динамическое программирование initial state начальное состояние state-transition equation уравнение перехода состояния knapsack problem задача о рюкзаке edit distance problem задача о расстоянии редактирования greedy algorithm жадный алгоритм","path":["Глава 16. Приложение","16.3   Глоссарий"],"tags":[]},{"location":"chapter_array_and_linkedlist/","level":1,"title":"Глава 4.   Массивы и списки","text":"

        Abstract

        Мир структур данных напоминает прочную кирпичную стену.

        Кирпичи массива уложены ровно и плотно прилегают друг к другу. Узлы связного списка, напротив, разбросаны в разных местах, а соединяющие их связи свободно тянутся между промежутками.

        ","path":["Глава 4. Массивы и списки","Глава 4.   Массивы и списки"],"tags":[]},{"location":"chapter_array_and_linkedlist/#_1","level":2,"title":"Содержание главы","text":"
        • 4.1   Массив
        • 4.2   Связный список
        • 4.3   Список
        • 4.4   Оперативная память и кэш *
        • 4.5   Резюме
        ","path":["Глава 4. Массивы и списки","Глава 4.   Массивы и списки"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/","level":1,"title":"4.1   Массив","text":"

        Массив (array) - это линейная структура данных, в которой элементы одного типа хранятся в непрерывной области памяти. Положение элемента в массиве называется его индексом (index). На рисунке 4-1 показаны основные понятия, связанные с массивом, и способ его хранения.

        Рисунок 4-1   Определение массива и способ хранения

        ","path":["Глава 4. Массивы и списки","4.1   Массив"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#411","level":2,"title":"4.1.1   Основные операции с массивом","text":"","path":["Глава 4. Массивы и списки","4.1   Массив"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#1","level":3,"title":"1.   Инициализация массива","text":"

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

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
        # Инициализация массива\narr: list[int] = [0] * 5  # [ 0, 0, 0, 0, 0 ]\nnums: list[int] = [1, 3, 2, 5, 4]\n
        array.cpp
        /* Инициализация массива */\n// Хранится в стеке\nint arr[5];\nint nums[5] = { 1, 3, 2, 5, 4 };\n// Хранится в куче (требуется ручное освобождение памяти)\nint* arr1 = new int[5];\nint* nums1 = new int[5] { 1, 3, 2, 5, 4 };\n
        array.java
        /* Инициализация массива */\nint[] arr = new int[5]; // { 0, 0, 0, 0, 0 }\nint[] nums = { 1, 3, 2, 5, 4 };\n
        array.cs
        /* Инициализация массива */\nint[] arr = new int[5]; // [ 0, 0, 0, 0, 0 ]\nint[] nums = [1, 3, 2, 5, 4];\n
        array.go
        /* Инициализация массива */\nvar arr [5]int\n// В Go указание длины ([5]int) создает массив, а отсутствие длины ([]int) - срез\n// Поскольку длина массива в Go определяется на этапе компиляции, для задания длины можно использовать только константы\n// Чтобы упростить реализацию метода extend(), ниже будем рассматривать срезы (Slice) как массивы (Array)\nnums := []int{1, 3, 2, 5, 4}\n
        array.swift
        /* Инициализация массива */\nlet arr = Array(repeating: 0, count: 5) // [0, 0, 0, 0, 0]\nlet nums = [1, 3, 2, 5, 4]\n
        array.js
        /* Инициализация массива */\nvar arr = new Array(5).fill(0);\nvar nums = [1, 3, 2, 5, 4];\n
        array.ts
        /* Инициализация массива */\nlet arr: number[] = new Array(5).fill(0);\nlet nums: number[] = [1, 3, 2, 5, 4];\n
        array.dart
        /* Инициализация массива */\nList<int> arr = List.filled(5, 0); // [0, 0, 0, 0, 0]\nList<int> nums = [1, 3, 2, 5, 4];\n
        array.rs
        /* Инициализация массива */\nlet arr: [i32; 5] = [0; 5]; // [0, 0, 0, 0, 0]\nlet slice: &[i32] = &[0; 5];\n// В Rust указание длины ([i32; 5]) создает массив, а отсутствие длины (&[i32]) - срез\n// Поскольку длина массива в Rust определяется на этапе компиляции, для задания длины можно использовать только константы\n// Vector в Rust обычно используется как динамический массив\n// Чтобы упростить реализацию метода extend(), ниже будем рассматривать vector как массив (array)\nlet nums: Vec<i32> = vec![1, 3, 2, 5, 4];\n
        array.c
        /* Инициализация массива */\nint arr[5] = { 0 }; // { 0, 0, 0, 0, 0 }\nint nums[5] = { 1, 3, 2, 5, 4 };\n
        array.kt
        /* Инициализация массива */\nvar arr = IntArray(5) // { 0, 0, 0, 0, 0 }\nvar nums = intArrayOf(1, 3, 2, 5, 4)\n
        array.rb
        # Инициализация массива\narr = Array.new(5, 0)\nnums = [1, 3, 2, 5, 4]\n
        Визуализация выполнения

        https://pythontutor.com/render.html#code=%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D0%BC%D0%B0%D1%81%D1%81%D0%B8%D0%B2%0Aarr%20%3D%20%5B0%5D%20%2A%205%20%20%23%20%5B%200%2C%200%2C%200%2C%200%2C%200%20%5D%0Anums%20%3D%20%5B1%2C%203%2C%202%2C%205%2C%204%5D&cumulative=false&curInstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

        ","path":["Глава 4. Массивы и списки","4.1   Массив"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#2","level":3,"title":"2.   Доступ к элементам","text":"

        Элементы массива хранятся в непрерывной области памяти, что упрощает вычисление их адресов. Зная адрес массива в памяти (то есть адрес первого элемента) и индекс некоторого элемента, мы можем по формуле с рисунка ниже вычислить адрес этого элемента и напрямую обратиться к нему.

        Рисунок 4-2   Вычисление адреса элемента массива

        Если посмотреть на рисунок 4-2, можно заметить, что индекс первого элемента массива равен \\(0\\) , и это кажется не слишком интуитивным, ведь естественнее было бы начинать счет с \\(1\\) . Однако с точки зрения формулы адресации индекс по сути является смещением относительно адреса памяти. Смещение первого элемента равно \\(0\\) , поэтому индекс \\(0\\) полностью логичен.

        Доступ к элементам массива очень эффективен: любой элемент массива можно получить за \\(O(1)\\) времени.

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
        def random_access(nums: list[int]) -> int:\n    \"\"\"Случайный доступ к элементу\"\"\"\n    # Случайным образом выбрать число из интервала [0, len(nums)-1]\n    random_index = random.randint(0, len(nums) - 1)\n    # Получить и вернуть случайный элемент\n    random_num = nums[random_index]\n    return random_num\n
        array.cpp
        /* Случайный доступ к элементу */\nint randomAccess(int *nums, int size) {\n    // Случайным образом выбрать число из интервала [0, size)\n    int randomIndex = rand() % size;\n    // Получить и вернуть случайный элемент\n    int randomNum = nums[randomIndex];\n    return randomNum;\n}\n
        array.java
        /* Случайный доступ к элементу */\nint randomAccess(int[] nums) {\n    // Случайным образом выбрать число из интервала [0, nums.length)\n    int randomIndex = ThreadLocalRandom.current().nextInt(0, nums.length);\n    // Получить и вернуть случайный элемент\n    int randomNum = nums[randomIndex];\n    return randomNum;\n}\n
        array.cs
        /* Случайный доступ к элементу */\nint RandomAccess(int[] nums) {\n    Random random = new();\n    // Случайным образом выбрать число из интервала [0, nums.Length)\n    int randomIndex = random.Next(nums.Length);\n    // Получить и вернуть случайный элемент\n    int randomNum = nums[randomIndex];\n    return randomNum;\n}\n
        array.go
        /* Случайный доступ к элементу */\nfunc randomAccess(nums []int) (randomNum int) {\n    // Случайным образом выбрать число из интервала [0, nums.length)\n    randomIndex := rand.Intn(len(nums))\n    // Получить и вернуть случайный элемент\n    randomNum = nums[randomIndex]\n    return\n}\n
        array.swift
        /* Случайный доступ к элементу */\nfunc randomAccess(nums: [Int]) -> Int {\n    // Случайным образом выбрать число из интервала [0, nums.count)\n    let randomIndex = nums.indices.randomElement()!\n    // Получить и вернуть случайный элемент\n    let randomNum = nums[randomIndex]\n    return randomNum\n}\n
        array.js
        /* Случайный доступ к элементу */\nfunction randomAccess(nums) {\n    // Случайным образом выбрать число из интервала [0, nums.length)\n    const random_index = Math.floor(Math.random() * nums.length);\n    // Получить и вернуть случайный элемент\n    const random_num = nums[random_index];\n    return random_num;\n}\n
        array.ts
        /* Случайный доступ к элементу */\nfunction randomAccess(nums: number[]): number {\n    // Случайным образом выбрать число из интервала [0, nums.length)\n    const random_index = Math.floor(Math.random() * nums.length);\n    // Получить и вернуть случайный элемент\n    const random_num = nums[random_index];\n    return random_num;\n}\n
        array.dart
        /* Случайный доступ к элементу */\nint randomAccess(List<int> nums) {\n  // Случайным образом выбрать число из интервала [0, nums.length)\n  int randomIndex = Random().nextInt(nums.length);\n  // Получить и вернуть случайный элемент\n  int randomNum = nums[randomIndex];\n  return randomNum;\n}\n
        array.rs
        /* Случайный доступ к элементу */\nfn random_access(nums: &[i32]) -> i32 {\n    // Случайным образом выбрать число из интервала [0, nums.len())\n    let random_index = rand::thread_rng().gen_range(0..nums.len());\n    // Получить и вернуть случайный элемент\n    let random_num = nums[random_index];\n    random_num\n}\n
        array.c
        /* Случайный доступ к элементу */\nint randomAccess(int *nums, int size) {\n    // Случайным образом выбрать число из интервала [0, size)\n    int randomIndex = rand() % size;\n    // Получить и вернуть случайный элемент\n    int randomNum = nums[randomIndex];\n    return randomNum;\n}\n
        array.kt
        /* Случайный доступ к элементу */\nfun randomAccess(nums: IntArray): Int {\n    // Случайным образом выбрать число из интервала [0, nums.size)\n    val randomIndex = ThreadLocalRandom.current().nextInt(0, nums.size)\n    // Получить и вернуть случайный элемент\n    val randomNum = nums[randomIndex]\n    return randomNum\n}\n
        array.rb
        ### Случайный доступ к элементу ###\ndef random_access(nums)\n  # Случайным образом выбрать число из интервала [0, nums.length)\n  random_index = Random.rand(0...nums.length)\n\n  # Получить и вернуть случайный элемент\n  nums[random_index]\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 4. Массивы и списки","4.1   Массив"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#3","level":3,"title":"3.   Вставка элемента","text":"

        Элементы массива в памяти расположены вплотную друг к другу, и между ними нет места для размещения новых данных. Как показано на рисунке 4-3, если мы хотим вставить элемент в середину массива, то все элементы после этой позиции нужно сдвинуть на одну позицию вправо, а затем записать новое значение в освободившийся индекс.

        Рисунок 4-3   Пример вставки элемента в массив

        Стоит отметить, что длина массива фиксирована, поэтому вставка нового элемента неизбежно приведет к потере элемента на конце массива. Решение этой проблемы мы оставим для обсуждения в разделе о \"списках\".

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
        def insert(nums: list[int], num: int, index: int):\n    \"\"\"Вставить элемент num по индексу index в массив\"\"\"\n    # Сдвинуть элемент с индексом index и все последующие элементы на одну позицию назад\n    for i in range(len(nums) - 1, index, -1):\n        nums[i] = nums[i - 1]\n    # Присвоить num элементу по индексу index\n    nums[index] = num\n
        array.cpp
        /* Вставить элемент num по индексу index в массив */\nvoid insert(int *nums, int size, int num, int index) {\n    // Сдвинуть элемент с индексом index и все последующие элементы на одну позицию назад\n    for (int i = size - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // Присвоить num элементу по индексу index\n    nums[index] = num;\n}\n
        array.java
        /* Вставить элемент num по индексу index в массив */\nvoid insert(int[] nums, int num, int index) {\n    // Сдвинуть элемент с индексом index и все последующие элементы на одну позицию назад\n    for (int i = nums.length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // Присвоить num элементу по индексу index\n    nums[index] = num;\n}\n
        array.cs
        /* Вставить элемент num по индексу index в массив */\nvoid Insert(int[] nums, int num, int index) {\n    // Сдвинуть элемент с индексом index и все последующие элементы на одну позицию назад\n    for (int i = nums.Length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // Присвоить num элементу по индексу index\n    nums[index] = num;\n}\n
        array.go
        /* Вставить элемент num по индексу index в массив */\nfunc insert(nums []int, num int, index int) {\n    // Сдвинуть элемент с индексом index и все последующие элементы на одну позицию назад\n    for i := len(nums) - 1; i > index; i-- {\n        nums[i] = nums[i-1]\n    }\n    // Присвоить num элементу по индексу index\n    nums[index] = num\n}\n
        array.swift
        /* Вставить элемент num по индексу index в массив */\nfunc insert(nums: inout [Int], num: Int, index: Int) {\n    // Сдвинуть элемент с индексом index и все последующие элементы на одну позицию назад\n    for i in nums.indices.dropFirst(index).reversed() {\n        nums[i] = nums[i - 1]\n    }\n    // Присвоить num элементу по индексу index\n    nums[index] = num\n}\n
        array.js
        /* Вставить элемент num по индексу index в массив */\nfunction insert(nums, num, index) {\n    // Сдвинуть элемент с индексом index и все последующие элементы на одну позицию назад\n    for (let i = nums.length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // Присвоить num элементу по индексу index\n    nums[index] = num;\n}\n
        array.ts
        /* Вставить элемент num по индексу index в массив */\nfunction insert(nums: number[], num: number, index: number): void {\n    // Сдвинуть элемент с индексом index и все последующие элементы на одну позицию назад\n    for (let i = nums.length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // Присвоить num элементу по индексу index\n    nums[index] = num;\n}\n
        array.dart
        /* Вставить элемент _num по индексу index в массив */\nvoid insert(List<int> nums, int _num, int index) {\n  // Сдвинуть элемент с индексом index и все последующие элементы на одну позицию назад\n  for (var i = nums.length - 1; i > index; i--) {\n    nums[i] = nums[i - 1];\n  }\n  // Присвоить _num элементу по индексу index\n  nums[index] = _num;\n}\n
        array.rs
        /* Вставить элемент num по индексу index в массив */\nfn insert(nums: &mut [i32], num: i32, index: usize) {\n    // Сдвинуть элемент с индексом index и все последующие элементы на одну позицию назад\n    for i in (index + 1..nums.len()).rev() {\n        nums[i] = nums[i - 1];\n    }\n    // Присвоить num элементу по индексу index\n    nums[index] = num;\n}\n
        array.c
        /* Вставить элемент num по индексу index в массив */\nvoid insert(int *nums, int size, int num, int index) {\n    // Сдвинуть элемент с индексом index и все последующие элементы на одну позицию назад\n    for (int i = size - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // Присвоить num элементу по индексу index\n    nums[index] = num;\n}\n
        array.kt
        /* Вставить элемент num по индексу index в массив */\nfun insert(nums: IntArray, num: Int, index: Int) {\n    // Сдвинуть элемент с индексом index и все последующие элементы на одну позицию назад\n    for (i in nums.size - 1 downTo index + 1) {\n        nums[i] = nums[i - 1]\n    }\n    // Присвоить num элементу по индексу index\n    nums[index] = num\n}\n
        array.rb
        ### Вставка элемента num по индексу index в массив ###\ndef insert(nums, num, index)\n  # Сдвинуть элемент с индексом index и все последующие элементы на одну позицию назад\n  for i in (nums.length - 1).downto(index + 1)\n    nums[i] = nums[i - 1]\n  end\n\n  # Присвоить num элементу по индексу index\n  nums[index] = num\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 4. Массивы и списки","4.1   Массив"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#4","level":3,"title":"4.   Удаление элемента","text":"

        Аналогично, как показано на рисунке 4-4, если нужно удалить элемент по индексу \\(i\\) , то все элементы после индекса \\(i\\) необходимо сдвинуть на одну позицию влево.

        Рисунок 4-4   Пример удаления элемента из массива

        Обрати внимание: после удаления исходный последний элемент становится бессмысленным, поэтому специально изменять его не требуется.

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
        def remove(nums: list[int], index: int):\n    \"\"\"Удалить элемент по индексу index\"\"\"\n    # Сдвинуть все элементы после индекса index на одну позицию вперед\n    for i in range(index, len(nums) - 1):\n        nums[i] = nums[i + 1]\n
        array.cpp
        /* Удалить элемент по индексу index */\nvoid remove(int *nums, int size, int index) {\n    // Сдвинуть все элементы после индекса index на одну позицию вперед\n    for (int i = index; i < size - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
        array.java
        /* Удалить элемент по индексу index */\nvoid remove(int[] nums, int index) {\n    // Сдвинуть все элементы после индекса index на одну позицию вперед\n    for (int i = index; i < nums.length - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
        array.cs
        /* Удалить элемент по индексу index */\nvoid Remove(int[] nums, int index) {\n    // Сдвинуть все элементы после индекса index на одну позицию вперед\n    for (int i = index; i < nums.Length - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
        array.go
        /* Удалить элемент по индексу index */\nfunc remove(nums []int, index int) {\n    // Сдвинуть все элементы после индекса index на одну позицию вперед\n    for i := index; i < len(nums)-1; i++ {\n        nums[i] = nums[i+1]\n    }\n}\n
        array.swift
        /* Удалить элемент по индексу index */\nfunc remove(nums: inout [Int], index: Int) {\n    // Сдвинуть все элементы после индекса index на одну позицию вперед\n    for i in nums.indices.dropFirst(index).dropLast() {\n        nums[i] = nums[i + 1]\n    }\n}\n
        array.js
        /* Удалить элемент по индексу index */\nfunction remove(nums, index) {\n    // Сдвинуть все элементы после индекса index на одну позицию вперед\n    for (let i = index; i < nums.length - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
        array.ts
        /* Удалить элемент по индексу index */\nfunction remove(nums: number[], index: number): void {\n    // Сдвинуть все элементы после индекса index на одну позицию вперед\n    for (let i = index; i < nums.length - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
        array.dart
        /* Удалить элемент по индексу index */\nvoid remove(List<int> nums, int index) {\n  // Сдвинуть все элементы после индекса index на одну позицию вперед\n  for (var i = index; i < nums.length - 1; i++) {\n    nums[i] = nums[i + 1];\n  }\n}\n
        array.rs
        /* Удалить элемент по индексу index */\nfn remove(nums: &mut [i32], index: usize) {\n    // Сдвинуть все элементы после индекса index на одну позицию вперед\n    for i in index..nums.len() - 1 {\n        nums[i] = nums[i + 1];\n    }\n}\n
        array.c
        /* Удалить элемент по индексу index */\n// Внимание: stdio.h уже использует ключевое слово remove\nvoid removeItem(int *nums, int size, int index) {\n    // Сдвинуть все элементы после индекса index на одну позицию вперед\n    for (int i = index; i < size - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
        array.kt
        /* Удалить элемент по индексу index */\nfun remove(nums: IntArray, index: Int) {\n    // Сдвинуть все элементы после индекса index на одну позицию вперед\n    for (i in index..<nums.size - 1) {\n        nums[i] = nums[i + 1]\n    }\n}\n
        array.rb
        ### Удаление элемента по индексу index ###\ndef remove(nums, index)\n  # Сдвинуть все элементы после индекса index на одну позицию вперед\n  for i in index...(nums.length - 1)\n    nums[i] = nums[i + 1]\n  end\nend\n
        Визуализация кода

        Во весь экран >

        В целом операции вставки и удаления в массиве имеют следующие недостатки.

        • Высокая временная сложность: средняя временная сложность и вставки, и удаления равна \\(O(n)\\) , где \\(n\\) - длина массива.
        • Потеря элементов: поскольку длина массива неизменяема, после вставки элементы, выходящие за пределы длины массива, будут потеряны.
        • Потери памяти: можно заранее инициализировать более длинный массив и использовать только его переднюю часть; тогда теряемые при вставке элементы на конце не будут нести смысла, но такой подход приводит к лишнему расходу памяти.
        ","path":["Глава 4. Массивы и списки","4.1   Массив"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#5","level":3,"title":"5.   Обход массива","text":"

        В большинстве языков программирования массив можно обходить как по индексу, так и напрямую перебирая каждый элемент:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
        def traverse(nums: list[int]):\n    \"\"\"Обход массива\"\"\"\n    count = 0\n    # Обход массива по индексам\n    for i in range(len(nums)):\n        count += nums[i]\n    # Непосредственно обходить элементы массива\n    for num in nums:\n        count += num\n    # Одновременно обходить индексы и элементы данных\n    for i, num in enumerate(nums):\n        count += nums[i]\n        count += num\n
        array.cpp
        /* Обход массива */\nvoid traverse(int *nums, int size) {\n    int count = 0;\n    // Обход массива по индексам\n    for (int i = 0; i < size; i++) {\n        count += nums[i];\n    }\n}\n
        array.java
        /* Обход массива */\nvoid traverse(int[] nums) {\n    int count = 0;\n    // Обход массива по индексам\n    for (int i = 0; i < nums.length; i++) {\n        count += nums[i];\n    }\n    // Непосредственно обходить элементы массива\n    for (int num : nums) {\n        count += num;\n    }\n}\n
        array.cs
        /* Обход массива */\nvoid Traverse(int[] nums) {\n    int count = 0;\n    // Обход массива по индексам\n    for (int i = 0; i < nums.Length; i++) {\n        count += nums[i];\n    }\n    // Непосредственно обходить элементы массива\n    foreach (int num in nums) {\n        count += num;\n    }\n}\n
        array.go
        /* Обход массива */\nfunc traverse(nums []int) {\n    count := 0\n    // Обход массива по индексам\n    for i := 0; i < len(nums); i++ {\n        count += nums[i]\n    }\n    count = 0\n    // Непосредственно обходить элементы массива\n    for _, num := range nums {\n        count += num\n    }\n    // Одновременно обходить индексы и элементы данных\n    for i, num := range nums {\n        count += nums[i]\n        count += num\n    }\n}\n
        array.swift
        /* Обход массива */\nfunc traverse(nums: [Int]) {\n    var count = 0\n    // Обход массива по индексам\n    for i in nums.indices {\n        count += nums[i]\n    }\n    // Непосредственно обходить элементы массива\n    for num in nums {\n        count += num\n    }\n    // Одновременно обходить индексы и элементы данных\n    for (i, num) in nums.enumerated() {\n        count += nums[i]\n        count += num\n    }\n}\n
        array.js
        /* Обход массива */\nfunction traverse(nums) {\n    let count = 0;\n    // Обход массива по индексам\n    for (let i = 0; i < nums.length; i++) {\n        count += nums[i];\n    }\n    // Непосредственно обходить элементы массива\n    for (const num of nums) {\n        count += num;\n    }\n}\n
        array.ts
        /* Обход массива */\nfunction traverse(nums: number[]): void {\n    let count = 0;\n    // Обход массива по индексам\n    for (let i = 0; i < nums.length; i++) {\n        count += nums[i];\n    }\n    // Непосредственно обходить элементы массива\n    for (const num of nums) {\n        count += num;\n    }\n}\n
        array.dart
        /* Перебрать элементы массива */\nvoid traverse(List<int> nums) {\n  int count = 0;\n  // Обход массива по индексам\n  for (var i = 0; i < nums.length; i++) {\n    count += nums[i];\n  }\n  // Непосредственно обходить элементы массива\n  for (int _num in nums) {\n    count += _num;\n  }\n  // Перебрать массив методом forEach\n  nums.forEach((_num) {\n    count += _num;\n  });\n}\n
        array.rs
        /* Обход массива */\nfn traverse(nums: &[i32]) {\n    let mut _count = 0;\n    // Обход массива по индексам\n    for i in 0..nums.len() {\n        _count += nums[i];\n    }\n    // Непосредственно обходить элементы массива\n    _count = 0;\n    for &num in nums {\n        _count += num;\n    }\n}\n
        array.c
        /* Обход массива */\nvoid traverse(int *nums, int size) {\n    int count = 0;\n    // Обход массива по индексам\n    for (int i = 0; i < size; i++) {\n        count += nums[i];\n    }\n}\n
        array.kt
        /* Обход массива */\nfun traverse(nums: IntArray) {\n    var count = 0\n    // Обход массива по индексам\n    for (i in nums.indices) {\n        count += nums[i]\n    }\n    // Непосредственно обходить элементы массива\n    for (j in nums) {\n        count += j\n    }\n}\n
        array.rb
        ### Обход массива ###\ndef traverse(nums)\n  count = 0\n\n  # Обход массива по индексам\n  for i in 0...nums.length\n    count += nums[i]\n  end\n\n  # Непосредственно обходить элементы массива\n  for num in nums\n    count += num\n  end\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 4. Массивы и списки","4.1   Массив"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#6","level":3,"title":"6.   Поиск элемента","text":"

        Чтобы найти заданный элемент в массиве, нужно пройти по массиву и на каждой итерации проверять, совпадает ли значение; если совпадает, вернуть соответствующий индекс.

        Поскольку массив - это линейная структура данных, такая операция поиска называется линейным поиском.

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
        def find(nums: list[int], target: int) -> int:\n    \"\"\"Найти заданный элемент в массиве\"\"\"\n    for i in range(len(nums)):\n        if nums[i] == target:\n            return i\n    return -1\n
        array.cpp
        /* Найти заданный элемент в массиве */\nint find(int *nums, int size, int target) {\n    for (int i = 0; i < size; i++) {\n        if (nums[i] == target)\n            return i;\n    }\n    return -1;\n}\n
        array.java
        /* Найти заданный элемент в массиве */\nint find(int[] nums, int target) {\n    for (int i = 0; i < nums.length; i++) {\n        if (nums[i] == target)\n            return i;\n    }\n    return -1;\n}\n
        array.cs
        /* Найти заданный элемент в массиве */\nint Find(int[] nums, int target) {\n    for (int i = 0; i < nums.Length; i++) {\n        if (nums[i] == target)\n            return i;\n    }\n    return -1;\n}\n
        array.go
        /* Найти заданный элемент в массиве */\nfunc find(nums []int, target int) (index int) {\n    index = -1\n    for i := 0; i < len(nums); i++ {\n        if nums[i] == target {\n            index = i\n            break\n        }\n    }\n    return\n}\n
        array.swift
        /* Найти заданный элемент в массиве */\nfunc find(nums: [Int], target: Int) -> Int {\n    for i in nums.indices {\n        if nums[i] == target {\n            return i\n        }\n    }\n    return -1\n}\n
        array.js
        /* Найти заданный элемент в массиве */\nfunction find(nums, target) {\n    for (let i = 0; i < nums.length; i++) {\n        if (nums[i] === target) return i;\n    }\n    return -1;\n}\n
        array.ts
        /* Найти заданный элемент в массиве */\nfunction find(nums: number[], target: number): number {\n    for (let i = 0; i < nums.length; i++) {\n        if (nums[i] === target) {\n            return i;\n        }\n    }\n    return -1;\n}\n
        array.dart
        /* Найти заданный элемент в массиве */\nint find(List<int> nums, int target) {\n  for (var i = 0; i < nums.length; i++) {\n    if (nums[i] == target) return i;\n  }\n  return -1;\n}\n
        array.rs
        /* Найти заданный элемент в массиве */\nfn find(nums: &[i32], target: i32) -> Option<usize> {\n    for i in 0..nums.len() {\n        if nums[i] == target {\n            return Some(i);\n        }\n    }\n    None\n}\n
        array.c
        /* Найти заданный элемент в массиве */\nint find(int *nums, int size, int target) {\n    for (int i = 0; i < size; i++) {\n        if (nums[i] == target)\n            return i;\n    }\n    return -1;\n}\n
        array.kt
        /* Найти заданный элемент в массиве */\nfun find(nums: IntArray, target: Int): Int {\n    for (i in nums.indices) {\n        if (nums[i] == target)\n            return i\n    }\n    return -1\n}\n
        array.rb
        ### Поиск заданного элемента в массиве ###\ndef find(nums, target)\n  for i in 0...nums.length\n    return i if nums[i] == target\n  end\n\n  -1\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 4. Массивы и списки","4.1   Массив"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#7","level":3,"title":"7.   Расширение массива","text":"

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

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

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
        def extend(nums: list[int], enlarge: int) -> list[int]:\n    \"\"\"Увеличить длину массива\"\"\"\n    # Инициализировать массив увеличенной длины\n    res = [0] * (len(nums) + enlarge)\n    # Скопировать все элементы исходного массива в новый массив\n    for i in range(len(nums)):\n        res[i] = nums[i]\n    # Вернуть новый массив после расширения\n    return res\n
        array.cpp
        /* Увеличить длину массива */\nint *extend(int *nums, int size, int enlarge) {\n    // Инициализировать массив увеличенной длины\n    int *res = new int[size + enlarge];\n    // Скопировать все элементы исходного массива в новый массив\n    for (int i = 0; i < size; i++) {\n        res[i] = nums[i];\n    }\n    // Освободить память\n    delete[] nums;\n    // Вернуть новый массив после расширения\n    return res;\n}\n
        array.java
        /* Увеличить длину массива */\nint[] extend(int[] nums, int enlarge) {\n    // Инициализировать массив увеличенной длины\n    int[] res = new int[nums.length + enlarge];\n    // Скопировать все элементы исходного массива в новый массив\n    for (int i = 0; i < nums.length; i++) {\n        res[i] = nums[i];\n    }\n    // Вернуть новый массив после расширения\n    return res;\n}\n
        array.cs
        /* Увеличить длину массива */\nint[] Extend(int[] nums, int enlarge) {\n    // Инициализировать массив увеличенной длины\n    int[] res = new int[nums.Length + enlarge];\n    // Скопировать все элементы исходного массива в новый массив\n    for (int i = 0; i < nums.Length; i++) {\n        res[i] = nums[i];\n    }\n    // Вернуть новый массив после расширения\n    return res;\n}\n
        array.go
        /* Увеличить длину массива */\nfunc extend(nums []int, enlarge int) []int {\n    // Инициализировать массив увеличенной длины\n    res := make([]int, len(nums)+enlarge)\n    // Скопировать все элементы исходного массива в новый массив\n    for i, num := range nums {\n        res[i] = num\n    }\n    // Вернуть новый массив после расширения\n    return res\n}\n
        array.swift
        /* Увеличить длину массива */\nfunc extend(nums: [Int], enlarge: Int) -> [Int] {\n    // Инициализировать массив увеличенной длины\n    var res = Array(repeating: 0, count: nums.count + enlarge)\n    // Скопировать все элементы исходного массива в новый массив\n    for i in nums.indices {\n        res[i] = nums[i]\n    }\n    // Вернуть новый массив после расширения\n    return res\n}\n
        array.js
        /* Увеличить длину массива */\n// Обратите внимание: Array в JavaScript — это динамический массив, его можно расширять напрямую\n// Для удобства обучения в этой функции Array рассматривается как массив неизменяемой длины\nfunction extend(nums, enlarge) {\n    // Инициализировать массив увеличенной длины\n    const res = new Array(nums.length + enlarge).fill(0);\n    // Скопировать все элементы исходного массива в новый массив\n    for (let i = 0; i < nums.length; i++) {\n        res[i] = nums[i];\n    }\n    // Вернуть новый массив после расширения\n    return res;\n}\n
        array.ts
        /* Увеличить длину массива */\n// Обратите внимание: Array в TypeScript — это динамический массив, его можно расширять напрямую\n// Для удобства обучения в этой функции Array рассматривается как массив неизменяемой длины\nfunction extend(nums: number[], enlarge: number): number[] {\n    // Инициализировать массив увеличенной длины\n    const res = new Array(nums.length + enlarge).fill(0);\n    // Скопировать все элементы исходного массива в новый массив\n    for (let i = 0; i < nums.length; i++) {\n        res[i] = nums[i];\n    }\n    // Вернуть новый массив после расширения\n    return res;\n}\n
        array.dart
        /* Увеличить длину массива */\nList<int> extend(List<int> nums, int enlarge) {\n  // Инициализировать массив увеличенной длины\n  List<int> res = List.filled(nums.length + enlarge, 0);\n  // Скопировать все элементы исходного массива в новый массив\n  for (var i = 0; i < nums.length; i++) {\n    res[i] = nums[i];\n  }\n  // Вернуть новый массив после расширения\n  return res;\n}\n
        array.rs
        /* Увеличить длину массива */\nfn extend(nums: &[i32], enlarge: usize) -> Vec<i32> {\n    // Инициализировать массив увеличенной длины\n    let mut res: Vec<i32> = vec![0; nums.len() + enlarge];\n    // Скопировать все элементы исходного массива в новый\n    res[0..nums.len()].copy_from_slice(nums);\n\n    // Вернуть новый массив после расширения\n    res\n}\n
        array.c
        /* Увеличить длину массива */\nint *extend(int *nums, int size, int enlarge) {\n    // Инициализировать массив увеличенной длины\n    int *res = (int *)malloc(sizeof(int) * (size + enlarge));\n    // Скопировать все элементы исходного массива в новый массив\n    for (int i = 0; i < size; i++) {\n        res[i] = nums[i];\n    }\n    // Инициализировать расширенное пространство\n    for (int i = size; i < size + enlarge; i++) {\n        res[i] = 0;\n    }\n    // Вернуть новый массив после расширения\n    return res;\n}\n
        array.kt
        /* Увеличить длину массива */\nfun extend(nums: IntArray, enlarge: Int): IntArray {\n    // Инициализировать массив увеличенной длины\n    val res = IntArray(nums.size + enlarge)\n    // Скопировать все элементы исходного массива в новый массив\n    for (i in nums.indices) {\n        res[i] = nums[i]\n    }\n    // Вернуть новый массив после расширения\n    return res\n}\n
        array.rb
        ### Увеличить длину массива ###\n# Обратите внимание: Array в Ruby является динамическим массивом и может расширяться напрямую\n# Для удобства обучения в этой функции Array рассматривается как массив неизменяемой длины\ndef extend(nums, enlarge)\n  # Инициализировать массив увеличенной длины\n  res = Array.new(nums.length + enlarge, 0)\n\n  # Скопировать все элементы исходного массива в новый массив\n  for i in 0...nums.length\n    res[i] = nums[i]\n  end\n\n  # Вернуть новый массив после расширения\n  res\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 4. Массивы и списки","4.1   Массив"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#412","level":2,"title":"4.1.2   Преимущества и ограничения массива","text":"

        Массив хранится в непрерывной области памяти, и все его элементы имеют один и тот же тип. Такой подход содержит богатую априорную информацию, которую система может использовать для оптимизации эффективности операций с этой структурой данных.

        • Высокая пространственная эффективность: массив выделяет для данных непрерывный блок памяти без дополнительного структурного накладного расхода.
        • Поддержка произвольного доступа: массив позволяет обращаться к любому элементу за \\(O(1)\\) времени.
        • Локальность кэша: при обращении к элементу массива компьютер загружает не только сам элемент, но и соседние данные, что позволяет использовать кэш для ускорения последующих операций.

        Непрерывное хранение данных - это палка о двух концах, и у него есть следующие ограничения.

        • Низкая эффективность вставки и удаления: когда элементов в массиве много, вставка и удаление требуют сдвига большого количества элементов.
        • Неизменяемая длина: после инициализации длина массива фиксирована; расширение массива требует копирования всех данных в новый массив, что стоит дорого.
        • Потери памяти: если выделенный массив больше, чем реально необходимо, лишнее пространство пропадает впустую.
        ","path":["Глава 4. Массивы и списки","4.1   Массив"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#413","level":2,"title":"4.1.3   Типичные применения массива","text":"

        Массив - это базовая и очень распространенная структура данных. Он часто используется как в различных алгоритмах, так и при реализации более сложных структур данных.

        • Произвольный доступ: если мы хотим случайным образом выбирать некоторые образцы, можно сохранить их в массиве и сгенерировать случайную последовательность индексов для выборки.
        • Сортировка и поиск: массив - самая распространенная структура данных для алгоритмов сортировки и поиска. Быстрая сортировка, сортировка слиянием, двоичный поиск и многие другие алгоритмы в основном работают именно с массивами.
        • Таблица поиска: когда нужно быстро находить элемент или его соответствие, массив можно использовать как таблицу поиска. Например, если мы хотим реализовать отображение символов в коды ASCII, можно использовать значение ASCII как индекс, а соответствующий элемент хранить по этой позиции массива.
        • Машинное обучение: в нейронных сетях широко используются операции линейной алгебры над векторами, матрицами и тензорами, и все эти данные строятся в форме массивов. Массив - самая часто используемая структура данных в программировании нейросетей.
        • Реализация структур данных: массивы можно использовать для реализации стеков, очередей, хеш-таблиц, куч, графов и других структур данных. Например, матрица смежности графа по сути является двумерным массивом.
        ","path":["Глава 4. Массивы и списки","4.1   Массив"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/","level":1,"title":"4.2   Связный список","text":"

        Память - общий ресурс для всех программ, и в сложной среде выполнения свободные участки памяти могут быть разбросаны по всему адресному пространству. Мы знаем, что память для хранения массива должна быть непрерывной, а если массив очень велик, в памяти может не оказаться столь большого непрерывного блока. Именно здесь и проявляется преимущество гибкости связного списка.

        Связный список (linked list) - это линейная структура данных, в которой каждый элемент представляет собой объект-узел, а сами узлы соединены между собой с помощью ссылок. Ссылка хранит адрес памяти следующего узла, благодаря чему из текущего узла можно перейти к следующему.

        Конструкция связного списка позволяет хранить отдельные узлы в разных местах памяти, и их адреса вовсе не обязаны быть последовательными.

        Рисунок 4-5   Определение связного списка и способ хранения

        Как видно на рисунке 4-5, базовой единицей связного списка является объект узел (node). Каждый узел содержит две части данных: значение узла и ссылку на следующий узел.

        • Первый узел связного списка называется головным узлом, а последний - хвостовым узлом.
        • Хвостовой узел указывает на пустое значение, что в Java, C++ и Python обозначается как null , nullptr и None соответственно.
        • В языках, поддерживающих указатели, таких как C, C++, Go и Rust, упомянутую выше ссылку следует заменить на указатель.

        Как показано в коде ниже, узел связного списка ListNode хранит не только значение, но и дополнительную ссылку (указатель). Поэтому при одинаковом объеме данных связный список занимает больше памяти, чем массив.

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
        class ListNode:\n    \"\"\"Класс узла связного списка\"\"\"\n    def __init__(self, val: int):\n        self.val: int = val               # Значение узла\n        self.next: ListNode | None = None # Ссылка на следующий узел\n
        /* Структура узла связного списка */\nstruct ListNode {\n    int val;         // Значение узла\n    ListNode *next;  // Указатель на следующий узел\n    ListNode(int x) : val(x), next(nullptr) {}  // Конструктор\n};\n
        /* Класс узла связного списка */\nclass ListNode {\n    int val;        // Значение узла\n    ListNode next;  // Ссылка на следующий узел\n    ListNode(int x) { val = x; }  // Конструктор\n}\n
        /* Класс узла связного списка */\nclass ListNode(int x) {  // Конструктор\n    int val = x;         // Значение узла\n    ListNode? next;      // Ссылка на следующий узел\n}\n
        /* Структура узла связного списка */\ntype ListNode struct {\n    Val  int       // Значение узла\n    Next *ListNode // Указатель на следующий узел\n}\n\n// NewListNode Конструктор, создает новый узел\nfunc NewListNode(val int) *ListNode {\n    return &ListNode{\n        Val:  val,\n        Next: nil,\n    }\n}\n
        /* Класс узла связного списка */\nclass ListNode {\n    var val: Int // Значение узла\n    var next: ListNode? // Ссылка на следующий узел\n\n    init(x: Int) { // Конструктор\n        val = x\n    }\n}\n
        /* Класс узла связного списка */\nclass ListNode {\n    constructor(val, next) {\n        this.val = (val === undefined ? 0 : val);       // Значение узла\n        this.next = (next === undefined ? null : next); // Ссылка на следующий узел\n    }\n}\n
        /* Класс узла связного списка */\nclass ListNode {\n    val: number;\n    next: ListNode | null;\n    constructor(val?: number, next?: ListNode | null) {\n        this.val = val === undefined ? 0 : val;        // Значение узла\n        this.next = next === undefined ? null : next;  // Ссылка на следующий узел\n    }\n}\n
        /* Класс узла связного списка */\nclass ListNode {\n  int val; // Значение узла\n  ListNode? next; // Ссылка на следующий узел\n  ListNode(this.val, [this.next]); // Конструктор\n}\n
        use std::rc::Rc;\nuse std::cell::RefCell;\n/* Класс узла связного списка */\n#[derive(Debug)]\nstruct ListNode {\n    val: i32, // Значение узла\n    next: Option<Rc<RefCell<ListNode>>>, // Указатель на следующий узел\n}\n
        /* Структура узла связного списка */\ntypedef struct ListNode {\n    int val;               // Значение узла\n    struct ListNode *next; // Указатель на следующий узел\n} ListNode;\n\n/* Конструктор */\nListNode *newListNode(int val) {\n    ListNode *node;\n    node = (ListNode *) malloc(sizeof(ListNode));\n    node->val = val;\n    node->next = NULL;\n    return node;\n}\n
        /* Класс узла связного списка */\n// Конструктор\nclass ListNode(x: Int) {\n    val _val: Int = x          // Значение узла\n    val next: ListNode? = null // Ссылка на следующий узел\n}\n
        # Класс узла связного списка\nclass ListNode\n  attr_accessor :val  # Значение узла\n  attr_accessor :next # Ссылка на следующий узел\n\n  def initialize(val=0, next_node=nil)\n    @val = val\n    @next = next_node\n  end\nend\n
        ","path":["Глава 4. Массивы и списки","4.2   Связный список"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#421","level":2,"title":"4.2.1   Основные операции со связным списком","text":"","path":["Глава 4. Массивы и списки","4.2   Связный список"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#1","level":3,"title":"1.   Инициализация связного списка","text":"

        Построение связного списка состоит из двух шагов: сначала нужно инициализировать объекты всех узлов, затем установить ссылочные связи между ними. После завершения инициализации мы можем, начиная с головы списка, последовательно проходить все узлы по ссылке next.

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
        # Инициализация связного списка 1 -> 3 -> 2 -> 5 -> 4\n# Инициализация отдельных узлов\nn0 = ListNode(1)\nn1 = ListNode(3)\nn2 = ListNode(2)\nn3 = ListNode(5)\nn4 = ListNode(4)\n# Построение ссылок между узлами\nn0.next = n1\nn1.next = n2\nn2.next = n3\nn3.next = n4\n
        linked_list.cpp
        /* Инициализация связного списка 1 -> 3 -> 2 -> 5 -> 4 */\n// Инициализация отдельных узлов\nListNode* n0 = new ListNode(1);\nListNode* n1 = new ListNode(3);\nListNode* n2 = new ListNode(2);\nListNode* n3 = new ListNode(5);\nListNode* n4 = new ListNode(4);\n// Построение ссылок между узлами\nn0->next = n1;\nn1->next = n2;\nn2->next = n3;\nn3->next = n4;\n
        linked_list.java
        /* Инициализация связного списка 1 -> 3 -> 2 -> 5 -> 4 */\n// Инициализация отдельных узлов\nListNode n0 = new ListNode(1);\nListNode n1 = new ListNode(3);\nListNode n2 = new ListNode(2);\nListNode n3 = new ListNode(5);\nListNode n4 = new ListNode(4);\n// Построение ссылок между узлами\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
        linked_list.cs
        /* Инициализация связного списка 1 -> 3 -> 2 -> 5 -> 4 */\n// Инициализация отдельных узлов\nListNode n0 = new(1);\nListNode n1 = new(3);\nListNode n2 = new(2);\nListNode n3 = new(5);\nListNode n4 = new(4);\n// Построение ссылок между узлами\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
        linked_list.go
        /* Инициализация связного списка 1 -> 3 -> 2 -> 5 -> 4 */\n// Инициализация отдельных узлов\nn0 := NewListNode(1)\nn1 := NewListNode(3)\nn2 := NewListNode(2)\nn3 := NewListNode(5)\nn4 := NewListNode(4)\n// Построение ссылок между узлами\nn0.Next = n1\nn1.Next = n2\nn2.Next = n3\nn3.Next = n4\n
        linked_list.swift
        /* Инициализация связного списка 1 -> 3 -> 2 -> 5 -> 4 */\n// Инициализация отдельных узлов\nlet n0 = ListNode(x: 1)\nlet n1 = ListNode(x: 3)\nlet n2 = ListNode(x: 2)\nlet n3 = ListNode(x: 5)\nlet n4 = ListNode(x: 4)\n// Построение ссылок между узлами\nn0.next = n1\nn1.next = n2\nn2.next = n3\nn3.next = n4\n
        linked_list.js
        /* Инициализация связного списка 1 -> 3 -> 2 -> 5 -> 4 */\n// Инициализация отдельных узлов\nconst n0 = new ListNode(1);\nconst n1 = new ListNode(3);\nconst n2 = new ListNode(2);\nconst n3 = new ListNode(5);\nconst n4 = new ListNode(4);\n// Построение ссылок между узлами\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
        linked_list.ts
        /* Инициализация связного списка 1 -> 3 -> 2 -> 5 -> 4 */\n// Инициализация отдельных узлов\nconst n0 = new ListNode(1);\nconst n1 = new ListNode(3);\nconst n2 = new ListNode(2);\nconst n3 = new ListNode(5);\nconst n4 = new ListNode(4);\n// Построение ссылок между узлами\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
        linked_list.dart
        /* Инициализация связного списка 1 -> 3 -> 2 -> 5 -> 4 */\\\n// Инициализация отдельных узлов\nListNode n0 = ListNode(1);\nListNode n1 = ListNode(3);\nListNode n2 = ListNode(2);\nListNode n3 = ListNode(5);\nListNode n4 = ListNode(4);\n// Построение ссылок между узлами\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
        linked_list.rs
        /* Инициализация связного списка 1 -> 3 -> 2 -> 5 -> 4 */\n// Инициализация отдельных узлов\nlet n0 = Rc::new(RefCell::new(ListNode { val: 1, next: None }));\nlet n1 = Rc::new(RefCell::new(ListNode { val: 3, next: None }));\nlet n2 = Rc::new(RefCell::new(ListNode { val: 2, next: None }));\nlet n3 = Rc::new(RefCell::new(ListNode { val: 5, next: None }));\nlet n4 = Rc::new(RefCell::new(ListNode { val: 4, next: None }));\n\n// Построение ссылок между узлами\nn0.borrow_mut().next = Some(n1.clone());\nn1.borrow_mut().next = Some(n2.clone());\nn2.borrow_mut().next = Some(n3.clone());\nn3.borrow_mut().next = Some(n4.clone());\n
        linked_list.c
        /* Инициализация связного списка 1 -> 3 -> 2 -> 5 -> 4 */\n// Инициализация отдельных узлов\nListNode* n0 = newListNode(1);\nListNode* n1 = newListNode(3);\nListNode* n2 = newListNode(2);\nListNode* n3 = newListNode(5);\nListNode* n4 = newListNode(4);\n// Построение ссылок между узлами\nn0->next = n1;\nn1->next = n2;\nn2->next = n3;\nn3->next = n4;\n
        linked_list.kt
        /* Инициализация связного списка 1 -> 3 -> 2 -> 5 -> 4 */\n// Инициализация отдельных узлов\nval n0 = ListNode(1)\nval n1 = ListNode(3)\nval n2 = ListNode(2)\nval n3 = ListNode(5)\nval n4 = ListNode(4)\n// Построение ссылок между узлами\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
        linked_list.rb
        # Инициализация связного списка 1 -> 3 -> 2 -> 5 -> 4\n# Инициализация отдельных узлов\nn0 = ListNode.new(1)\nn1 = ListNode.new(3)\nn2 = ListNode.new(2)\nn3 = ListNode.new(5)\nn4 = ListNode.new(4)\n# Построение ссылок между узлами\nn0.next = n1\nn1.next = n2\nn2.next = n3\nn3.next = n4\n
        Визуализация выполнения

        https://pythontutor.com/render.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%D1%81%D0%B2%D1%8F%D0%B7%D0%BD%D1%8B%D0%B9%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%D1%83%D0%B7%D0%B5%D0%BB%D0%BA%D0%BB%D0%B0%D1%81%D1%81%22%22%22%0A%20%20%20%20def%20__init__%28self%2C%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%D0%97%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%D1%83%D0%B7%D0%BB%D0%B0%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%D0%A1%D1%81%D1%8B%D0%BB%D0%BA%D0%B0%20%D0%BD%D0%B0%20%D1%81%D0%BB%D0%B5%D0%B4%D1%83%D1%8E%D1%89%D0%B8%D0%B9%20%D1%83%D0%B7%D0%B5%D0%BB%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%81%D0%B2%D1%8F%D0%B7%D0%BD%D1%8B%D0%B9%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%201%20-%3E%203%20-%3E%202%20-%3E%205%20-%3E%204%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D0%BA%D0%B0%D0%B6%D0%B4%D1%8B%D0%B9%20%D1%83%D0%B7%D0%B5%D0%BB%0A%20%20%20%20n0%20%3D%20ListNode%281%29%0A%20%20%20%20n1%20%3D%20ListNode%283%29%0A%20%20%20%20n2%20%3D%20ListNode%282%29%0A%20%20%20%20n3%20%3D%20ListNode%285%29%0A%20%20%20%20n4%20%3D%20ListNode%284%29%0A%20%20%20%20%23%20%D0%9F%D0%BE%D1%81%D1%82%D1%80%D0%BE%D0%B8%D1%82%D1%8C%20%D1%81%D1%81%D1%8B%D0%BB%D0%BA%D0%B8%20%D0%BC%D0%B5%D0%B6%D0%B4%D1%83%20%D1%83%D0%B7%D0%BB%D0%B0%D0%BC%D0%B8%0A%20%20%20%20n0.next%20%3D%20n1%0A%20%20%20%20n1.next%20%3D%20n2%0A%20%20%20%20n2.next%20%3D%20n3%0A%20%20%20%20n3.next%20%3D%20n4&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

        Массив в целом - это одна переменная: например, массив nums содержит элементы nums[0] , nums[1] и т.д. Связный список же состоит из множества независимых объектов-узлов. Обычно в качестве обозначения всего связного списка используют головной узел; например, в приведенном выше коде связный список можно обозначить как n0 .

        ","path":["Глава 4. Массивы и списки","4.2   Связный список"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#2","level":3,"title":"2.   Вставка узла","text":"

        Вставить узел в связный список очень легко. Как показано на рисунке 4-6, предположим, что мы хотим вставить новый узел P между двумя соседними узлами n0 и n1 ; для этого нужно изменить всего две ссылки (указателя), а временная сложность будет равна \\(O(1)\\) .

        Для сравнения: временная сложность вставки элемента в массив составляет \\(O(n)\\) , и при большом объеме данных это менее эффективно.

        Рисунок 4-6   Пример вставки узла в связный список

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
        def insert(n0: ListNode, P: ListNode):\n    \"\"\"Вставить узел P после узла n0 в связном списке\"\"\"\n    n1 = n0.next\n    P.next = n1\n    n0.next = P\n
        linked_list.cpp
        /* Вставить узел P после узла n0 в связном списке */\nvoid insert(ListNode *n0, ListNode *P) {\n    ListNode *n1 = n0->next;\n    P->next = n1;\n    n0->next = P;\n}\n
        linked_list.java
        /* Вставить узел P после узла n0 в связном списке */\nvoid insert(ListNode n0, ListNode P) {\n    ListNode n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
        linked_list.cs
        /* Вставить узел P после узла n0 в связном списке */\nvoid Insert(ListNode n0, ListNode P) {\n    ListNode? n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
        linked_list.go
        /* Вставить узел P после узла n0 в связном списке */\nfunc insertNode(n0 *ListNode, P *ListNode) {\n    n1 := n0.Next\n    P.Next = n1\n    n0.Next = P\n}\n
        linked_list.swift
        /* Вставить узел P после узла n0 в связном списке */\nfunc insert(n0: ListNode, P: ListNode) {\n    let n1 = n0.next\n    P.next = n1\n    n0.next = P\n}\n
        linked_list.js
        /* Вставить узел P после узла n0 в связном списке */\nfunction insert(n0, P) {\n    const n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
        linked_list.ts
        /* Вставить узел P после узла n0 в связном списке */\nfunction insert(n0: ListNode, P: ListNode): void {\n    const n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
        linked_list.dart
        /* Вставить узел P после узла n0 в связном списке */\nvoid insert(ListNode n0, ListNode P) {\n  ListNode? n1 = n0.next;\n  P.next = n1;\n  n0.next = P;\n}\n
        linked_list.rs
        /* Вставить узел P после узла n0 в связном списке */\n#[allow(non_snake_case)]\npub fn insert<T>(n0: &Rc<RefCell<ListNode<T>>>, P: Rc<RefCell<ListNode<T>>>) {\n    let n1 = n0.borrow_mut().next.take();\n    P.borrow_mut().next = n1;\n    n0.borrow_mut().next = Some(P);\n}\n
        linked_list.c
        /* Вставить узел P после узла n0 в связном списке */\nvoid insert(ListNode *n0, ListNode *P) {\n    ListNode *n1 = n0->next;\n    P->next = n1;\n    n0->next = P;\n}\n
        linked_list.kt
        /* Вставить узел P после узла n0 в связном списке */\nfun insert(n0: ListNode?, p: ListNode?) {\n    val n1 = n0?.next\n    p?.next = n1\n    n0?.next = p\n}\n
        linked_list.rb
        ### Вставка узла _p после узла n0 в связном списке ###\n# В Ruby `p` является встроенной функцией, а `P` — константой, поэтому вместо них можно использовать `_p`\ndef insert(n0, _p)\n  n1 = n0.next\n  _p.next = n1\n  n0.next = _p\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 4. Массивы и списки","4.2   Связный список"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#3","level":3,"title":"3.   Удаление узла","text":"

        Как показано на рисунке 4-7, удалить узел из связного списка тоже очень просто: нужно изменить всего одну ссылку (указатель).

        Стоит отметить, что хотя после завершения операции удаления узел P все еще указывает на n1 , при обходе связного списка до P уже нельзя добраться. Это означает, что P фактически больше не принадлежит данному списку.

        Рисунок 4-7   Удаление узла из связного списка

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
        def remove(n0: ListNode):\n    \"\"\"Удалить первый узел после узла n0 в связном списке\"\"\"\n    if not n0.next:\n        return\n    # n0 -> P -> n1\n    P = n0.next\n    n1 = P.next\n    n0.next = n1\n
        linked_list.cpp
        /* Удалить первый узел после узла n0 в связном списке */\nvoid remove(ListNode *n0) {\n    if (n0->next == nullptr)\n        return;\n    // n0 -> P -> n1\n    ListNode *P = n0->next;\n    ListNode *n1 = P->next;\n    n0->next = n1;\n    // Освободить память\n    delete P;\n}\n
        linked_list.java
        /* Удалить первый узел после узла n0 в связном списке */\nvoid remove(ListNode n0) {\n    if (n0.next == null)\n        return;\n    // n0 -> P -> n1\n    ListNode P = n0.next;\n    ListNode n1 = P.next;\n    n0.next = n1;\n}\n
        linked_list.cs
        /* Удалить первый узел после узла n0 в связном списке */\nvoid Remove(ListNode n0) {\n    if (n0.next == null)\n        return;\n    // n0 -> P -> n1\n    ListNode P = n0.next;\n    ListNode? n1 = P.next;\n    n0.next = n1;\n}\n
        linked_list.go
        /* Удалить первый узел после узла n0 в связном списке */\nfunc removeItem(n0 *ListNode) {\n    if n0.Next == nil {\n        return\n    }\n    // n0 -> P -> n1\n    P := n0.Next\n    n1 := P.Next\n    n0.Next = n1\n}\n
        linked_list.swift
        /* Удалить первый узел после узла n0 в связном списке */\nfunc remove(n0: ListNode) {\n    if n0.next == nil {\n        return\n    }\n    // n0 -> P -> n1\n    let P = n0.next\n    let n1 = P?.next\n    n0.next = n1\n}\n
        linked_list.js
        /* Удалить первый узел после узла n0 в связном списке */\nfunction remove(n0) {\n    if (!n0.next) return;\n    // n0 -> P -> n1\n    const P = n0.next;\n    const n1 = P.next;\n    n0.next = n1;\n}\n
        linked_list.ts
        /* Удалить первый узел после узла n0 в связном списке */\nfunction remove(n0: ListNode): void {\n    if (!n0.next) {\n        return;\n    }\n    // n0 -> P -> n1\n    const P = n0.next;\n    const n1 = P.next;\n    n0.next = n1;\n}\n
        linked_list.dart
        /* Удалить первый узел после узла n0 в связном списке */\nvoid remove(ListNode n0) {\n  if (n0.next == null) return;\n  // n0 -> P -> n1\n  ListNode P = n0.next!;\n  ListNode? n1 = P.next;\n  n0.next = n1;\n}\n
        linked_list.rs
        /* Удалить первый узел после узла n0 в связном списке */\n#[allow(non_snake_case)]\npub fn remove<T>(n0: &Rc<RefCell<ListNode<T>>>) {\n    // n0 -> P -> n1\n    let P = n0.borrow_mut().next.take();\n    if let Some(node) = P {\n        let n1 = node.borrow_mut().next.take();\n        n0.borrow_mut().next = n1;\n    }\n}\n
        linked_list.c
        /* Удалить первый узел после узла n0 в связном списке */\n// Внимание: stdio.h уже использует ключевое слово remove\nvoid removeItem(ListNode *n0) {\n    if (!n0->next)\n        return;\n    // n0 -> P -> n1\n    ListNode *P = n0->next;\n    ListNode *n1 = P->next;\n    n0->next = n1;\n    // Освободить память\n    free(P);\n}\n
        linked_list.kt
        /* Удалить первый узел после узла n0 в связном списке */\nfun remove(n0: ListNode?) {\n    if (n0?.next == null)\n        return\n    // n0 -> P -> n1\n    val p = n0.next\n    val n1 = p?.next\n    n0.next = n1\n}\n
        linked_list.rb
        ### Удаление первого узла после узла n0 в связном списке ###\ndef remove(n0)\n  return if n0.next.nil?\n\n  # n0 -> remove_node -> n1\n  remove_node = n0.next\n  n1 = remove_node.next\n  n0.next = n1\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 4. Массивы и списки","4.2   Связный список"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#4","level":3,"title":"4.   Доступ к узлу","text":"

        Доступ к узлам в связном списке менее эффективен. Как уже обсуждалось в предыдущем разделе, к любому элементу массива можно обратиться за \\(O(1)\\) времени. Со связным списком это не так: программе нужно начать с головного узла и последовательно двигаться дальше, пока не будет найден целевой узел. То есть для доступа к \\(i\\) -му узлу списка нужно выполнить \\(i - 1\\) итераций, а временная сложность составляет \\(O(n)\\) .

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
        def access(head: ListNode, index: int) -> ListNode | None:\n    \"\"\"Доступ к узлу связного списка по индексу index\"\"\"\n    for _ in range(index):\n        if not head:\n            return None\n        head = head.next\n    return head\n
        linked_list.cpp
        /* Доступ к узлу связного списка по индексу index */\nListNode *access(ListNode *head, int index) {\n    for (int i = 0; i < index; i++) {\n        if (head == nullptr)\n            return nullptr;\n        head = head->next;\n    }\n    return head;\n}\n
        linked_list.java
        /* Доступ к узлу связного списка по индексу index */\nListNode access(ListNode head, int index) {\n    for (int i = 0; i < index; i++) {\n        if (head == null)\n            return null;\n        head = head.next;\n    }\n    return head;\n}\n
        linked_list.cs
        /* Доступ к узлу связного списка по индексу index */\nListNode? Access(ListNode? head, int index) {\n    for (int i = 0; i < index; i++) {\n        if (head == null)\n            return null;\n        head = head.next;\n    }\n    return head;\n}\n
        linked_list.go
        /* Доступ к узлу связного списка по индексу index */\nfunc access(head *ListNode, index int) *ListNode {\n    for i := 0; i < index; i++ {\n        if head == nil {\n            return nil\n        }\n        head = head.Next\n    }\n    return head\n}\n
        linked_list.swift
        /* Доступ к узлу связного списка по индексу index */\nfunc access(head: ListNode, index: Int) -> ListNode? {\n    var head: ListNode? = head\n    for _ in 0 ..< index {\n        if head == nil {\n            return nil\n        }\n        head = head?.next\n    }\n    return head\n}\n
        linked_list.js
        /* Доступ к узлу связного списка по индексу index */\nfunction access(head, index) {\n    for (let i = 0; i < index; i++) {\n        if (!head) {\n            return null;\n        }\n        head = head.next;\n    }\n    return head;\n}\n
        linked_list.ts
        /* Доступ к узлу связного списка по индексу index */\nfunction access(head: ListNode | null, index: number): ListNode | null {\n    for (let i = 0; i < index; i++) {\n        if (!head) {\n            return null;\n        }\n        head = head.next;\n    }\n    return head;\n}\n
        linked_list.dart
        /* Доступ к узлу связного списка по индексу index */\nListNode? access(ListNode? head, int index) {\n  for (var i = 0; i < index; i++) {\n    if (head == null) return null;\n    head = head.next;\n  }\n  return head;\n}\n
        linked_list.rs
        /* Доступ к узлу связного списка по индексу index */\npub fn access<T>(head: Rc<RefCell<ListNode<T>>>, index: i32) -> Option<Rc<RefCell<ListNode<T>>>> {\n    fn dfs<T>(\n        head: Option<&Rc<RefCell<ListNode<T>>>>,\n        index: i32,\n    ) -> Option<Rc<RefCell<ListNode<T>>>> {\n        if index <= 0 {\n            return head.cloned();\n        }\n\n        if let Some(node) = head {\n            dfs(node.borrow().next.as_ref(), index - 1)\n        } else {\n            None\n        }\n    }\n\n    dfs(Some(head).as_ref(), index)\n}\n
        linked_list.c
        /* Доступ к узлу связного списка по индексу index */\nListNode *access(ListNode *head, int index) {\n    for (int i = 0; i < index; i++) {\n        if (head == NULL)\n            return NULL;\n        head = head->next;\n    }\n    return head;\n}\n
        linked_list.kt
        /* Доступ к узлу связного списка по индексу index */\nfun access(head: ListNode?, index: Int): ListNode? {\n    var h = head\n    for (i in 0..<index) {\n        if (h == null)\n            return null\n        h = h.next\n    }\n    return h\n}\n
        linked_list.rb
        ### Доступ к узлу связного списка по индексу index ###\ndef access(head, index)\n  for i in 0...index\n    return nil if head.nil?\n    head = head.next\n  end\n\n  head\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 4. Массивы и списки","4.2   Связный список"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#5","level":3,"title":"5.   Поиск узла","text":"

        Поиск узла заключается в обходе связного списка, нахождении узла со значением target и возврате его индекса в списке. Этот процесс тоже относится к линейному поиску. Код выглядит следующим образом:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
        def find(head: ListNode, target: int) -> int:\n    \"\"\"Найти в связном списке первый узел со значением target\"\"\"\n    index = 0\n    while head:\n        if head.val == target:\n            return index\n        head = head.next\n        index += 1\n    return -1\n
        linked_list.cpp
        /* Найти в связном списке первый узел со значением target */\nint find(ListNode *head, int target) {\n    int index = 0;\n    while (head != nullptr) {\n        if (head->val == target)\n            return index;\n        head = head->next;\n        index++;\n    }\n    return -1;\n}\n
        linked_list.java
        /* Найти в связном списке первый узел со значением target */\nint find(ListNode head, int target) {\n    int index = 0;\n    while (head != null) {\n        if (head.val == target)\n            return index;\n        head = head.next;\n        index++;\n    }\n    return -1;\n}\n
        linked_list.cs
        /* Найти в связном списке первый узел со значением target */\nint Find(ListNode? head, int target) {\n    int index = 0;\n    while (head != null) {\n        if (head.val == target)\n            return index;\n        head = head.next;\n        index++;\n    }\n    return -1;\n}\n
        linked_list.go
        /* Найти в связном списке первый узел со значением target */\nfunc findNode(head *ListNode, target int) int {\n    index := 0\n    for head != nil {\n        if head.Val == target {\n            return index\n        }\n        head = head.Next\n        index++\n    }\n    return -1\n}\n
        linked_list.swift
        /* Найти в связном списке первый узел со значением target */\nfunc find(head: ListNode, target: Int) -> Int {\n    var head: ListNode? = head\n    var index = 0\n    while head != nil {\n        if head?.val == target {\n            return index\n        }\n        head = head?.next\n        index += 1\n    }\n    return -1\n}\n
        linked_list.js
        /* Найти в связном списке первый узел со значением target */\nfunction find(head, target) {\n    let index = 0;\n    while (head !== null) {\n        if (head.val === target) {\n            return index;\n        }\n        head = head.next;\n        index += 1;\n    }\n    return -1;\n}\n
        linked_list.ts
        /* Найти в связном списке первый узел со значением target */\nfunction find(head: ListNode | null, target: number): number {\n    let index = 0;\n    while (head !== null) {\n        if (head.val === target) {\n            return index;\n        }\n        head = head.next;\n        index += 1;\n    }\n    return -1;\n}\n
        linked_list.dart
        /* Найти в связном списке первый узел со значением target */\nint find(ListNode? head, int target) {\n  int index = 0;\n  while (head != null) {\n    if (head.val == target) {\n      return index;\n    }\n    head = head.next;\n    index++;\n  }\n  return -1;\n}\n
        linked_list.rs
        /* Найти в связном списке первый узел со значением target */\npub fn find<T: PartialEq>(head: Rc<RefCell<ListNode<T>>>, target: T) -> i32 {\n    fn find<T: PartialEq>(head: Option<&Rc<RefCell<ListNode<T>>>>, target: T, idx: i32) -> i32 {\n        if let Some(node) = head {\n            if node.borrow().val == target {\n                return idx;\n            }\n            return find(node.borrow().next.as_ref(), target, idx + 1);\n        } else {\n            -1\n        }\n    }\n\n    find(Some(head).as_ref(), target, 0)\n}\n
        linked_list.c
        /* Найти в связном списке первый узел со значением target */\nint find(ListNode *head, int target) {\n    int index = 0;\n    while (head) {\n        if (head->val == target)\n            return index;\n        head = head->next;\n        index++;\n    }\n    return -1;\n}\n
        linked_list.kt
        /* Найти в связном списке первый узел со значением target */\nfun find(head: ListNode?, target: Int): Int {\n    var index = 0\n    var h = head\n    while (h != null) {\n        if (h._val == target)\n            return index\n        h = h.next\n        index++\n    }\n    return -1\n}\n
        linked_list.rb
        ### Поиск первого узла со значением target в связном списке ###\ndef find(head, target)\n  index = 0\n  while head\n    return index if head.val == target\n    head = head.next\n    index += 1\n  end\n\n  -1\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 4. Массивы и списки","4.2   Связный список"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#422","level":2,"title":"4.2.2   Сравнение массива и связного списка","text":"

        В таблице 4-1 обобщаются свойства массива и связного списка, а также сравнивается эффективность соответствующих операций. Поскольку они используют противоположные стратегии хранения, их свойства и эффективность операций тоже во многом противоположны.

        Таблица 4-1   Сравнение эффективности массива и связного списка

        Массив Связный список Способ хранения Непрерывная область памяти Разрозненная область памяти Расширение емкости Длина неизменяема Гибкое расширение Эффективность памяти Элементы занимают меньше памяти, но возможны потери пространства Элементы занимают больше памяти Доступ к элементу \\(O(1)\\) \\(O(n)\\) Добавление элемента \\(O(n)\\) \\(O(1)\\) Удаление элемента \\(O(n)\\) \\(O(1)\\)","path":["Глава 4. Массивы и списки","4.2   Связный список"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#423","level":2,"title":"4.2.3   Основные типы связных списков","text":"

        Как показано на рисунке 4-8, существует три распространенных типа связных списков.

        • Односвязный список: это обычный связный список, рассмотренный выше. Узел односвязного списка содержит значение и ссылку на следующий узел. Первый узел называется головным, последний - хвостовым, и хвост указывает на None .
        • Циклический список: если заставить хвостовой узел односвязного списка указывать на головной, то есть соединить хвост с головой, получится циклический список. В циклическом списке любой узел можно рассматривать как головной.
        • Двусвязный список: по сравнению с односвязным списком двусвязный хранит ссылки в двух направлениях. Определение узла двусвязного списка включает как ссылку на следующий узел, так и ссылку на предыдущий узел. По сравнению с односвязным списком двусвязный более гибок и позволяет обходить список в обе стороны, но за это приходится платить дополнительной памятью.
        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
        class ListNode:\n    \"\"\"Класс узла двусвязного списка\"\"\"\n    def __init__(self, val: int):\n        self.val: int = val                # Значение узла\n        self.next: ListNode | None = None  # Ссылка на следующий узел\n        self.prev: ListNode | None = None  # Ссылка на предыдущий узел\n
        /* Структура узла двусвязного списка */\nstruct ListNode {\n    int val;         // Значение узла\n    ListNode *next;  // Указатель на следующий узел\n    ListNode *prev;  // Указатель на предыдущий узел\n    ListNode(int x) : val(x), next(nullptr), prev(nullptr) {}  // Конструктор\n};\n
        /* Класс узла двусвязного списка */\nclass ListNode {\n    int val;        // Значение узла\n    ListNode next;  // Ссылка на следующий узел\n    ListNode prev;  // Ссылка на предыдущий узел\n    ListNode(int x) { val = x; }  // Конструктор\n}\n
        /* Класс узла двусвязного списка */\nclass ListNode(int x) {  // Конструктор\n    int val = x;    // Значение узла\n    ListNode next;  // Ссылка на следующий узел\n    ListNode prev;  // Ссылка на предыдущий узел\n}\n
        /* Структура узла двусвязного списка */\ntype DoublyListNode struct {\n    Val  int             // Значение узла\n    Next *DoublyListNode // Указатель на следующий узел\n    Prev *DoublyListNode // Указатель на предыдущий узел\n}\n\n// NewDoublyListNode Инициализация\nfunc NewDoublyListNode(val int) *DoublyListNode {\n    return &DoublyListNode{\n        Val:  val,\n        Next: nil,\n        Prev: nil,\n    }\n}\n
        /* Класс узла двусвязного списка */\nclass ListNode {\n    var val: Int // Значение узла\n    var next: ListNode? // Ссылка на следующий узел\n    var prev: ListNode? // Ссылка на предыдущий узел\n\n    init(x: Int) { // Конструктор\n        val = x\n    }\n}\n
        /* Класс узла двусвязного списка */\nclass ListNode {\n    constructor(val, next, prev) {\n        this.val = val  ===  undefined ? 0 : val;        // Значение узла\n        this.next = next  ===  undefined ? null : next;  // Ссылка на следующий узел\n        this.prev = prev  ===  undefined ? null : prev;  // Ссылка на предыдущий узел\n    }\n}\n
        /* Класс узла двусвязного списка */\nclass ListNode {\n    val: number;\n    next: ListNode | null;\n    prev: ListNode | null;\n    constructor(val?: number, next?: ListNode | null, prev?: ListNode | null) {\n        this.val = val  ===  undefined ? 0 : val;        // Значение узла\n        this.next = next  ===  undefined ? null : next;  // Ссылка на следующий узел\n        this.prev = prev  ===  undefined ? null : prev;  // Ссылка на предыдущий узел\n    }\n}\n
        /* Класс узла двусвязного списка */\nclass ListNode {\n    int val;        // Значение узла\n    ListNode? next;  // Ссылка на следующий узел\n    ListNode? prev;  // Ссылка на предыдущий узел\n    ListNode(this.val, [this.next, this.prev]);  // Конструктор\n}\n
        use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* Тип узла двусвязного списка */\n#[derive(Debug)]\nstruct ListNode {\n    val: i32, // Значение узла\n    next: Option<Rc<RefCell<ListNode>>>, // Указатель на следующий узел\n    prev: Option<Rc<RefCell<ListNode>>>, // Указатель на предыдущий узел\n}\n\n/* Конструктор */\nimpl ListNode {\n    fn new(val: i32) -> Self {\n        ListNode {\n            val,\n            next: None,\n            prev: None,\n        }\n    }\n}\n
        /* Структура узла двусвязного списка */\ntypedef struct ListNode {\n    int val;               // Значение узла\n    struct ListNode *next; // Указатель на следующий узел\n    struct ListNode *prev; // Указатель на предыдущий узел\n} ListNode;\n\n/* Конструктор */\nListNode *newListNode(int val) {\n    ListNode *node;\n    node = (ListNode *) malloc(sizeof(ListNode));\n    node->val = val;\n    node->next = NULL;\n    node->prev = NULL;\n    return node;\n}\n
        /* Класс узла двусвязного списка */\n// Конструктор\nclass ListNode(x: Int) {\n    val _val: Int = x           // Значение узла\n    val next: ListNode? = null  // Ссылка на следующий узел\n    val prev: ListNode? = null  // Ссылка на предыдущий узел\n}\n
        # Класс узла двусвязного списка\nclass ListNode\n  attr_accessor :val    # Значение узла\n  attr_accessor :next   # Ссылка на следующий узел\n  attr_accessor :prev   # Ссылка на предыдущий узел\n\n  def initialize(val=0, next_node=nil, prev_node=nil)\n    @val = val\n    @next = next_node\n    @prev = prev_node\n  end\nend\n

        Рисунок 4-8   Распространенные типы связных списков

        ","path":["Глава 4. Массивы и списки","4.2   Связный список"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#424","level":2,"title":"4.2.4   Типичные применения связных списков","text":"

        Односвязные списки обычно используются для реализации стеков, очередей, хеш-таблиц и графов.

        • Стеки и очереди: если операции вставки и удаления выполняются на одном конце связного списка, он проявляет свойства LIFO, соответствующие стеку; если вставка происходит на одном конце, а удаление на другом, он проявляет свойства FIFO, соответствующие очереди.
        • Хеш-таблицы: метод цепочек - один из основных способов разрешения коллизий в хеш-таблицах. В этом подходе все конфликтующие элементы помещаются в связный список.
        • Графы: список смежности - это распространенный способ представления графа, при котором каждой вершине графа соответствует связный список, а каждый элемент этого списка представляет другую вершину, соединенную с данной.

        Двусвязные списки обычно используются там, где нужен быстрый доступ как к предыдущему, так и к следующему элементу.

        • Продвинутые структуры данных: например, в красно-черных деревьях и B-деревьях нам нужен доступ к родительскому узлу; этого можно добиться, сохранив в узле ссылку на родителя, по аналогии с двусвязным списком.
        • История браузера: когда пользователь в браузере нажимает кнопки \"вперед\" или \"назад\", браузеру нужно знать предыдущую и следующую посещенные страницы. Свойства двусвязного списка делают такую операцию простой.
        • Алгоритм LRU: в алгоритмах вытеснения из кэша (LRU) нужно быстро находить наименее недавно использованные данные, а также быстро добавлять и удалять узлы. Для этого двусвязный список подходит очень хорошо.

        Циклические списки часто применяются в сценариях, требующих циклических операций, например при планировании ресурсов в операционной системе.

        • Алгоритм циклического распределения кванта времени: в операционных системах round-robin scheduling - это распространенный алгоритм планирования CPU, который циклически обходит набор процессов. Каждому процессу выделяется квант времени, и когда он исчерпан, CPU переключается на следующий процесс. Такую циклическую операцию удобно реализовать с помощью кольцевого списка.
        • Буферы данных: в некоторых реализациях буферов данных также могут использоваться циклические списки. Например, в аудио- и видеоплеерах поток данных может делиться на несколько буферных блоков и помещаться в кольцевой список для обеспечения непрерывного воспроизведения.
        ","path":["Глава 4. Массивы и списки","4.2   Связный список"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/","level":1,"title":"4.3   Список","text":"

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

        • Связный список естественным образом можно рассматривать как список: он поддерживает операции добавления, удаления, поиска и изменения элементов и может гибко расширяться динамически.
        • Массив тоже поддерживает операции добавления, удаления, поиска и изменения элементов, но из-за неизменяемости длины его можно считать лишь списком с ограниченной длиной.

        Когда список реализуется с помощью массива, неизменяемость длины снижает его практическую полезность. Причина в том, что мы обычно не можем заранее точно знать, сколько данных нужно хранить, а значит, трудно выбрать подходящую длину списка. Если длина слишком мала, она может не покрыть реальные потребности; если слишком велика, будет зря расходоваться память.

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

        На практике списки из стандартных библиотек многих языков программирования реализованы именно на основе динамических массивов, например list в Python, ArrayList в Java, vector в C++ и List в C#. В дальнейшем обсуждении мы будем считать понятия \"список\" и \"динамический массив\" эквивалентными.

        ","path":["Глава 4. Массивы и списки","4.3   Список"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#431","level":2,"title":"4.3.1   Основные операции со списком","text":"","path":["Глава 4. Массивы и списки","4.3   Список"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#1","level":3,"title":"1.   Инициализация списка","text":"

        Обычно используются два способа инициализации: без начальных значений и с начальными значениями:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
        # Инициализация списка\n# Без начальных значений\nnums1: list[int] = []\n# С начальными значениями\nnums: list[int] = [1, 3, 2, 5, 4]\n
        list.cpp
        /* Инициализация списка */\n// Обрати внимание: в C++ vector соответствует описываемому здесь nums\n// Без начальных значений\nvector<int> nums1;\n// С начальными значениями\nvector<int> nums = { 1, 3, 2, 5, 4 };\n
        list.java
        /* Инициализация списка */\n// Без начальных значений\nList<Integer> nums1 = new ArrayList<>();\n// С начальными значениями (обрати внимание: элементы массива должны использовать обертку Integer[] вместо int[])\nInteger[] numbers = new Integer[] { 1, 3, 2, 5, 4 };\nList<Integer> nums = new ArrayList<>(Arrays.asList(numbers));\n
        list.cs
        /* Инициализация списка */\n// Без начальных значений\nList<int> nums1 = [];\n// С начальными значениями\nint[] numbers = [1, 3, 2, 5, 4];\nList<int> nums = [.. numbers];\n
        list_test.go
        /* Инициализация списка */\n// Без начальных значений\nnums1 := []int{}\n// С начальными значениями\nnums := []int{1, 3, 2, 5, 4}\n
        list.swift
        /* Инициализация списка */\n// Без начальных значений\nlet nums1: [Int] = []\n// С начальными значениями\nvar nums = [1, 3, 2, 5, 4]\n
        list.js
        /* Инициализация списка */\n// Без начальных значений\nconst nums1 = [];\n// С начальными значениями\nconst nums = [1, 3, 2, 5, 4];\n
        list.ts
        /* Инициализация списка */\n// Без начальных значений\nconst nums1: number[] = [];\n// С начальными значениями\nconst nums: number[] = [1, 3, 2, 5, 4];\n
        list.dart
        /* Инициализация списка */\n// Без начальных значений\nList<int> nums1 = [];\n// С начальными значениями\nList<int> nums = [1, 3, 2, 5, 4];\n
        list.rs
        /* Инициализация списка */\n// Без начальных значений\nlet nums1: Vec<i32> = Vec::new();\n// С начальными значениями\nlet nums: Vec<i32> = vec![1, 3, 2, 5, 4];\n
        list.c
        // В C нет встроенного динамического массива\n
        list.kt
        /* Инициализация списка */\n// Без начальных значений\nvar nums1 = listOf<Int>()\n// С начальными значениями\nvar numbers = arrayOf(1, 3, 2, 5, 4)\nvar nums = numbers.toMutableList()\n
        list.rb
        # Инициализация списка\n# Без начальных значений\nnums1 = []\n# С начальными значениями\nnums = [1, 3, 2, 5, 4]\n
        Визуализация выполнения

        https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%0A%20%20%20%20%23%20%D0%91%D0%B5%D0%B7%20%D0%BD%D0%B0%D1%87%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D1%85%20%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B9%0A%20%20%20%20nums1%20%3D%20%5B%5D%0A%20%20%20%20%23%20%D0%A1%20%D0%BD%D0%B0%D1%87%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%BC%D0%B8%20%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D1%8F%D0%BC%D0%B8%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%202%2C%205%2C%204%5D&cumulative=false&curInstr=4&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

        ","path":["Глава 4. Массивы и списки","4.3   Список"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#2","level":3,"title":"2.   Доступ к элементам","text":"

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

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
        # Доступ к элементу\nnum: int = nums[1]  # Доступ к элементу по индексу 1\n\n# Обновление элемента\nnums[1] = 0    # Обновить элемент по индексу 1 значением 0\n
        list.cpp
        /* Доступ к элементу */\nint num = nums[1];  // Доступ к элементу по индексу 1\n\n/* Обновление элемента */\nnums[1] = 0;  // Обновить элемент по индексу 1 значением 0\n
        list.java
        /* Доступ к элементу */\nint num = nums.get(1);  // Доступ к элементу по индексу 1\n\n/* Обновление элемента */\nnums.set(1, 0);  // Обновить элемент по индексу 1 значением 0\n
        list.cs
        /* Доступ к элементу */\nint num = nums[1];  // Доступ к элементу по индексу 1\n\n/* Обновление элемента */\nnums[1] = 0;  // Обновить элемент по индексу 1 значением 0\n
        list_test.go
        /* Доступ к элементу */\nnum := nums[1]  // Доступ к элементу по индексу 1\n\n/* Обновление элемента */\nnums[1] = 0     // Обновить элемент по индексу 1 значением 0\n
        list.swift
        /* Доступ к элементу */\nlet num = nums[1] // Доступ к элементу по индексу 1\n\n/* Обновление элемента */\nnums[1] = 0 // Обновить элемент по индексу 1 значением 0\n
        list.js
        /* Доступ к элементу */\nconst num = nums[1];  // Доступ к элементу по индексу 1\n\n/* Обновление элемента */\nnums[1] = 0;  // Обновить элемент по индексу 1 значением 0\n
        list.ts
        /* Доступ к элементу */\nconst num: number = nums[1];  // Доступ к элементу по индексу 1\n\n/* Обновление элемента */\nnums[1] = 0;  // Обновить элемент по индексу 1 значением 0\n
        list.dart
        /* Доступ к элементу */\nint num = nums[1];  // Доступ к элементу по индексу 1\n\n/* Обновление элемента */\nnums[1] = 0;  // Обновить элемент по индексу 1 значением 0\n
        list.rs
        /* Доступ к элементу */\nlet num: i32 = nums[1];  // Доступ к элементу по индексу 1\n/* Обновление элемента */\nnums[1] = 0;             // Обновить элемент по индексу 1 значением 0\n
        list.c
        // В C нет встроенного динамического массива\n
        list.kt
        /* Доступ к элементу */\nval num = nums[1]       // Доступ к элементу по индексу 1\n/* Обновление элемента */\nnums[1] = 0             // Обновить элемент по индексу 1 значением 0\n
        list.rb
        # Доступ к элементу\nnum = nums[1] # Доступ к элементу по индексу 1\n# Обновление элемента\nnums[1] = 0 # Обновить элемент по индексу 1 значением 0\n
        Визуализация выполнения

        https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%202%2C%205%2C%204%5D%0A%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B8%D1%82%D1%8C%20%D0%B4%D0%BE%D1%81%D1%82%D1%83%D0%BF%20%D0%BA%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D1%83%0A%20%20%20%20num%20%3D%20nums%5B1%5D%20%20%23%20%D0%BE%D0%B1%D1%80%D0%B0%D1%82%D0%B8%D1%82%D1%8C%D1%81%D1%8F%20%D0%BA%D0%B8%D0%BD%D0%B4%D0%B5%D0%BA%D1%81%201%20%D0%BF%D0%BE%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%0A%0A%20%20%20%20%23%20%D0%9E%D0%B1%D0%BD%D0%BE%D0%B2%D0%B8%D1%82%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%0A%20%20%20%20nums%5B1%5D%20%3D%200%20%20%20%20%23%20%D0%9E%D0%B1%D0%BD%D0%BE%D0%B2%D0%B8%D1%82%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%BF%D0%BE%20%D0%B8%D0%BD%D0%B4%D0%B5%D0%BA%D1%81%D1%83%201%20%D0%B4%D0%BE%200&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

        ","path":["Глава 4. Массивы и списки","4.3   Список"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#3","level":3,"title":"3.   Вставка и удаление элементов","text":"

        В отличие от массива список позволяет свободно добавлять и удалять элементы. Добавление элемента в конец списка имеет временную сложность \\(O(1)\\) , но операции вставки и удаления по-прежнему имеют ту же эффективность, что и у массива, то есть \\(O(n)\\) .

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
        # Очистить список\nnums.clear()\n\n# Добавить элементы в конец\nnums.append(1)\nnums.append(3)\nnums.append(2)\nnums.append(5)\nnums.append(4)\n\n# Вставить элемент в середину\nnums.insert(3, 6)  # Вставить число 6 по индексу 3\n\n# Удалить элемент\nnums.pop(3)        # Удалить элемент по индексу 3\n
        list.cpp
        /* Очистить список */\nnums.clear();\n\n/* Добавить элементы в конец */\nnums.push_back(1);\nnums.push_back(3);\nnums.push_back(2);\nnums.push_back(5);\nnums.push_back(4);\n\n/* Вставить элемент в середину */\nnums.insert(nums.begin() + 3, 6);  // Вставить число 6 по индексу 3\n\n/* Удалить элемент */\nnums.erase(nums.begin() + 3);      // Удалить элемент по индексу 3\n
        list.java
        /* Очистить список */\nnums.clear();\n\n/* Добавить элементы в конец */\nnums.add(1);\nnums.add(3);\nnums.add(2);\nnums.add(5);\nnums.add(4);\n\n/* Вставить элемент в середину */\nnums.add(3, 6);  // Вставить число 6 по индексу 3\n\n/* Удалить элемент */\nnums.remove(3);  // Удалить элемент по индексу 3\n
        list.cs
        /* Очистить список */\nnums.Clear();\n\n/* Добавить элементы в конец */\nnums.Add(1);\nnums.Add(3);\nnums.Add(2);\nnums.Add(5);\nnums.Add(4);\n\n/* Вставить элемент в середину */\nnums.Insert(3, 6);  // Вставить число 6 по индексу 3\n\n/* Удалить элемент */\nnums.RemoveAt(3);  // Удалить элемент по индексу 3\n
        list_test.go
        /* Очистить список */\nnums = nil\n\n/* Добавить элементы в конец */\nnums = append(nums, 1)\nnums = append(nums, 3)\nnums = append(nums, 2)\nnums = append(nums, 5)\nnums = append(nums, 4)\n\n/* Вставить элемент в середину */\nnums = append(nums[:3], append([]int{6}, nums[3:]...)...) // Вставить число 6 по индексу 3\n\n/* Удалить элемент */\nnums = append(nums[:3], nums[4:]...) // Удалить элемент по индексу 3\n
        list.swift
        /* Очистить список */\nnums.removeAll()\n\n/* Добавить элементы в конец */\nnums.append(1)\nnums.append(3)\nnums.append(2)\nnums.append(5)\nnums.append(4)\n\n/* Вставить элемент в середину */\nnums.insert(6, at: 3) // Вставить число 6 по индексу 3\n\n/* Удалить элемент */\nnums.remove(at: 3) // Удалить элемент по индексу 3\n
        list.js
        /* Очистить список */\nnums.length = 0;\n\n/* Добавить элементы в конец */\nnums.push(1);\nnums.push(3);\nnums.push(2);\nnums.push(5);\nnums.push(4);\n\n/* Вставить элемент в середину */\nnums.splice(3, 0, 6); // Вставить число 6 по индексу 3\n\n/* Удалить элемент */\nnums.splice(3, 1);  // Удалить элемент по индексу 3\n
        list.ts
        /* Очистить список */\nnums.length = 0;\n\n/* Добавить элементы в конец */\nnums.push(1);\nnums.push(3);\nnums.push(2);\nnums.push(5);\nnums.push(4);\n\n/* Вставить элемент в середину */\nnums.splice(3, 0, 6); // Вставить число 6 по индексу 3\n\n/* Удалить элемент */\nnums.splice(3, 1);  // Удалить элемент по индексу 3\n
        list.dart
        /* Очистить список */\nnums.clear();\n\n/* Добавить элементы в конец */\nnums.add(1);\nnums.add(3);\nnums.add(2);\nnums.add(5);\nnums.add(4);\n\n/* Вставить элемент в середину */\nnums.insert(3, 6); // Вставить число 6 по индексу 3\n\n/* Удалить элемент */\nnums.removeAt(3); // Удалить элемент по индексу 3\n
        list.rs
        /* Очистить список */\nnums.clear();\n\n/* Добавить элементы в конец */\nnums.push(1);\nnums.push(3);\nnums.push(2);\nnums.push(5);\nnums.push(4);\n\n/* Вставить элемент в середину */\nnums.insert(3, 6);  // Вставить число 6 по индексу 3\n\n/* Удалить элемент */\nnums.remove(3);    // Удалить элемент по индексу 3\n
        list.c
        // В C нет встроенного динамического массива\n
        list.kt
        /* Очистить список */\nnums.clear();\n\n/* Добавить элементы в конец */\nnums.add(1);\nnums.add(3);\nnums.add(2);\nnums.add(5);\nnums.add(4);\n\n/* Вставить элемент в середину */\nnums.add(3, 6);  // Вставить число 6 по индексу 3\n\n/* Удалить элемент */\nnums.remove(3);  // Удалить элемент по индексу 3\n
        list.rb
        # Очистить список\nnums.clear\n\n# Добавить элементы в конец\nnums << 1\nnums << 3\nnums << 2\nnums << 5\nnums << 4\n\n# Вставить элемент в середину\nnums.insert(3, 6) # Вставить число 6 по индексу 3\n\n# Удалить элемент\nnums.delete_at(3) # Удалить элемент по индексу 3\n
        Визуализация выполнения

        https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%A1%20%D0%BD%D0%B0%D1%87%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%BC%D0%B8%20%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D1%8F%D0%BC%D0%B8%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%202%2C%205%2C%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%9E%D1%87%D0%B8%D1%81%D1%82%D0%B8%D1%82%D1%8C%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%0A%20%20%20%20nums.clear%28%29%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%94%D0%BE%D0%B1%D0%B0%D0%B2%D0%B8%D1%82%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B2%20%D0%BA%D0%BE%D0%BD%D0%B5%D1%86%0A%20%20%20%20nums.append%281%29%0A%20%20%20%20nums.append%283%29%0A%20%20%20%20nums.append%282%29%0A%20%20%20%20nums.append%285%29%0A%20%20%20%20nums.append%284%29%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%92%D1%81%D1%82%D0%B0%D0%B2%D0%B8%D1%82%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B2%20%D1%81%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%D0%BD%D1%83%0A%20%20%20%20nums.insert%283%2C%206%29%20%20%23%20%D0%92%D1%81%D1%82%D0%B0%D0%B2%D0%B8%D1%82%D1%8C%20%D1%87%D0%B8%D1%81%D0%BB%D0%BE%206%20%D0%BF%D0%BE%20%D0%B8%D0%BD%D0%B4%D0%B5%D0%BA%D1%81%D1%83%203%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%A3%D0%B4%D0%B0%D0%BB%D0%B8%D1%82%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%0A%20%20%20%20nums.pop%283%29%20%20%20%20%20%20%20%20%23%20%D0%A3%D0%B4%D0%B0%D0%BB%D0%B8%D1%82%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%BF%D0%BE%20%D0%B8%D0%BD%D0%B4%D0%B5%D0%BA%D1%81%D1%83%203&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

        ","path":["Глава 4. Массивы и списки","4.3   Список"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#4","level":3,"title":"4.   Обход списка","text":"

        Как и массив, список можно обходить как по индексам, так и напрямую по элементам.

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
        # Обход списка по индексам\ncount = 0\nfor i in range(len(nums)):\n    count += nums[i]\n\n# Прямой обход элементов списка\nfor num in nums:\n    count += num\n
        list.cpp
        /* Обход списка по индексам */\nint count = 0;\nfor (int i = 0; i < nums.size(); i++) {\n    count += nums[i];\n}\n\n/* Прямой обход элементов списка */\ncount = 0;\nfor (int num : nums) {\n    count += num;\n}\n
        list.java
        /* Обход списка по индексам */\nint count = 0;\nfor (int i = 0; i < nums.size(); i++) {\n    count += nums.get(i);\n}\n\n/* Прямой обход элементов списка */\nfor (int num : nums) {\n    count += num;\n}\n
        list.cs
        /* Обход списка по индексам */\nint count = 0;\nfor (int i = 0; i < nums.Count; i++) {\n    count += nums[i];\n}\n\n/* Прямой обход элементов списка */\ncount = 0;\nforeach (int num in nums) {\n    count += num;\n}\n
        list_test.go
        /* Обход списка по индексам */\ncount := 0\nfor i := 0; i < len(nums); i++ {\n    count += nums[i]\n}\n\n/* Прямой обход элементов списка */\ncount = 0\nfor _, num := range nums {\n    count += num\n}\n
        list.swift
        /* Обход списка по индексам */\nvar count = 0\nfor i in nums.indices {\n    count += nums[i]\n}\n\n/* Прямой обход элементов списка */\ncount = 0\nfor num in nums {\n    count += num\n}\n
        list.js
        /* Обход списка по индексам */\nlet count = 0;\nfor (let i = 0; i < nums.length; i++) {\n    count += nums[i];\n}\n\n/* Прямой обход элементов списка */\ncount = 0;\nfor (const num of nums) {\n    count += num;\n}\n
        list.ts
        /* Обход списка по индексам */\nlet count = 0;\nfor (let i = 0; i < nums.length; i++) {\n    count += nums[i];\n}\n\n/* Прямой обход элементов списка */\ncount = 0;\nfor (const num of nums) {\n    count += num;\n}\n
        list.dart
        /* Обход списка по индексам */\nint count = 0;\nfor (var i = 0; i < nums.length; i++) {\n    count += nums[i];\n}\n\n/* Прямой обход элементов списка */\ncount = 0;\nfor (var num in nums) {\n    count += num;\n}\n
        list.rs
        // Обход списка по индексам\nlet mut _count = 0;\nfor i in 0..nums.len() {\n    _count += nums[i];\n}\n\n// Прямой обход элементов списка\n_count = 0;\nfor num in &nums {\n    _count += num;\n}\n
        list.c
        // В C нет встроенного динамического массива\n
        list.kt
        /* Обход списка по индексам */\nvar count = 0\nfor (i in nums.indices) {\n    count += nums[i]\n}\n\n/* Прямой обход элементов списка */\nfor (num in nums) {\n    count += num\n}\n
        list.rb
        # Обход списка по индексам\ncount = 0\nfor i in 0...nums.length\n    count += nums[i]\nend\n\n# Прямой обход элементов списка\ncount = 0\nfor num in nums\n    count += num\nend\n
        Визуализация выполнения

        https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%202%2C%205%2C%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%9E%D0%B1%D1%85%D0%BE%D0%B4%D0%B8%D1%82%D1%8C%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%20%D0%BF%D0%BE%20%D0%B8%D0%BD%D0%B4%D0%B5%D0%BA%D1%81%D0%B0%D0%BC%0A%20%20%20%20count%20%3D%200%0A%20%20%20%20for%20i%20in%20range%28len%28nums%29%29%3A%0A%20%20%20%20%20%20%20%20count%20%2B%3D%20nums%5Bi%5D%0A%0A%20%20%20%20%23%20%D0%9D%D0%B5%D0%BF%D0%BE%D1%81%D1%80%D0%B5%D0%B4%D1%81%D1%82%D0%B2%D0%B5%D0%BD%D0%BD%D0%BE%20%D0%BE%D0%B1%D1%85%D0%BE%D0%B4%D0%B8%D1%82%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D1%8B%20%D1%81%D0%BF%D0%B8%D1%81%D0%BA%D0%B0%0A%20%20%20%20for%20num%20in%20nums%3A%0A%20%20%20%20%20%20%20%20count%20%2B%3D%20num&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

        ","path":["Глава 4. Массивы и списки","4.3   Список"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#5","level":3,"title":"5.   Конкатенация списков","text":"

        Создав новый список nums1 , мы можем присоединить его к хвосту исходного списка.

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
        # Конкатенация двух списков\nnums1: list[int] = [6, 8, 7, 10, 9]\nnums += nums1  # Присоединить список nums1 к концу nums\n
        list.cpp
        /* Конкатенация двух списков */\nvector<int> nums1 = { 6, 8, 7, 10, 9 };\n// Присоединить список nums1 к концу nums\nnums.insert(nums.end(), nums1.begin(), nums1.end());\n
        list.java
        /* Конкатенация двух списков */\nList<Integer> nums1 = new ArrayList<>(Arrays.asList(new Integer[] { 6, 8, 7, 10, 9 }));\nnums.addAll(nums1);  // Присоединить список nums1 к концу nums\n
        list.cs
        /* Конкатенация двух списков */\nList<int> nums1 = [6, 8, 7, 10, 9];\nnums.AddRange(nums1);  // Присоединить список nums1 к концу nums\n
        list_test.go
        /* Конкатенация двух списков */\nnums1 := []int{6, 8, 7, 10, 9}\nnums = append(nums, nums1...)  // Присоединить список nums1 к концу nums\n
        list.swift
        /* Конкатенация двух списков */\nlet nums1 = [6, 8, 7, 10, 9]\nnums.append(contentsOf: nums1) // Присоединить список nums1 к концу nums\n
        list.js
        /* Конкатенация двух списков */\nconst nums1 = [6, 8, 7, 10, 9];\nnums.push(...nums1);  // Присоединить список nums1 к концу nums\n
        list.ts
        /* Конкатенация двух списков */\nconst nums1: number[] = [6, 8, 7, 10, 9];\nnums.push(...nums1);  // Присоединить список nums1 к концу nums\n
        list.dart
        /* Конкатенация двух списков */\nList<int> nums1 = [6, 8, 7, 10, 9];\nnums.addAll(nums1);  // Присоединить список nums1 к концу nums\n
        list.rs
        /* Конкатенация двух списков */\nlet nums1: Vec<i32> = vec![6, 8, 7, 10, 9];\nnums.extend(nums1);\n
        list.c
        // В C нет встроенного динамического массива\n
        list.kt
        /* Конкатенация двух списков */\nval nums1 = intArrayOf(6, 8, 7, 10, 9).toMutableList()\nnums.addAll(nums1)  // Присоединить список nums1 к концу nums\n
        list.rb
        # Конкатенация двух списков\nnums1 = [6, 8, 7, 10, 9]\nnums += nums1\n
        Визуализация выполнения

        https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%202%2C%205%2C%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%9E%D0%B1%D1%8A%D0%B5%D0%B4%D0%B8%D0%BD%D0%B8%D1%82%D1%8C%20%D0%B4%D0%B2%D0%B0%20%D1%81%D0%BF%D0%B8%D1%81%D0%BA%D0%B0%0A%20%20%20%20nums1%20%3D%20%5B6%2C%208%2C%207%2C%2010%2C%209%5D%0A%20%20%20%20nums%20%2B%3D%20nums1%20%20%23%20%D0%9F%D1%80%D0%B8%D1%81%D0%BE%D0%B5%D0%B4%D0%B8%D0%BD%D0%B8%D1%82%D1%8C%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%20nums1%20%D0%BA%20nums&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

        ","path":["Глава 4. Массивы и списки","4.3   Список"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#6","level":3,"title":"6.   Сортировка списка","text":"

        После сортировки списка мы сможем применять алгоритмы \"двоичный поиск\" и \"два указателя\", которые очень часто встречаются в задачах по массивам.

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
        # Отсортировать список\nnums.sort()  # После сортировки элементы списка идут по возрастанию\n
        list.cpp
        /* Отсортировать список */\nsort(nums.begin(), nums.end());  // После сортировки элементы списка идут по возрастанию\n
        list.java
        /* Отсортировать список */\nCollections.sort(nums);  // После сортировки элементы списка идут по возрастанию\n
        list.cs
        /* Отсортировать список */\nnums.Sort(); // После сортировки элементы списка идут по возрастанию\n
        list_test.go
        /* Отсортировать список */\nsort.Ints(nums)  // После сортировки элементы списка идут по возрастанию\n
        list.swift
        /* Отсортировать список */\nnums.sort() // После сортировки элементы списка идут по возрастанию\n
        list.js
        /* Отсортировать список */\nnums.sort((a, b) => a - b);  // После сортировки элементы списка идут по возрастанию\n
        list.ts
        /* Отсортировать список */\nnums.sort((a, b) => a - b);  // После сортировки элементы списка идут по возрастанию\n
        list.dart
        /* Отсортировать список */\nnums.sort(); // После сортировки элементы списка идут по возрастанию\n
        list.rs
        /* Отсортировать список */\nnums.sort(); // После сортировки элементы списка идут по возрастанию\n
        list.c
        // В C нет встроенного динамического массива\n
        list.kt
        /* Отсортировать список */\nnums.sort() // После сортировки элементы списка идут по возрастанию\n
        list.rb
        # Отсортировать список\nnums = nums.sort { |a, b| a <=> b } # После сортировки элементы списка идут по возрастанию\n
        Визуализация выполнения

        https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%202%2C%205%2C%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%9E%D1%82%D1%81%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%0A%20%20%20%20nums.sort%28%29%20%20%23%20%D0%A1%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D0%B0%D0%BF%D0%BE%D1%81%D0%BB%D0%B5%2C%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D1%80%D0%B0%D1%81%D0%BF%D0%BE%D0%BB%D0%BE%D0%B6%D0%B5%D0%BD%D1%8B%20%D0%BF%D0%BE%20%D0%B2%D0%BE%D0%B7%D1%80%D0%B0%D1%81%D1%82%D0%B0%D0%BD%D0%B8%D1%8E&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

        ","path":["Глава 4. Массивы и списки","4.3   Список"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#432","level":2,"title":"4.3.2   Реализация списка","text":"

        Во многих языках программирования списки встроены в стандартную библиотеку, например в Java, C++, Python и других языках. Их реализация довольно сложна, а настройки параметров тщательно продуманы: начальная емкость, коэффициент расширения и так далее. Если это интересно, стоит заглянуть в исходный код.

        Чтобы лучше понять принцип работы списка, попробуем реализовать его упрощенную версию, в которой есть три ключевых аспекта проектирования.

        • Начальная емкость: выбрать разумную начальную емкость внутреннего массива. В этом примере мы берем 10.
        • Учет количества элементов: объявить переменную size , которая будет хранить текущее число элементов в списке и обновляться в реальном времени при вставке и удалении элементов. С помощью этой переменной можно определять конец списка и понимать, требуется ли расширение.
        • Механизм расширения: если при вставке элементов емкость списка исчерпана, нужно выполнить расширение. Для этого сначала создается больший массив с учетом коэффициента расширения, а затем все элементы текущего массива по порядку переносятся в новый. В этом примере мы считаем, что каждый раз массив расширяется в 2 раза.
        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_list.py
        class MyList:\n    \"\"\"Класс списка\"\"\"\n\n    def __init__(self):\n        \"\"\"Конструктор\"\"\"\n        self._capacity: int = 10  # Вместимость списка\n        self._arr: list[int] = [0] * self._capacity  # Массив (для хранения элементов списка)\n        self._size: int = 0  # Длина списка (текущее число элементов)\n        self._extend_ratio: int = 2  # Коэффициент увеличения списка при каждом расширении\n\n    def size(self) -> int:\n        \"\"\"Получить длину списка (текущее число элементов)\"\"\"\n        return self._size\n\n    def capacity(self) -> int:\n        \"\"\"Получить вместимость списка\"\"\"\n        return self._capacity\n\n    def get(self, index: int) -> int:\n        \"\"\"Доступ к элементу\"\"\"\n        # Если индекс выходит за границы, выбрасывается исключение; далее аналогично\n        if index < 0 or index >= self._size:\n            raise IndexError(\"индекс выходит за границы\")\n        return self._arr[index]\n\n    def set(self, num: int, index: int):\n        \"\"\"Обновление элемента\"\"\"\n        if index < 0 or index >= self._size:\n            raise IndexError(\"индекс выходит за границы\")\n        self._arr[index] = num\n\n    def add(self, num: int):\n        \"\"\"Добавление элемента в конец\"\"\"\n        # При превышении вместимости по числу элементов запускается расширение\n        if self.size() == self.capacity():\n            self.extend_capacity()\n        self._arr[self._size] = num\n        self._size += 1\n\n    def insert(self, num: int, index: int):\n        \"\"\"Вставка элемента в середину\"\"\"\n        if index < 0 or index >= self._size:\n            raise IndexError(\"индекс выходит за границы\")\n        # При превышении вместимости по числу элементов запускается расширение\n        if self._size == self.capacity():\n            self.extend_capacity()\n        # Сдвинуть элемент с индексом index и все следующие элементы на одну позицию назад\n        for j in range(self._size - 1, index - 1, -1):\n            self._arr[j + 1] = self._arr[j]\n        self._arr[index] = num\n        # Обновить число элементов\n        self._size += 1\n\n    def remove(self, index: int) -> int:\n        \"\"\"Удаление элемента\"\"\"\n        if index < 0 or index >= self._size:\n            raise IndexError(\"индекс выходит за границы\")\n        num = self._arr[index]\n        # Сдвинуть все элементы после индекса index на одну позицию вперед\n        for j in range(index, self._size - 1):\n            self._arr[j] = self._arr[j + 1]\n        # Обновить число элементов\n        self._size -= 1\n        # Вернуть удаленный элемент\n        return num\n\n    def extend_capacity(self):\n        \"\"\"Расширение списка\"\"\"\n        # Создать новый массив длиной в _extend_ratio раз больше исходного массива и скопировать в него исходный массив\n        self._arr = self._arr + [0] * self.capacity() * (self._extend_ratio - 1)\n        # Обновить вместимость списка\n        self._capacity = len(self._arr)\n\n    def to_array(self) -> list[int]:\n        \"\"\"Вернуть список фактической длины\"\"\"\n        return self._arr[: self._size]\n
        my_list.cpp
        /* Класс списка */\nclass MyList {\n  private:\n    int *arr;             // Массив (для хранения элементов списка)\n    int arrCapacity = 10; // Вместимость списка\n    int arrSize = 0;      // Длина списка (текущее число элементов)\n    int extendRatio = 2;   // Коэффициент увеличения списка при каждом расширении\n\n  public:\n    /* Конструктор */\n    MyList() {\n        arr = new int[arrCapacity];\n    }\n\n    /* Метод-деструктор */\n    ~MyList() {\n        delete[] arr;\n    }\n\n    /* Получить длину списка (текущее число элементов) */\n    int size() {\n        return arrSize;\n    }\n\n    /* Получить вместимость списка */\n    int capacity() {\n        return arrCapacity;\n    }\n\n    /* Доступ к элементу */\n    int get(int index) {\n        // Если индекс выходит за границы, выбрасывается исключение; далее аналогично\n        if (index < 0 || index >= size())\n            throw out_of_range(\"индекс выходит за границы\");\n        return arr[index];\n    }\n\n    /* Обновление элемента */\n    void set(int index, int num) {\n        if (index < 0 || index >= size())\n            throw out_of_range(\"индекс выходит за границы\");\n        arr[index] = num;\n    }\n\n    /* Добавление элемента в конец */\n    void add(int num) {\n        // При превышении вместимости по числу элементов запускается расширение\n        if (size() == capacity())\n            extendCapacity();\n        arr[size()] = num;\n        // Обновить число элементов\n        arrSize++;\n    }\n\n    /* Вставка элемента в середину */\n    void insert(int index, int num) {\n        if (index < 0 || index >= size())\n            throw out_of_range(\"индекс выходит за границы\");\n        // При превышении вместимости по числу элементов запускается расширение\n        if (size() == capacity())\n            extendCapacity();\n        // Сдвинуть элемент с индексом index и все следующие элементы на одну позицию назад\n        for (int j = size() - 1; j >= index; j--) {\n            arr[j + 1] = arr[j];\n        }\n        arr[index] = num;\n        // Обновить число элементов\n        arrSize++;\n    }\n\n    /* Удаление элемента */\n    int remove(int index) {\n        if (index < 0 || index >= size())\n            throw out_of_range(\"индекс выходит за границы\");\n        int num = arr[index];\n        // Сдвинуть все элементы после индекса index на одну позицию вперед\n        for (int j = index; j < size() - 1; j++) {\n            arr[j] = arr[j + 1];\n        }\n        // Обновить число элементов\n        arrSize--;\n        // Вернуть удаленный элемент\n        return num;\n    }\n\n    /* Расширение списка */\n    void extendCapacity() {\n        // Создать новый массив длиной в extendRatio раз больше исходного массива\n        int newCapacity = capacity() * extendRatio;\n        int *tmp = arr;\n        arr = new int[newCapacity];\n        // Скопировать все элементы исходного массива в новый массив\n        for (int i = 0; i < size(); i++) {\n            arr[i] = tmp[i];\n        }\n        // Освободить память\n        delete[] tmp;\n        arrCapacity = newCapacity;\n    }\n\n    /* Преобразовать список в Vector для вывода */\n    vector<int> toVector() {\n        // Преобразовывать только элементы списка в пределах фактической длины\n        vector<int> vec(size());\n        for (int i = 0; i < size(); i++) {\n            vec[i] = arr[i];\n        }\n        return vec;\n    }\n};\n
        my_list.java
        /* Класс списка */\nclass MyList {\n    private int[] arr; // Массив (для хранения элементов списка)\n    private int capacity = 10; // Вместимость списка\n    private int size = 0; // Длина списка (текущее число элементов)\n    private int extendRatio = 2; // Коэффициент увеличения списка при каждом расширении\n\n    /* Конструктор */\n    public MyList() {\n        arr = new int[capacity];\n    }\n\n    /* Получить длину списка (текущее число элементов) */\n    public int size() {\n        return size;\n    }\n\n    /* Получить вместимость списка */\n    public int capacity() {\n        return capacity;\n    }\n\n    /* Доступ к элементу */\n    public int get(int index) {\n        // Если индекс выходит за границы, выбрасывается исключение; далее аналогично\n        if (index < 0 || index >= size)\n            throw new IndexOutOfBoundsException(\"индекс выходит за границы\");\n        return arr[index];\n    }\n\n    /* Обновление элемента */\n    public void set(int index, int num) {\n        if (index < 0 || index >= size)\n            throw new IndexOutOfBoundsException(\"индекс выходит за границы\");\n        arr[index] = num;\n    }\n\n    /* Добавление элемента в конец */\n    public void add(int num) {\n        // При превышении вместимости по числу элементов запускается расширение\n        if (size == capacity())\n            extendCapacity();\n        arr[size] = num;\n        // Обновить число элементов\n        size++;\n    }\n\n    /* Вставка элемента в середину */\n    public void insert(int index, int num) {\n        if (index < 0 || index >= size)\n            throw new IndexOutOfBoundsException(\"индекс выходит за границы\");\n        // При превышении вместимости по числу элементов запускается расширение\n        if (size == capacity())\n            extendCapacity();\n        // Сдвинуть элемент с индексом index и все следующие элементы на одну позицию назад\n        for (int j = size - 1; j >= index; j--) {\n            arr[j + 1] = arr[j];\n        }\n        arr[index] = num;\n        // Обновить число элементов\n        size++;\n    }\n\n    /* Удаление элемента */\n    public int remove(int index) {\n        if (index < 0 || index >= size)\n            throw new IndexOutOfBoundsException(\"индекс выходит за границы\");\n        int num = arr[index];\n        // Сдвинуть все элементы после индекса index на одну позицию вперед\n        for (int j = index; j < size - 1; j++) {\n            arr[j] = arr[j + 1];\n        }\n        // Обновить число элементов\n        size--;\n        // Вернуть удаленный элемент\n        return num;\n    }\n\n    /* Расширение списка */\n    public void extendCapacity() {\n        // Создать новый массив длиной в extendRatio раз больше исходного и скопировать в него исходный массив\n        arr = Arrays.copyOf(arr, capacity() * extendRatio);\n        // Обновить вместимость списка\n        capacity = arr.length;\n    }\n\n    /* Преобразовать список в массив */\n    public int[] toArray() {\n        int size = size();\n        // Преобразовывать только элементы списка в пределах фактической длины\n        int[] arr = new int[size];\n        for (int i = 0; i < size; i++) {\n            arr[i] = get(i);\n        }\n        return arr;\n    }\n}\n
        my_list.cs
        /* Класс списка */\nclass MyList {\n    private int[] arr;           // Массив (для хранения элементов списка)\n    private int arrCapacity = 10;    // Вместимость списка\n    private int arrSize = 0;         // Длина списка (текущее число элементов)\n    private readonly int extendRatio = 2;  // Коэффициент увеличения списка при каждом расширении\n\n    /* Конструктор */\n    public MyList() {\n        arr = new int[arrCapacity];\n    }\n\n    /* Получить длину списка (текущее число элементов) */\n    public int Size() {\n        return arrSize;\n    }\n\n    /* Получить вместимость списка */\n    public int Capacity() {\n        return arrCapacity;\n    }\n\n    /* Доступ к элементу */\n    public int Get(int index) {\n        // Если индекс выходит за границы, выбрасывается исключение; далее аналогично\n        if (index < 0 || index >= arrSize)\n            throw new IndexOutOfRangeException(\"индекс выходит за границы\");\n        return arr[index];\n    }\n\n    /* Обновление элемента */\n    public void Set(int index, int num) {\n        if (index < 0 || index >= arrSize)\n            throw new IndexOutOfRangeException(\"индекс выходит за границы\");\n        arr[index] = num;\n    }\n\n    /* Добавление элемента в конец */\n    public void Add(int num) {\n        // При превышении вместимости по числу элементов запускается расширение\n        if (arrSize == arrCapacity)\n            ExtendCapacity();\n        arr[arrSize] = num;\n        // Обновить число элементов\n        arrSize++;\n    }\n\n    /* Вставка элемента в середину */\n    public void Insert(int index, int num) {\n        if (index < 0 || index >= arrSize)\n            throw new IndexOutOfRangeException(\"индекс выходит за границы\");\n        // При превышении вместимости по числу элементов запускается расширение\n        if (arrSize == arrCapacity)\n            ExtendCapacity();\n        // Сдвинуть элемент с индексом index и все следующие элементы на одну позицию назад\n        for (int j = arrSize - 1; j >= index; j--) {\n            arr[j + 1] = arr[j];\n        }\n        arr[index] = num;\n        // Обновить число элементов\n        arrSize++;\n    }\n\n    /* Удаление элемента */\n    public int Remove(int index) {\n        if (index < 0 || index >= arrSize)\n            throw new IndexOutOfRangeException(\"индекс выходит за границы\");\n        int num = arr[index];\n        // Сдвинуть все элементы после индекса index на одну позицию вперед\n        for (int j = index; j < arrSize - 1; j++) {\n            arr[j] = arr[j + 1];\n        }\n        // Обновить число элементов\n        arrSize--;\n        // Вернуть удаленный элемент\n        return num;\n    }\n\n    /* Расширение списка */\n    public void ExtendCapacity() {\n        // Создать новый массив длиной arrCapacity * extendRatio и скопировать в него исходный массив\n        Array.Resize(ref arr, arrCapacity * extendRatio);\n        // Обновить вместимость списка\n        arrCapacity = arr.Length;\n    }\n\n    /* Преобразовать список в массив */\n    public int[] ToArray() {\n        // Преобразовывать только элементы списка в пределах фактической длины\n        int[] arr = new int[arrSize];\n        for (int i = 0; i < arrSize; i++) {\n            arr[i] = Get(i);\n        }\n        return arr;\n    }\n}\n
        my_list.go
        /* Класс списка */\ntype myList struct {\n    arrCapacity int\n    arr         []int\n    arrSize     int\n    extendRatio int\n}\n\n/* Конструктор */\nfunc newMyList() *myList {\n    return &myList{\n        arrCapacity: 10,              // Вместимость списка\n        arr:         make([]int, 10), // Массив (для хранения элементов списка)\n        arrSize:     0,               // Длина списка (текущее число элементов)\n        extendRatio: 2,               // Коэффициент увеличения списка при каждом расширении\n    }\n}\n\n/* Получить длину списка (текущее число элементов) */\nfunc (l *myList) size() int {\n    return l.arrSize\n}\n\n/* Получить вместимость списка */\nfunc (l *myList) capacity() int {\n    return l.arrCapacity\n}\n\n/* Доступ к элементу */\nfunc (l *myList) get(index int) int {\n    // Если индекс выходит за границы, выбрасывается исключение; далее аналогично\n    if index < 0 || index >= l.arrSize {\n        panic(\"индекс выходит за границы\")\n    }\n    return l.arr[index]\n}\n\n/* Обновление элемента */\nfunc (l *myList) set(num, index int) {\n    if index < 0 || index >= l.arrSize {\n        panic(\"индекс выходит за границы\")\n    }\n    l.arr[index] = num\n}\n\n/* Добавление элемента в конец */\nfunc (l *myList) add(num int) {\n    // При превышении вместимости по числу элементов запускается расширение\n    if l.arrSize == l.arrCapacity {\n        l.extendCapacity()\n    }\n    l.arr[l.arrSize] = num\n    // Обновить число элементов\n    l.arrSize++\n}\n\n/* Вставка элемента в середину */\nfunc (l *myList) insert(num, index int) {\n    if index < 0 || index >= l.arrSize {\n        panic(\"индекс выходит за границы\")\n    }\n    // При превышении вместимости по числу элементов запускается расширение\n    if l.arrSize == l.arrCapacity {\n        l.extendCapacity()\n    }\n    // Сдвинуть элемент с индексом index и все следующие элементы на одну позицию назад\n    for j := l.arrSize - 1; j >= index; j-- {\n        l.arr[j+1] = l.arr[j]\n    }\n    l.arr[index] = num\n    // Обновить число элементов\n    l.arrSize++\n}\n\n/* Удаление элемента */\nfunc (l *myList) remove(index int) int {\n    if index < 0 || index >= l.arrSize {\n        panic(\"индекс выходит за границы\")\n    }\n    num := l.arr[index]\n    // Сдвинуть все элементы после индекса index на одну позицию вперед\n    for j := index; j < l.arrSize-1; j++ {\n        l.arr[j] = l.arr[j+1]\n    }\n    // Обновить число элементов\n    l.arrSize--\n    // Вернуть удаленный элемент\n    return num\n}\n\n/* Расширение списка */\nfunc (l *myList) extendCapacity() {\n    // Создать новый массив длиной в extendRatio раз больше исходного и скопировать в него исходный массив\n    l.arr = append(l.arr, make([]int, l.arrCapacity*(l.extendRatio-1))...)\n    // Обновить вместимость списка\n    l.arrCapacity = len(l.arr)\n}\n\n/* Вернуть список фактической длины */\nfunc (l *myList) toArray() []int {\n    // Преобразовывать только элементы списка в пределах фактической длины\n    return l.arr[:l.arrSize]\n}\n
        my_list.swift
        /* Класс списка */\nclass MyList {\n    private var arr: [Int] // Массив (для хранения элементов списка)\n    private var _capacity: Int // Вместимость списка\n    private var _size: Int // Длина списка (текущее число элементов)\n    private let extendRatio: Int // Коэффициент увеличения списка при каждом расширении\n\n    /* Конструктор */\n    init() {\n        _capacity = 10\n        _size = 0\n        extendRatio = 2\n        arr = Array(repeating: 0, count: _capacity)\n    }\n\n    /* Получить длину списка (текущее число элементов) */\n    func size() -> Int {\n        _size\n    }\n\n    /* Получить вместимость списка */\n    func capacity() -> Int {\n        _capacity\n    }\n\n    /* Доступ к элементу */\n    func get(index: Int) -> Int {\n        // Если индекс выходит за границы, выбросить ошибку; далее аналогично\n        if index < 0 || index >= size() {\n            fatalError(\"индекс выходит за границы\")\n        }\n        return arr[index]\n    }\n\n    /* Обновление элемента */\n    func set(index: Int, num: Int) {\n        if index < 0 || index >= size() {\n            fatalError(\"индекс выходит за границы\")\n        }\n        arr[index] = num\n    }\n\n    /* Добавление элемента в конец */\n    func add(num: Int) {\n        // При превышении вместимости по числу элементов запускается расширение\n        if size() == capacity() {\n            extendCapacity()\n        }\n        arr[size()] = num\n        // Обновить число элементов\n        _size += 1\n    }\n\n    /* Вставка элемента в середину */\n    func insert(index: Int, num: Int) {\n        if index < 0 || index >= size() {\n            fatalError(\"индекс выходит за границы\")\n        }\n        // При превышении вместимости по числу элементов запускается расширение\n        if size() == capacity() {\n            extendCapacity()\n        }\n        // Сдвинуть элемент с индексом index и все следующие элементы на одну позицию назад\n        for j in (index ..< size()).reversed() {\n            arr[j + 1] = arr[j]\n        }\n        arr[index] = num\n        // Обновить число элементов\n        _size += 1\n    }\n\n    /* Удаление элемента */\n    @discardableResult\n    func remove(index: Int) -> Int {\n        if index < 0 || index >= size() {\n            fatalError(\"индекс выходит за границы\")\n        }\n        let num = arr[index]\n        // Сдвинуть все элементы после индекса index на одну позицию вперед\n        for j in index ..< (size() - 1) {\n            arr[j] = arr[j + 1]\n        }\n        // Обновить число элементов\n        _size -= 1\n        // Вернуть удаленный элемент\n        return num\n    }\n\n    /* Расширение списка */\n    func extendCapacity() {\n        // Создать новый массив длиной в extendRatio раз больше исходного и скопировать в него исходный массив\n        arr = arr + Array(repeating: 0, count: capacity() * (extendRatio - 1))\n        // Обновить вместимость списка\n        _capacity = arr.count\n    }\n\n    /* Преобразовать список в массив */\n    func toArray() -> [Int] {\n        Array(arr.prefix(size()))\n    }\n}\n
        my_list.js
        /* Класс списка */\nclass MyList {\n    #arr = new Array(); // Массив (для хранения элементов списка)\n    #capacity = 10; // Вместимость списка\n    #size = 0; // Длина списка (текущее число элементов)\n    #extendRatio = 2; // Коэффициент увеличения списка при каждом расширении\n\n    /* Конструктор */\n    constructor() {\n        this.#arr = new Array(this.#capacity);\n    }\n\n    /* Получить длину списка (текущее число элементов) */\n    size() {\n        return this.#size;\n    }\n\n    /* Получить вместимость списка */\n    capacity() {\n        return this.#capacity;\n    }\n\n    /* Доступ к элементу */\n    get(index) {\n        // Если индекс выходит за границы, выбрасывается исключение; далее аналогично\n        if (index < 0 || index >= this.#size) throw new Error('индекс выходит за границы');\n        return this.#arr[index];\n    }\n\n    /* Обновление элемента */\n    set(index, num) {\n        if (index < 0 || index >= this.#size) throw new Error('индекс выходит за границы');\n        this.#arr[index] = num;\n    }\n\n    /* Добавление элемента в конец */\n    add(num) {\n        // Если длина равна вместимости, требуется расширение\n        if (this.#size === this.#capacity) {\n            this.extendCapacity();\n        }\n        // Добавить новый элемент в конец списка\n        this.#arr[this.#size] = num;\n        this.#size++;\n    }\n\n    /* Вставка элемента в середину */\n    insert(index, num) {\n        if (index < 0 || index >= this.#size) throw new Error('индекс выходит за границы');\n        // При превышении вместимости по числу элементов запускается расширение\n        if (this.#size === this.#capacity) {\n            this.extendCapacity();\n        }\n        // Сдвинуть элемент с индексом index и все следующие элементы на одну позицию назад\n        for (let j = this.#size - 1; j >= index; j--) {\n            this.#arr[j + 1] = this.#arr[j];\n        }\n        // Обновить число элементов\n        this.#arr[index] = num;\n        this.#size++;\n    }\n\n    /* Удаление элемента */\n    remove(index) {\n        if (index < 0 || index >= this.#size) throw new Error('индекс выходит за границы');\n        let num = this.#arr[index];\n        // Сдвинуть все элементы после индекса index на одну позицию вперед\n        for (let j = index; j < this.#size - 1; j++) {\n            this.#arr[j] = this.#arr[j + 1];\n        }\n        // Обновить число элементов\n        this.#size--;\n        // Вернуть удаленный элемент\n        return num;\n    }\n\n    /* Расширение списка */\n    extendCapacity() {\n        // Создать новый массив длиной в extendRatio раз больше исходного и скопировать в него исходный массив\n        this.#arr = this.#arr.concat(\n            new Array(this.capacity() * (this.#extendRatio - 1))\n        );\n        // Обновить вместимость списка\n        this.#capacity = this.#arr.length;\n    }\n\n    /* Преобразовать список в массив */\n    toArray() {\n        let size = this.size();\n        // Преобразовывать только элементы списка в пределах фактической длины\n        const arr = new Array(size);\n        for (let i = 0; i < size; i++) {\n            arr[i] = this.get(i);\n        }\n        return arr;\n    }\n}\n
        my_list.ts
        /* Класс списка */\nclass MyList {\n    private arr: Array<number>; // Массив (для хранения элементов списка)\n    private _capacity: number = 10; // Вместимость списка\n    private _size: number = 0; // Длина списка (текущее число элементов)\n    private extendRatio: number = 2; // Коэффициент увеличения списка при каждом расширении\n\n    /* Конструктор */\n    constructor() {\n        this.arr = new Array(this._capacity);\n    }\n\n    /* Получить длину списка (текущее число элементов) */\n    public size(): number {\n        return this._size;\n    }\n\n    /* Получить вместимость списка */\n    public capacity(): number {\n        return this._capacity;\n    }\n\n    /* Доступ к элементу */\n    public get(index: number): number {\n        // Если индекс выходит за границы, выбрасывается исключение; далее аналогично\n        if (index < 0 || index >= this._size) throw new Error('индекс выходит за границы');\n        return this.arr[index];\n    }\n\n    /* Обновление элемента */\n    public set(index: number, num: number): void {\n        if (index < 0 || index >= this._size) throw new Error('индекс выходит за границы');\n        this.arr[index] = num;\n    }\n\n    /* Добавление элемента в конец */\n    public add(num: number): void {\n        // Если длина равна вместимости, требуется расширение\n        if (this._size === this._capacity) this.extendCapacity();\n        // Добавить новый элемент в конец списка\n        this.arr[this._size] = num;\n        this._size++;\n    }\n\n    /* Вставка элемента в середину */\n    public insert(index: number, num: number): void {\n        if (index < 0 || index >= this._size) throw new Error('индекс выходит за границы');\n        // При превышении вместимости по числу элементов запускается расширение\n        if (this._size === this._capacity) {\n            this.extendCapacity();\n        }\n        // Сдвинуть элемент с индексом index и все следующие элементы на одну позицию назад\n        for (let j = this._size - 1; j >= index; j--) {\n            this.arr[j + 1] = this.arr[j];\n        }\n        // Обновить число элементов\n        this.arr[index] = num;\n        this._size++;\n    }\n\n    /* Удаление элемента */\n    public remove(index: number): number {\n        if (index < 0 || index >= this._size) throw new Error('индекс выходит за границы');\n        let num = this.arr[index];\n        // Сдвинуть все элементы после индекса index на одну позицию вперед\n        for (let j = index; j < this._size - 1; j++) {\n            this.arr[j] = this.arr[j + 1];\n        }\n        // Обновить число элементов\n        this._size--;\n        // Вернуть удаленный элемент\n        return num;\n    }\n\n    /* Расширение списка */\n    public extendCapacity(): void {\n        // Создать новый массив длиной size и скопировать в него исходный массив\n        this.arr = this.arr.concat(\n            new Array(this.capacity() * (this.extendRatio - 1))\n        );\n        // Обновить вместимость списка\n        this._capacity = this.arr.length;\n    }\n\n    /* Преобразовать список в массив */\n    public toArray(): number[] {\n        let size = this.size();\n        // Преобразовывать только элементы списка в пределах фактической длины\n        const arr = new Array(size);\n        for (let i = 0; i < size; i++) {\n            arr[i] = this.get(i);\n        }\n        return arr;\n    }\n}\n
        my_list.dart
        /* Класс списка */\nclass MyList {\n  late List<int> _arr; // Массив (для хранения элементов списка)\n  int _capacity = 10; // Вместимость списка\n  int _size = 0; // Длина списка (текущее число элементов)\n  int _extendRatio = 2; // Коэффициент увеличения списка при каждом расширении\n\n  /* Конструктор */\n  MyList() {\n    _arr = List.filled(_capacity, 0);\n  }\n\n  /* Получить длину списка (текущее число элементов) */\n  int size() => _size;\n\n  /* Получить вместимость списка */\n  int capacity() => _capacity;\n\n  /* Доступ к элементу */\n  int get(int index) {\n    if (index >= _size) throw RangeError('индекс выходит за границы');\n    return _arr[index];\n  }\n\n  /* Обновление элемента */\n  void set(int index, int _num) {\n    if (index >= _size) throw RangeError('индекс выходит за границы');\n    _arr[index] = _num;\n  }\n\n  /* Добавление элемента в конец */\n  void add(int _num) {\n    // При превышении вместимости по числу элементов запускается расширение\n    if (_size == _capacity) extendCapacity();\n    _arr[_size] = _num;\n    // Обновить число элементов\n    _size++;\n  }\n\n  /* Вставка элемента в середину */\n  void insert(int index, int _num) {\n    if (index >= _size) throw RangeError('индекс выходит за границы');\n    // При превышении вместимости по числу элементов запускается расширение\n    if (_size == _capacity) extendCapacity();\n    // Сдвинуть элемент с индексом index и все следующие элементы на одну позицию назад\n    for (var j = _size - 1; j >= index; j--) {\n      _arr[j + 1] = _arr[j];\n    }\n    _arr[index] = _num;\n    // Обновить число элементов\n    _size++;\n  }\n\n  /* Удаление элемента */\n  int remove(int index) {\n    if (index >= _size) throw RangeError('индекс выходит за границы');\n    int _num = _arr[index];\n    // Сдвинуть все элементы после индекса index на одну позицию вперед\n    for (var j = index; j < _size - 1; j++) {\n      _arr[j] = _arr[j + 1];\n    }\n    // Обновить число элементов\n    _size--;\n    // Вернуть удаленный элемент\n    return _num;\n  }\n\n  /* Расширение списка */\n  void extendCapacity() {\n    // Создать новый массив длиной в _extendRatio раз больше исходного массива\n    final _newNums = List.filled(_capacity * _extendRatio, 0);\n    // Скопировать исходный массив в новый массив\n    List.copyRange(_newNums, 0, _arr);\n    // Обновить ссылку на _arr\n    _arr = _newNums;\n    // Обновить вместимость списка\n    _capacity = _arr.length;\n  }\n\n  /* Преобразовать список в массив */\n  List<int> toArray() {\n    List<int> arr = [];\n    for (var i = 0; i < _size; i++) {\n      arr.add(get(i));\n    }\n    return arr;\n  }\n}\n
        my_list.rs
        /* Класс списка */\n#[allow(dead_code)]\nstruct MyList {\n    arr: Vec<i32>,       // Массив (для хранения элементов списка)\n    capacity: usize,     // Вместимость списка\n    size: usize,         // Длина списка (текущее число элементов)\n    extend_ratio: usize, // Коэффициент увеличения списка при каждом расширении\n}\n\n#[allow(unused, unused_comparisons)]\nimpl MyList {\n    /* Конструктор */\n    pub fn new(capacity: usize) -> Self {\n        let mut vec = vec![0; capacity];\n        Self {\n            arr: vec,\n            capacity,\n            size: 0,\n            extend_ratio: 2,\n        }\n    }\n\n    /* Получить длину списка (текущее число элементов) */\n    pub fn size(&self) -> usize {\n        return self.size;\n    }\n\n    /* Получить вместимость списка */\n    pub fn capacity(&self) -> usize {\n        return self.capacity;\n    }\n\n    /* Доступ к элементу */\n    pub fn get(&self, index: usize) -> i32 {\n        // Если индекс выходит за границы, выбрасывается исключение; далее аналогично\n        if index >= self.size {\n            panic!(\"индекс выходит за границы\")\n        };\n        return self.arr[index];\n    }\n\n    /* Обновление элемента */\n    pub fn set(&mut self, index: usize, num: i32) {\n        if index >= self.size {\n            panic!(\"индекс выходит за границы\")\n        };\n        self.arr[index] = num;\n    }\n\n    /* Добавление элемента в конец */\n    pub fn add(&mut self, num: i32) {\n        // При превышении вместимости по числу элементов запускается расширение\n        if self.size == self.capacity() {\n            self.extend_capacity();\n        }\n        self.arr[self.size] = num;\n        // Обновить число элементов\n        self.size += 1;\n    }\n\n    /* Вставка элемента в середину */\n    pub fn insert(&mut self, index: usize, num: i32) {\n        if index >= self.size() {\n            panic!(\"индекс выходит за границы\")\n        };\n        // При превышении вместимости по числу элементов запускается расширение\n        if self.size == self.capacity() {\n            self.extend_capacity();\n        }\n        // Сдвинуть элемент с индексом index и все следующие элементы на одну позицию назад\n        for j in (index..self.size).rev() {\n            self.arr[j + 1] = self.arr[j];\n        }\n        self.arr[index] = num;\n        // Обновить число элементов\n        self.size += 1;\n    }\n\n    /* Удаление элемента */\n    pub fn remove(&mut self, index: usize) -> i32 {\n        if index >= self.size() {\n            panic!(\"индекс выходит за границы\")\n        };\n        let num = self.arr[index];\n        // Сдвинуть все элементы после индекса index на одну позицию вперед\n        for j in index..self.size - 1 {\n            self.arr[j] = self.arr[j + 1];\n        }\n        // Обновить число элементов\n        self.size -= 1;\n        // Вернуть удаленный элемент\n        return num;\n    }\n\n    /* Расширение списка */\n    pub fn extend_capacity(&mut self) {\n        // Создать новый массив длиной в extend_ratio раз больше исходного и скопировать в него исходный массив\n        let new_capacity = self.capacity * self.extend_ratio;\n        self.arr.resize(new_capacity, 0);\n        // Обновить вместимость списка\n        self.capacity = new_capacity;\n    }\n\n    /* Преобразовать список в массив */\n    pub fn to_array(&self) -> Vec<i32> {\n        // Преобразовывать только элементы списка в пределах фактической длины\n        let mut arr = Vec::new();\n        for i in 0..self.size {\n            arr.push(self.get(i));\n        }\n        arr\n    }\n}\n
        my_list.c
        /* Класс списка */\ntypedef struct {\n    int *arr;        // Массив (для хранения элементов списка)\n    int capacity;    // Вместимость списка\n    int size;        // Размер списка\n    int extendRatio; // Коэффициент расширения списка при каждом увеличении\n} MyList;\n\n/* Конструктор */\nMyList *newMyList() {\n    MyList *nums = malloc(sizeof(MyList));\n    nums->capacity = 10;\n    nums->arr = malloc(sizeof(int) * nums->capacity);\n    nums->size = 0;\n    nums->extendRatio = 2;\n    return nums;\n}\n\n/* Деструктор */\nvoid delMyList(MyList *nums) {\n    free(nums->arr);\n    free(nums);\n}\n\n/* Получить длину списка */\nint size(MyList *nums) {\n    return nums->size;\n}\n\n/* Получить вместимость списка */\nint capacity(MyList *nums) {\n    return nums->capacity;\n}\n\n/* Доступ к элементу */\nint get(MyList *nums, int index) {\n    assert(index >= 0 && index < nums->size);\n    return nums->arr[index];\n}\n\n/* Обновление элемента */\nvoid set(MyList *nums, int index, int num) {\n    assert(index >= 0 && index < nums->size);\n    nums->arr[index] = num;\n}\n\n/* Добавление элемента в конец */\nvoid add(MyList *nums, int num) {\n    if (size(nums) == capacity(nums)) {\n        extendCapacity(nums); // Расширение емкости\n    }\n    nums->arr[size(nums)] = num;\n    nums->size++;\n}\n\n/* Вставка элемента в середину */\nvoid insert(MyList *nums, int index, int num) {\n    assert(index >= 0 && index < size(nums));\n    // При превышении вместимости по числу элементов запускается расширение\n    if (size(nums) == capacity(nums)) {\n        extendCapacity(nums); // Расширение емкости\n    }\n    for (int i = size(nums); i > index; --i) {\n        nums->arr[i] = nums->arr[i - 1];\n    }\n    nums->arr[index] = num;\n    nums->size++;\n}\n\n/* Удаление элемента */\n// Внимание: stdio.h уже использует ключевое слово remove\nint removeItem(MyList *nums, int index) {\n    assert(index >= 0 && index < size(nums));\n    int num = nums->arr[index];\n    for (int i = index; i < size(nums) - 1; i++) {\n        nums->arr[i] = nums->arr[i + 1];\n    }\n    nums->size--;\n    return num;\n}\n\n/* Расширение списка */\nvoid extendCapacity(MyList *nums) {\n    // Сначала выделить память\n    int newCapacity = capacity(nums) * nums->extendRatio;\n    int *extend = (int *)malloc(sizeof(int) * newCapacity);\n    int *temp = nums->arr;\n\n    // Скопировать старые данные в новые\n    for (int i = 0; i < size(nums); i++)\n        extend[i] = nums->arr[i];\n\n    // Освободить старые данные\n    free(temp);\n\n    // Обновить новые данные\n    nums->arr = extend;\n    nums->capacity = newCapacity;\n}\n\n/* Преобразовать список в Array для вывода */\nint *toArray(MyList *nums) {\n    return nums->arr;\n}\n
        my_list.kt
        /* Класс списка */\nclass MyList {\n    private var arr: IntArray = intArrayOf() // Массив (для хранения элементов списка)\n    private var capacity: Int = 10 // Вместимость списка\n    private var size: Int = 0 // Длина списка (текущее число элементов)\n    private var extendRatio: Int = 2 // Коэффициент увеличения списка при каждом расширении\n\n    /* Конструктор */\n    init {\n        arr = IntArray(capacity)\n    }\n\n    /* Получить длину списка (текущее число элементов) */\n    fun size(): Int {\n        return size\n    }\n\n    /* Получить вместимость списка */\n    fun capacity(): Int {\n        return capacity\n    }\n\n    /* Доступ к элементу */\n    fun get(index: Int): Int {\n        // Если индекс выходит за границы, выбрасывается исключение; далее аналогично\n        if (index < 0 || index >= size)\n            throw IndexOutOfBoundsException(\"индекс выходит за границы\")\n        return arr[index]\n    }\n\n    /* Обновление элемента */\n    fun set(index: Int, num: Int) {\n        if (index < 0 || index >= size)\n            throw IndexOutOfBoundsException(\"индекс выходит за границы\")\n        arr[index] = num\n    }\n\n    /* Добавление элемента в конец */\n    fun add(num: Int) {\n        // При превышении вместимости по числу элементов запускается расширение\n        if (size == capacity())\n            extendCapacity()\n        arr[size] = num\n        // Обновить число элементов\n        size++\n    }\n\n    /* Вставка элемента в середину */\n    fun insert(index: Int, num: Int) {\n        if (index < 0 || index >= size)\n            throw IndexOutOfBoundsException(\"индекс выходит за границы\")\n        // При превышении вместимости по числу элементов запускается расширение\n        if (size == capacity())\n            extendCapacity()\n        // Сдвинуть элемент с индексом index и все следующие элементы на одну позицию назад\n        for (j in size - 1 downTo index)\n            arr[j + 1] = arr[j]\n        arr[index] = num\n        // Обновить число элементов\n        size++\n    }\n\n    /* Удаление элемента */\n    fun remove(index: Int): Int {\n        if (index < 0 || index >= size)\n            throw IndexOutOfBoundsException(\"индекс выходит за границы\")\n        val num = arr[index]\n        // Сдвинуть все элементы после индекса index на одну позицию вперед\n        for (j in index..<size - 1)\n            arr[j] = arr[j + 1]\n        // Обновить число элементов\n        size--\n        // Вернуть удаленный элемент\n        return num\n    }\n\n    /* Расширение списка */\n    fun extendCapacity() {\n        // Создать новый массив длиной в extendRatio раз больше исходного и скопировать в него исходный массив\n        arr = arr.copyOf(capacity() * extendRatio)\n        // Обновить вместимость списка\n        capacity = arr.size\n    }\n\n    /* Преобразовать список в массив */\n    fun toArray(): IntArray {\n        val size = size()\n        // Преобразовывать только элементы списка в пределах фактической длины\n        val arr = IntArray(size)\n        for (i in 0..<size) {\n            arr[i] = get(i)\n        }\n        return arr\n    }\n}\n
        my_list.rb
        ### Класс списка ###\nclass MyList\n  attr_reader :size       # Получить длину списка (текущее число элементов)\n  attr_reader :capacity   # Получить вместимость списка\n\n  ### Конструктор ###\n  def initialize\n    @capacity = 10\n    @size = 0\n    @extend_ratio = 2\n    @arr = Array.new(capacity)\n  end\n\n  ### Доступ к элементу ###\n  def get(index)\n    # Если индекс выходит за границы, выбрасывается исключение; далее аналогично\n    raise IndexError, \"индекс выходит за границы\" if index < 0 || index >= size\n    @arr[index]\n  end\n\n  ### Доступ к элементу ###\n  def set(index, num)\n    raise IndexError, \"индекс выходит за границы\" if index < 0 || index >= size\n    @arr[index] = num\n  end\n\n  ### Добавление элемента в конец ###\n  def add(num)\n    # При превышении вместимости по числу элементов запускается расширение\n    extend_capacity if size == capacity\n    @arr[size] = num\n\n    # Обновить число элементов\n    @size += 1\n  end\n\n  ### Вставка элемента в середину ###\n  def insert(index, num)\n    raise IndexError, \"индекс выходит за границы\" if index < 0 || index >= size\n\n    # При превышении вместимости по числу элементов запускается расширение\n    extend_capacity if size == capacity\n\n    # Сдвинуть элемент с индексом index и все следующие элементы на одну позицию назад\n    for j in (size - 1).downto(index)\n      @arr[j + 1] = @arr[j]\n    end\n    @arr[index] = num\n\n    # Обновить число элементов\n    @size += 1\n  end\n\n  ### Удаление элемента ###\n  def remove(index)\n    raise IndexError, \"индекс выходит за границы\" if index < 0 || index >= size\n    num = @arr[index]\n\n    # Сдвинуть все элементы после индекса index на одну позицию вперед\n    for j in index...size\n      @arr[j] = @arr[j + 1]\n    end\n\n    # Обновить число элементов\n    @size -= 1\n\n    # Вернуть удаленный элемент\n    num\n  end\n\n  ### Расширение списка ###\n  def extend_capacity\n    # Создать новый массив длиной в extend_ratio раз больше исходного и скопировать в него исходный массив\n    arr = @arr.dup + Array.new(capacity * (@extend_ratio - 1))\n    # Обновить вместимость списка\n    @capacity = arr.length\n  end\n\n  ### Преобразование списка в массив ###\n  def to_array\n    sz = size\n    # Преобразовывать только элементы списка в пределах фактической длины\n    arr = Array.new(sz)\n    for i in 0...sz\n      arr[i] = get(i)\n    end\n    arr\n  end\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 4. Массивы и списки","4.3   Список"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/","level":1,"title":"4.4   Оперативная память и кэш *","text":"

        В первых двух разделах этой главы мы разобрали массивы и связные списки - две базовые и важные структуры данных, которые представляют соответственно непрерывное хранение и разрозненное хранение.

        На практике физическая структура во многом определяет, насколько эффективно программа использует память и кэш, а это, в свою очередь, влияет на общую производительность алгоритма.

        ","path":["Глава 4. Массивы и списки","4.4   Оперативная память и кэш *"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/#441","level":2,"title":"4.4.1   Устройства хранения данных в компьютере","text":"

        В компьютере есть три типа устройств хранения данных: жесткий диск (hard disk) , оперативная память (random-access memory, RAM) и кэш-память (cache memory) . В таблице 4-2 показаны их различные роли и характеристики в компьютерной системе.

        Таблица 4-2   Устройства хранения данных в компьютере

        Жесткий диск Оперативная память Кэш Назначение Долговременное хранение данных, включая ОС, программы, файлы и т.д. Временное хранение выполняемых программ и обрабатываемых данных Хранение часто используемых данных и инструкций, уменьшающее число обращений CPU к памяти Энергозависимость Данные не теряются после отключения питания Данные теряются после отключения питания Данные теряются после отключения питания Емкость Большая, уровень TB Меньшая, уровень GB Очень малая, уровень MB Скорость Низкая, от сотен до тысяч MB/s Высокая, десятки GB/s Очень высокая, десятки и сотни GB/s Цена Низкая, единицы валюты за GB Высокая, десятки и сотни валютных единиц за GB Очень высокая, входит в стоимость CPU

        Компьютерную систему хранения можно представить в виде пирамиды, показанной на рисунке 4-9. Чем ближе устройство хранения к вершине пирамиды, тем оно быстрее, тем меньше его емкость и тем выше его стоимость. Такая многоуровневая конструкция возникла не случайно, а стала результатом тщательных инженерных компромиссов.

        • Жесткий диск трудно заменить оперативной памятью. Во-первых, данные в оперативной памяти исчезают после отключения питания, поэтому она не подходит для долговременного хранения. Во-вторых, память стоит в разы дороже жесткого диска, что мешает ее широкому применению.
        • Кэш не может одновременно быть и очень большим, и очень быстрым. По мере роста емкости кэшей L1, L2 и L3 их физический размер увеличивается, расстояние до ядра CPU становится больше, время передачи данных растет, а задержка доступа к элементам увеличивается. При текущем уровне технологий многоуровневая структура кэша является лучшим балансом между емкостью, скоростью и стоимостью.

        Рисунок 4-9   Система хранения данных компьютера

        Tip

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

        В итоге жесткий диск используется для долговременного хранения больших объемов данных, оперативная память - для временного хранения данных, с которыми программа работает прямо сейчас, а кэш - для хранения часто используемых данных и инструкций, чтобы ускорять выполнение программ. Все три уровня работают совместно и обеспечивают эффективную работу компьютерной системы.

        Как показано на рисунке 4-10, во время выполнения программы данные читаются с жесткого диска в оперативную память, а затем используются CPU в вычислениях. Кэш можно рассматривать как часть CPU: он подгружает данные из оперативной памяти, обеспечивая CPU высокоскоростной доступ и тем самым значительно ускоряя выполнение программы и уменьшая зависимость от более медленной RAM.

        Рисунок 4-10   Поток данных между жестким диском, RAM и кэшем

        ","path":["Глава 4. Массивы и списки","4.4   Оперативная память и кэш *"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/#442","level":2,"title":"4.4.2   Эффективность использования памяти структурами данных","text":"

        С точки зрения использования пространства памяти массивы и связные списки имеют свои преимущества и ограничения.

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

        С другой стороны, во время выполнения программы при многократном выделении и освобождении памяти фрагментация свободной памяти становится все более серьезной, что снижает эффективность ее использования. Массивы из-за непрерывного хранения относительно менее подвержены фрагментации. Напротив, элементы связного списка распределены по памяти, и частые операции вставки и удаления легче приводят к фрагментации.

        ","path":["Глава 4. Массивы и списки","4.4   Оперативная память и кэш *"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/#443","level":2,"title":"4.4.3   Эффективность использования кэша структурами данных","text":"

        Хотя по объему кэш намного меньше оперативной памяти, он значительно быстрее и играет критически важную роль в скорости выполнения программ. Поскольку объем кэша ограничен и в нем можно хранить только небольшую долю часто используемых данных, когда CPU пытается обратиться к данным, которых в кэше нет, происходит промах кэша (cache miss) , и CPU вынужден загружать нужные данные из более медленной памяти.

        Очевидно, что чем меньше промахов кэша, тем выше эффективность чтения и записи данных CPU, а значит, тем лучше производительность программы. Долю обращений, при которых CPU успешно получает данные из кэша, называют коэффициентом попадания в кэш (cache hit rate) ; этот показатель обычно используют для оценки эффективности кэша.

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

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

        На практике массивы и связные списки по-разному используют кэш, и это проявляется в нескольких аспектах.

        • Занимаемое пространство: элементы связного списка занимают больше места, чем элементы массива, поэтому в кэше помещается меньше полезных данных.
        • Строки кэша: данные списка разбросаны по памяти, а кэш загружает данные строками, поэтому доля бесполезно загружаемых данных оказывается выше.
        • Механизм предвыборки: шаблон доступа к данным у массивов более предсказуем, чем у списков, то есть системе легче угадать, какие данные понадобятся следующими.
        • Пространственная локальность: массив хранится в компактной области памяти, поэтому данные рядом с уже загруженными с большей вероятностью скоро будут использованы.

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

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

        • При решении алгоритмических задач мы обычно предпочитаем стек на основе массива, потому что он дает более высокую эффективность операций и поддерживает произвольный доступ, а цена за это - необходимость заранее выделить некоторый объем памяти под массив.
        • Если объем данных очень велик, структура сильно динамична, а ожидаемый размер стека трудно оценить заранее, то более уместен стек на основе связного списка. Список позволяет распределить большой объем данных по разным участкам памяти и избегает накладных расходов, связанных с расширением массива.
        ","path":["Глава 4. Массивы и списки","4.4   Оперативная память и кэш *"],"tags":[]},{"location":"chapter_array_and_linkedlist/summary/","level":1,"title":"4.5   Резюме","text":"","path":["Глава 4. Массивы и списки","4.5   Резюме"],"tags":[]},{"location":"chapter_array_and_linkedlist/summary/#1","level":3,"title":"1.   Ключевые выводы","text":"
        • Массивы и связные списки - это две базовые структуры данных, представляющие два способа хранения данных в памяти компьютера: хранение в непрерывном пространстве и хранение в разрозненном пространстве. Их свойства во многом взаимно дополняют друг друга.
        • Массив поддерживает произвольный доступ и занимает меньше памяти; однако вставка и удаление элементов в нем неэффективны, а длина после инициализации фиксирована.
        • Связный список позволяет эффективно вставлять и удалять узлы путем изменения ссылок (указателей), а также гибко менять длину; однако доступ к узлам менее эффективен, а памяти он занимает больше. Распространенные типы списков включают односвязные, циклические и двусвязные списки.
        • Список - это упорядоченная коллекция элементов, поддерживающая добавление, удаление, поиск и изменение, и обычно реализуемая на основе динамического массива. Он сохраняет преимущества массива и при этом может гибко менять длину.
        • Появление списка значительно повысило практическую ценность массива, хотя это и может приводить к потере части памяти.
        • Во время работы программы данные в основном хранятся в оперативной памяти. Массив обеспечивает более высокую эффективность использования пространства памяти, а связный список дает большую гибкость в использовании памяти.
        • Кэш, используя строки кэша, механизм предвыборки, а также пространственную и временную локальность, предоставляет CPU быстрый доступ к данным и заметно повышает эффективность выполнения программ.
        • Поскольку массивы обычно имеют более высокий коэффициент попадания в кэш, они в большинстве случаев работают эффективнее списков. При выборе структуры данных нужно исходить из конкретных требований и сценариев.
        ","path":["Глава 4. Массивы и списки","4.5   Резюме"],"tags":[]},{"location":"chapter_array_and_linkedlist/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

        Q: Влияет ли хранение массива в стеке или в куче на временную и пространственную эффективность?

        Массивы, расположенные и в стеке, и в куче, все равно хранятся в непрерывной области памяти, поэтому эффективность операций с данными у них в целом одинакова. Однако у стека и кучи есть собственные особенности, из-за которых возникают следующие различия.

        1. Эффективность выделения и освобождения: стек представляет собой относительно небольшой участок памяти, а выделение в нем обычно выполняется автоматически компилятором; куча же обычно больше, может выделяться динамически из кода и легче фрагментируется. Поэтому выделение и освобождение памяти в куче обычно медленнее, чем в стеке.
        2. Ограничение размера: объем стека относительно невелик, а размер кучи обычно ограничивается доступной памятью. Поэтому куча лучше подходит для хранения больших массивов.
        3. Гибкость: размер массива в стеке должен быть известен во время компиляции, а размер массива в куче может определяться динамически во время выполнения.

        Q: Почему для массива требуется, чтобы все элементы были одного типа, а для связного списка это не подчеркивается?

        Связный список состоит из узлов, а узлы соединяются между собой через ссылки (указатели), поэтому каждый узел в принципе может хранить данные разного типа, например int , double , string , object и т.д.

        Напротив, элементы массива должны быть одного типа, иначе нельзя будет вычислять адрес элемента через смещение. Например, если массив одновременно содержит int и long , один элемент занимает 4 байта, а другой - 8 байт ; в этом случае формула ниже уже не позволит вычислить смещение, потому что в массиве будут присутствовать элементы разной длины.

        # Адрес элемента в памяти = адрес массива в памяти (адрес первого элемента) + длина элемента * индекс элемента\n

        Q: После удаления узла P нужно ли присваивать P.next = None ?

        Можно и не изменять P.next . С точки зрения данного списка, при обходе от головы к хвосту узел P уже больше не встретится. Это означает, что узел P уже удален из списка, и то, куда он указывает после этого, на сам список больше не влияет.

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

        Q: Временная сложность вставки и удаления в связном списке равна \\(O(1)\\) . Но до вставки или удаления обычно еще нужно потратить \\(O(n)\\) на поиск элемента. Почему тогда общая сложность не \\(O(n)\\) ?

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

        Q: На рисунке \"Определение связного списка и способ хранения\" светло-голубой блок с указателем узла - это отдельный адрес памяти? Или он делит память пополам со значением узла?

        Этот рисунок дает только качественное представление; количественно все зависит от конкретных условий.

        • Значения узлов разных типов занимают разный объем памяти, например int , long , double и объекты-экземпляры.
        • Размер памяти, занимаемой переменной-указателем, зависит от операционной системы и среды компиляции и обычно составляет 8 байт или 4 байта.

        Q: Всегда ли добавление элемента в конец списка имеет сложность \\(O(1)\\) ?

        Если при добавлении элемента длина списка превышается, то сначала приходится расширять список, а уже затем добавлять новый элемент. Система выделяет новый участок памяти и переносит туда все элементы исходного списка, и в этот момент временная сложность становится \\(O(n)\\) .

        Q: В утверждении \"появление списка сильно повысило практическую полезность массива, но может приводить к потере части памяти\" под потерями памяти имеется в виду дополнительная память под такие переменные, как емкость, длина и коэффициент расширения?

        Потери памяти здесь в основном имеют два значения: во-первых, список обычно имеет некоторую начальную емкость, которая может быть нам не нужна целиком; во-вторых, чтобы избежать слишком частых расширений, емкость при расширении обычно умножается на некоторый коэффициент, например \\(\\times 1.5\\) . Из-за этого появляется много пустых слотов, которые обычно нельзя полностью заполнить.

        Q: В Python после инициализации n = [1, 2, 3] адреса этих трех элементов выглядят непрерывными, но после m = [2, 1, 3] можно заметить, что id элементов не идут подряд, а совпадают с одинаковыми числами из n . Если адреса элементов не непрерывны, остается ли m массивом?

        Предположим, что элементами списка являются узлы n = [n1, n2, n3, n4, n5] . Обычно эти 5 объектов-узлов тоже будут храниться в разных местах памяти. Однако, имея индекс списка, мы по-прежнему можем за \\(O(1)\\) получить адрес памяти соответствующего узла и обратиться к нему. Это связано с тем, что в массиве хранятся ссылки на узлы, а не сами узлы.

        В отличие от многих других языков, в Python даже числа обернуты в объекты, и в списке хранятся не сами числа, а ссылки на них. Поэтому мы и наблюдаем, что одинаковые числа в двух массивах имеют один и тот же id , а адреса этих чисел не обязаны быть непрерывными.

        Q: В C++ STL уже есть двусвязный список std::list , но в некоторых учебниках по алгоритмам им пользуются не так часто. Это связано с какими-то ограничениями?

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

        • Накладные расходы по памяти: поскольку каждому элементу нужны два дополнительных указателя (на предыдущий и следующий элементы), std::list обычно занимает больше памяти, чем std::vector .
        • Низкая дружелюбность к кэшу: поскольку данные не лежат непрерывно, std::list хуже использует кэш. В большинстве случаев std::vector показывает лучшую производительность.

        С другой стороны, случаи, когда связный список действительно необходим, в основном возникают в деревьях и графах. Для стеков и очередей чаще используют предоставляемые языком stack и queue , а не связный список напрямую.

        Q: Операция res = [[0]] * n создает двумерный список. Каждый [0] в нем независим?

        Нет, они не независимы. В таком двумерном списке все [0] на самом деле являются ссылками на один и тот же объект. Если изменить один из них, окажется, что меняются и все остальные соответствующие элементы.

        Если нужно, чтобы каждый [0] был независимым, можно использовать res = [[0] for _ in range(n)] . В этом варианте создаются \\(n\\) независимых объектов-списков [0] .

        Q: Операция res = [0] * n создает список. Каждый целочисленный 0 в нем независим?

        В этом списке все целые числа 0 являются ссылками на один и тот же объект. Это связано с тем, что Python использует механизм кэш-пула для маленьких целых чисел (обычно от -5 до 256), чтобы максимально переиспользовать объекты и повысить производительность.

        Хотя все элементы указывают на один и тот же объект, мы все равно можем независимо изменять элементы списка, потому что целые числа в Python - это \"неизменяемые объекты\". Когда мы изменяем некоторый элемент, на самом деле происходит переключение ссылки на другой объект, а не изменение исходного объекта.

        Однако если элементами списка являются \"изменяемые объекты\" (например списки, словари или экземпляры классов), то изменение одного элемента прямо меняет сам объект, и все элементы, ссылающиеся на него, увидят одно и то же изменение.

        ","path":["Глава 4. Массивы и списки","4.5   Резюме"],"tags":[]},{"location":"chapter_backtracking/","level":1,"title":"Глава 13.   Поиск с возвратом","text":"

        Abstract

        Мы словно исследователи в лабиринте: на пути вперед могут встречаться тупики и трудности.

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

        ","path":["Глава 13. Поиск с возвратом","Глава 13.   Поиск с возвратом"],"tags":[]},{"location":"chapter_backtracking/#_1","level":2,"title":"Содержание главы","text":"
        • 13.1   Алгоритм поиска с возвратом
        • 13.2   Задача о перестановках
        • 13.3   Задача о сумме подмножеств
        • 13.4   Задача о n ферзях
        • 13.5   Резюме
        ","path":["Глава 13. Поиск с возвратом","Глава 13.   Поиск с возвратом"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/","level":1,"title":"13.1   Алгоритм поиска с возвратом","text":"

        Алгоритм поиска с возвратом (backtracking algorithm) - это метод решения задач путем полного перебора. Его основная идея состоит в том, чтобы, начиная с некоторого исходного состояния, грубо перебрать все возможные решения, записывать корректные решения и продолжать поиск до тех пор, пока решение не будет найдено или пока не будут исчерпаны все возможные варианты.

        Обычно алгоритмы поиска с возвратом используют обход в глубину для обхода пространства решений. В главе \"Бинарные деревья\" мы уже упоминали, что прямой, симметричный и обратный обходы относятся к обходу в глубину. Теперь мы на основе прямого обхода построим задачу поиска с возвратом и постепенно разберем принцип работы этого алгоритма.

        Пример 1

        Дано двоичное дерево. Найдите и запишите все узлы со значением \\(7\\) ; верните список этих узлов.

        Для этой задачи мы выполняем прямой обход дерева и проверяем, равно ли значение текущего узла \\(7\\) ; если да, то добавляем значение этого узла в список результатов res . Соответствующий процесс показан на рисунке 13-1 и в коде:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_i_compact.py
        def pre_order(root: TreeNode):\n    \"\"\"Предварительный обход: пример 1\"\"\"\n    if root is None:\n        return\n    if root.val == 7:\n        # Записать решение\n        res.append(root)\n    pre_order(root.left)\n    pre_order(root.right)\n
        preorder_traversal_i_compact.cpp
        /* Предварительный обход: пример 1 */\nvoid preOrder(TreeNode *root) {\n    if (root == nullptr) {\n        return;\n    }\n    if (root->val == 7) {\n        // Записать решение\n        res.push_back(root);\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n}\n
        preorder_traversal_i_compact.java
        /* Предварительный обход: пример 1 */\nvoid preOrder(TreeNode root) {\n    if (root == null) {\n        return;\n    }\n    if (root.val == 7) {\n        // Записать решение\n        res.add(root);\n    }\n    preOrder(root.left);\n    preOrder(root.right);\n}\n
        preorder_traversal_i_compact.cs
        /* Предварительный обход: пример 1 */\nvoid PreOrder(TreeNode? root) {\n    if (root == null) {\n        return;\n    }\n    if (root.val == 7) {\n        // Записать решение\n        res.Add(root);\n    }\n    PreOrder(root.left);\n    PreOrder(root.right);\n}\n
        preorder_traversal_i_compact.go
        /* Предварительный обход: пример 1 */\nfunc preOrderI(root *TreeNode, res *[]*TreeNode) {\n    if root == nil {\n        return\n    }\n    if (root.Val).(int) == 7 {\n        // Записать решение\n        *res = append(*res, root)\n    }\n    preOrderI(root.Left, res)\n    preOrderI(root.Right, res)\n}\n
        preorder_traversal_i_compact.swift
        /* Предварительный обход: пример 1 */\nfunc preOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    if root.val == 7 {\n        // Записать решение\n        res.append(root)\n    }\n    preOrder(root: root.left)\n    preOrder(root: root.right)\n}\n
        preorder_traversal_i_compact.js
        /* Предварительный обход: пример 1 */\nfunction preOrder(root, res) {\n    if (root === null) {\n        return;\n    }\n    if (root.val === 7) {\n        // Записать решение\n        res.push(root);\n    }\n    preOrder(root.left, res);\n    preOrder(root.right, res);\n}\n
        preorder_traversal_i_compact.ts
        /* Предварительный обход: пример 1 */\nfunction preOrder(root: TreeNode | null, res: TreeNode[]): void {\n    if (root === null) {\n        return;\n    }\n    if (root.val === 7) {\n        // Записать решение\n        res.push(root);\n    }\n    preOrder(root.left, res);\n    preOrder(root.right, res);\n}\n
        preorder_traversal_i_compact.dart
        /* Предварительный обход: пример 1 */\nvoid preOrder(TreeNode? root, List<TreeNode> res) {\n  if (root == null) {\n    return;\n  }\n  if (root.val == 7) {\n    // Записать решение\n    res.add(root);\n  }\n  preOrder(root.left, res);\n  preOrder(root.right, res);\n}\n
        preorder_traversal_i_compact.rs
        /* Предварительный обход: пример 1 */\nfn pre_order(res: &mut Vec<Rc<RefCell<TreeNode>>>, root: Option<&Rc<RefCell<TreeNode>>>) {\n    if root.is_none() {\n        return;\n    }\n    if let Some(node) = root {\n        if node.borrow().val == 7 {\n            // Записать решение\n            res.push(node.clone());\n        }\n        pre_order(res, node.borrow().left.as_ref());\n        pre_order(res, node.borrow().right.as_ref());\n    }\n}\n
        preorder_traversal_i_compact.c
        /* Предварительный обход: пример 1 */\nvoid preOrder(TreeNode *root) {\n    if (root == NULL) {\n        return;\n    }\n    if (root->val == 7) {\n        // Записать решение\n        res[resSize++] = root;\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n}\n
        preorder_traversal_i_compact.kt
        /* Предварительный обход: пример 1 */\nfun preOrder(root: TreeNode?) {\n    if (root == null) {\n        return\n    }\n    if (root._val == 7) {\n        // Записать решение\n        res!!.add(root)\n    }\n    preOrder(root.left)\n    preOrder(root.right)\n}\n
        preorder_traversal_i_compact.rb
        ### Предварительный обход: пример 1 ###\ndef pre_order(root)\n  return unless root\n\n  # Записать решение\n  $res << root if root.val == 7\n\n  pre_order(root.left)\n  pre_order(root.right)\nend\n
        Визуализация кода

        Во весь экран >

        Рисунок 13-1   Поиск узлов при прямом обходе

        ","path":["Глава 13. Поиск с возвратом","13.1   Алгоритм поиска с возвратом"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1311","level":2,"title":"13.1.1   Попытка и откат","text":"

        Алгоритм называется поиском с возвратом, потому что при поиске в пространстве решений он использует стратегию \"попытка\" и \"откат\". Когда в процессе поиска алгоритм приходит в состояние, из которого нельзя двигаться дальше или нельзя получить удовлетворяющее условиям решение, он отменяет предыдущий выбор, возвращается к более раннему состоянию и пробует другие возможные варианты.

        Для примера 1 посещение каждого узла представляет собой \"попытку\", а прохождение листового узла или возврат к родителю через return означает \"откат\".

        Важно понимать, что откат не сводится только к возврату из функции. Чтобы показать это, слегка расширим пример 1.

        Пример 2

        Найдите в двоичном дереве все узлы со значением \\(7\\) и верните пути от корня до этих узлов.

        Взяв за основу код примера 1, добавим список path для записи пути посещенных узлов. Когда встречается узел со значением \\(7\\) , мы копируем path и добавляем его в список результатов res . После завершения обхода именно res будет содержать все решения. Код приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_ii_compact.py
        def pre_order(root: TreeNode):\n    \"\"\"Предварительный обход: пример 2\"\"\"\n    if root is None:\n        return\n    # Попытка\n    path.append(root)\n    if root.val == 7:\n        # Записать решение\n        res.append(list(path))\n    pre_order(root.left)\n    pre_order(root.right)\n    # Откат\n    path.pop()\n
        preorder_traversal_ii_compact.cpp
        /* Предварительный обход: пример 2 */\nvoid preOrder(TreeNode *root) {\n    if (root == nullptr) {\n        return;\n    }\n    // Попытка\n    path.push_back(root);\n    if (root->val == 7) {\n        // Записать решение\n        res.push_back(path);\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n    // Откат\n    path.pop_back();\n}\n
        preorder_traversal_ii_compact.java
        /* Предварительный обход: пример 2 */\nvoid preOrder(TreeNode root) {\n    if (root == null) {\n        return;\n    }\n    // Попытка\n    path.add(root);\n    if (root.val == 7) {\n        // Записать решение\n        res.add(new ArrayList<>(path));\n    }\n    preOrder(root.left);\n    preOrder(root.right);\n    // Откат\n    path.remove(path.size() - 1);\n}\n
        preorder_traversal_ii_compact.cs
        /* Предварительный обход: пример 2 */\nvoid PreOrder(TreeNode? root) {\n    if (root == null) {\n        return;\n    }\n    // Попытка\n    path.Add(root);\n    if (root.val == 7) {\n        // Записать решение\n        res.Add(new List<TreeNode>(path));\n    }\n    PreOrder(root.left);\n    PreOrder(root.right);\n    // Откат\n    path.RemoveAt(path.Count - 1);\n}\n
        preorder_traversal_ii_compact.go
        /* Предварительный обход: пример 2 */\nfunc preOrderII(root *TreeNode, res *[][]*TreeNode, path *[]*TreeNode) {\n    if root == nil {\n        return\n    }\n    // Попытка\n    *path = append(*path, root)\n    if root.Val.(int) == 7 {\n        // Записать решение\n        *res = append(*res, append([]*TreeNode{}, *path...))\n    }\n    preOrderII(root.Left, res, path)\n    preOrderII(root.Right, res, path)\n    // Откат\n    *path = (*path)[:len(*path)-1]\n}\n
        preorder_traversal_ii_compact.swift
        /* Предварительный обход: пример 2 */\nfunc preOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    // Попытка\n    path.append(root)\n    if root.val == 7 {\n        // Записать решение\n        res.append(path)\n    }\n    preOrder(root: root.left)\n    preOrder(root: root.right)\n    // Откат\n    path.removeLast()\n}\n
        preorder_traversal_ii_compact.js
        /* Предварительный обход: пример 2 */\nfunction preOrder(root, path, res) {\n    if (root === null) {\n        return;\n    }\n    // Попытка\n    path.push(root);\n    if (root.val === 7) {\n        // Записать решение\n        res.push([...path]);\n    }\n    preOrder(root.left, path, res);\n    preOrder(root.right, path, res);\n    // Откат\n    path.pop();\n}\n
        preorder_traversal_ii_compact.ts
        /* Предварительный обход: пример 2 */\nfunction preOrder(\n    root: TreeNode | null,\n    path: TreeNode[],\n    res: TreeNode[][]\n): void {\n    if (root === null) {\n        return;\n    }\n    // Попытка\n    path.push(root);\n    if (root.val === 7) {\n        // Записать решение\n        res.push([...path]);\n    }\n    preOrder(root.left, path, res);\n    preOrder(root.right, path, res);\n    // Откат\n    path.pop();\n}\n
        preorder_traversal_ii_compact.dart
        /* Предварительный обход: пример 2 */\nvoid preOrder(\n  TreeNode? root,\n  List<TreeNode> path,\n  List<List<TreeNode>> res,\n) {\n  if (root == null) {\n    return;\n  }\n\n  // Попытка\n  path.add(root);\n  if (root.val == 7) {\n    // Записать решение\n    res.add(List.from(path));\n  }\n  preOrder(root.left, path, res);\n  preOrder(root.right, path, res);\n  // Откат\n  path.removeLast();\n}\n
        preorder_traversal_ii_compact.rs
        /* Предварительный обход: пример 2 */\nfn pre_order(\n    res: &mut Vec<Vec<Rc<RefCell<TreeNode>>>>,\n    path: &mut Vec<Rc<RefCell<TreeNode>>>,\n    root: Option<&Rc<RefCell<TreeNode>>>,\n) {\n    if root.is_none() {\n        return;\n    }\n    if let Some(node) = root {\n        // Попытка\n        path.push(node.clone());\n        if node.borrow().val == 7 {\n            // Записать решение\n            res.push(path.clone());\n        }\n        pre_order(res, path, node.borrow().left.as_ref());\n        pre_order(res, path, node.borrow().right.as_ref());\n        // Откат\n        path.pop();\n    }\n}\n
        preorder_traversal_ii_compact.c
        /* Предварительный обход: пример 2 */\nvoid preOrder(TreeNode *root) {\n    if (root == NULL) {\n        return;\n    }\n    // Попытка\n    path[pathSize++] = root;\n    if (root->val == 7) {\n        // Записать решение\n        for (int i = 0; i < pathSize; ++i) {\n            res[resSize][i] = path[i];\n        }\n        resSize++;\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n    // Откат\n    pathSize--;\n}\n
        preorder_traversal_ii_compact.kt
        /* Предварительный обход: пример 2 */\nfun preOrder(root: TreeNode?) {\n    if (root == null) {\n        return\n    }\n    // Попытка\n    path!!.add(root)\n    if (root._val == 7) {\n        // Записать решение\n        res!!.add(path!!.toMutableList())\n    }\n    preOrder(root.left)\n    preOrder(root.right)\n    // Откат\n    path!!.removeAt(path!!.size - 1)\n}\n
        preorder_traversal_ii_compact.rb
        ### Предварительный обход: пример 2 ###\ndef pre_order(root)\n  return unless root\n\n  # Попытка\n  $path << root\n\n  # Записать решение\n  $res << $path.dup if root.val == 7\n\n  pre_order(root.left)\n  pre_order(root.right)\n\n  # Откат\n  $path.pop\nend\n
        Визуализация кода

        Во весь экран >

        В каждой \"попытке\" мы добавляем текущий узел в path , чтобы записать путь; а перед \"откатом\" нам нужно удалить этот узел из path , чтобы восстановить состояние, существовавшее до текущей попытки.

        Если посмотреть на процесс, изображенный на рисунке 13-2, то попытку и откат можно понимать как \"движение вперед\" и \"отмену\": это два взаимно противоположных действия.

        <1><2><3><4><5><6><7><8><9><10><11>

        Рисунок 13-2   Попытка и откат

        ","path":["Глава 13. Поиск с возвратом","13.1   Алгоритм поиска с возвратом"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1312","level":2,"title":"13.1.2   Обрезка","text":"

        Сложные задачи поиска с возвратом обычно содержат одно или несколько ограничений, которые часто можно использовать для \"обрезки\".

        Пример 3

        Найдите в двоичном дереве все узлы со значением \\(7\\) , верните пути от корня до этих узлов, причем путь не должен содержать узлы со значением \\(3\\).

        Чтобы выполнить это ограничение, нам нужно добавить операцию обрезки: во время поиска, если встречается узел со значением \\(3\\) , мы сразу возвращаемся и не продолжаем дальнейший поиск. Код выглядит так:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_iii_compact.py
        def pre_order(root: TreeNode):\n    \"\"\"Предварительный обход: пример 3\"\"\"\n    # Отсечение\n    if root is None or root.val == 3:\n        return\n    # Попытка\n    path.append(root)\n    if root.val == 7:\n        # Записать решение\n        res.append(list(path))\n    pre_order(root.left)\n    pre_order(root.right)\n    # Откат\n    path.pop()\n
        preorder_traversal_iii_compact.cpp
        /* Предварительный обход: пример 3 */\nvoid preOrder(TreeNode *root) {\n    // Отсечение\n    if (root == nullptr || root->val == 3) {\n        return;\n    }\n    // Попытка\n    path.push_back(root);\n    if (root->val == 7) {\n        // Записать решение\n        res.push_back(path);\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n    // Откат\n    path.pop_back();\n}\n
        preorder_traversal_iii_compact.java
        /* Предварительный обход: пример 3 */\nvoid preOrder(TreeNode root) {\n    // Отсечение\n    if (root == null || root.val == 3) {\n        return;\n    }\n    // Попытка\n    path.add(root);\n    if (root.val == 7) {\n        // Записать решение\n        res.add(new ArrayList<>(path));\n    }\n    preOrder(root.left);\n    preOrder(root.right);\n    // Откат\n    path.remove(path.size() - 1);\n}\n
        preorder_traversal_iii_compact.cs
        /* Предварительный обход: пример 3 */\nvoid PreOrder(TreeNode? root) {\n    // Отсечение\n    if (root == null || root.val == 3) {\n        return;\n    }\n    // Попытка\n    path.Add(root);\n    if (root.val == 7) {\n        // Записать решение\n        res.Add(new List<TreeNode>(path));\n    }\n    PreOrder(root.left);\n    PreOrder(root.right);\n    // Откат\n    path.RemoveAt(path.Count - 1);\n}\n
        preorder_traversal_iii_compact.go
        /* Предварительный обход: пример 3 */\nfunc preOrderIII(root *TreeNode, res *[][]*TreeNode, path *[]*TreeNode) {\n    // Отсечение\n    if root == nil || root.Val == 3 {\n        return\n    }\n    // Попытка\n    *path = append(*path, root)\n    if root.Val.(int) == 7 {\n        // Записать решение\n        *res = append(*res, append([]*TreeNode{}, *path...))\n    }\n    preOrderIII(root.Left, res, path)\n    preOrderIII(root.Right, res, path)\n    // Откат\n    *path = (*path)[:len(*path)-1]\n}\n
        preorder_traversal_iii_compact.swift
        /* Предварительный обход: пример 3 */\nfunc preOrder(root: TreeNode?) {\n    // Отсечение\n    guard let root = root, root.val != 3 else {\n        return\n    }\n    // Попытка\n    path.append(root)\n    if root.val == 7 {\n        // Записать решение\n        res.append(path)\n    }\n    preOrder(root: root.left)\n    preOrder(root: root.right)\n    // Откат\n    path.removeLast()\n}\n
        preorder_traversal_iii_compact.js
        /* Предварительный обход: пример 3 */\nfunction preOrder(root, path, res) {\n    // Отсечение\n    if (root === null || root.val === 3) {\n        return;\n    }\n    // Попытка\n    path.push(root);\n    if (root.val === 7) {\n        // Записать решение\n        res.push([...path]);\n    }\n    preOrder(root.left, path, res);\n    preOrder(root.right, path, res);\n    // Откат\n    path.pop();\n}\n
        preorder_traversal_iii_compact.ts
        /* Предварительный обход: пример 3 */\nfunction preOrder(\n    root: TreeNode | null,\n    path: TreeNode[],\n    res: TreeNode[][]\n): void {\n    // Отсечение\n    if (root === null || root.val === 3) {\n        return;\n    }\n    // Попытка\n    path.push(root);\n    if (root.val === 7) {\n        // Записать решение\n        res.push([...path]);\n    }\n    preOrder(root.left, path, res);\n    preOrder(root.right, path, res);\n    // Откат\n    path.pop();\n}\n
        preorder_traversal_iii_compact.dart
        /* Предварительный обход: пример 3 */\nvoid preOrder(\n  TreeNode? root,\n  List<TreeNode> path,\n  List<List<TreeNode>> res,\n) {\n  if (root == null || root.val == 3) {\n    return;\n  }\n\n  // Попытка\n  path.add(root);\n  if (root.val == 7) {\n    // Записать решение\n    res.add(List.from(path));\n  }\n  preOrder(root.left, path, res);\n  preOrder(root.right, path, res);\n  // Откат\n  path.removeLast();\n}\n
        preorder_traversal_iii_compact.rs
        /* Предварительный обход: пример 3 */\nfn pre_order(\n    res: &mut Vec<Vec<Rc<RefCell<TreeNode>>>>,\n    path: &mut Vec<Rc<RefCell<TreeNode>>>,\n    root: Option<&Rc<RefCell<TreeNode>>>,\n) {\n    // Отсечение\n    if root.is_none() || root.as_ref().unwrap().borrow().val == 3 {\n        return;\n    }\n    if let Some(node) = root {\n        // Попытка\n        path.push(node.clone());\n        if node.borrow().val == 7 {\n            // Записать решение\n            res.push(path.clone());\n        }\n        pre_order(res, path, node.borrow().left.as_ref());\n        pre_order(res, path, node.borrow().right.as_ref());\n        // Откат\n        path.pop();\n    }\n}\n
        preorder_traversal_iii_compact.c
        /* Предварительный обход: пример 3 */\nvoid preOrder(TreeNode *root) {\n    // Отсечение\n    if (root == NULL || root->val == 3) {\n        return;\n    }\n    // Попытка\n    path[pathSize++] = root;\n    if (root->val == 7) {\n        // Записать решение\n        for (int i = 0; i < pathSize; i++) {\n            res[resSize][i] = path[i];\n        }\n        resSize++;\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n    // Откат\n    pathSize--;\n}\n
        preorder_traversal_iii_compact.kt
        /* Предварительный обход: пример 3 */\nfun preOrder(root: TreeNode?) {\n    // Отсечение\n    if (root == null || root._val == 3) {\n        return\n    }\n    // Попытка\n    path!!.add(root)\n    if (root._val == 7) {\n        // Записать решение\n        res!!.add(path!!.toMutableList())\n    }\n    preOrder(root.left)\n    preOrder(root.right)\n    // Откат\n    path!!.removeAt(path!!.size - 1)\n}\n
        preorder_traversal_iii_compact.rb
        ### Предварительный обход: пример 3 ###\ndef pre_order(root)\n  # Отсечение\n  return if !root || root.val == 3\n\n  # Попытка\n  $path.append(root)\n\n  # Записать решение\n  $res << $path.dup if root.val == 7\n\n  pre_order(root.left)\n  pre_order(root.right)\n\n  # Откат\n  $path.pop\nend\n
        Визуализация кода

        Во весь экран >

        Термин \"обрезка\" очень нагляден. Как показано на рисунке 13-3, во время поиска мы отсекаем ветви, не удовлетворяющие ограничениям , тем самым избегая множества бессмысленных попыток и повышая эффективность поиска.

        Рисунок 13-3   Обрезка по условиям задачи

        ","path":["Глава 13. Поиск с возвратом","13.1   Алгоритм поиска с возвратом"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1313","level":2,"title":"13.1.3   Каркас кода","text":"

        Теперь попробуем извлечь общий каркас из действий \"попытка\", \"откат\" и \"обрезка\", чтобы сделать код более универсальным.

        В следующем каркасе кода state обозначает текущее состояние задачи, а choices - список выборов, доступных в текущем состоянии:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
        def backtrack(state: State, choices: list[choice], res: list[state]):\n    \"\"\"Каркас алгоритма поиска с возвратом\"\"\"\n    # Проверка, является ли текущее состояние решением\n    if is_solution(state):\n        # Запись решения\n        record_solution(state, res)\n        # Дальше не продолжаем поиск\n        return\n    # Перебор всех возможных выборов\n    for choice in choices:\n        # Обрезка: проверка допустимости выбора\n        if is_valid(state, choice):\n            # Попытка: сделать выбор и обновить состояние\n            make_choice(state, choice)\n            backtrack(state, choices, res)\n            # Откат: отменить выбор и восстановить предыдущее состояние\n            undo_choice(state, choice)\n
        /* Каркас алгоритма поиска с возвратом */\nvoid backtrack(State *state, vector<Choice *> &choices, vector<State *> &res) {\n    // Проверка, является ли текущее состояние решением\n    if (isSolution(state)) {\n        // Запись решения\n        recordSolution(state, res);\n        // Дальше не продолжаем поиск\n        return;\n    }\n    // Перебор всех возможных выборов\n    for (Choice choice : choices) {\n        // Обрезка: проверка допустимости выбора\n        if (isValid(state, choice)) {\n            // Попытка: сделать выбор и обновить состояние\n            makeChoice(state, choice);\n            backtrack(state, choices, res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            undoChoice(state, choice);\n        }\n    }\n}\n
        /* Каркас алгоритма поиска с возвратом */\nvoid backtrack(State state, List<Choice> choices, List<State> res) {\n    // Проверка, является ли текущее состояние решением\n    if (isSolution(state)) {\n        // Запись решения\n        recordSolution(state, res);\n        // Дальше не продолжаем поиск\n        return;\n    }\n    // Перебор всех возможных выборов\n    for (Choice choice : choices) {\n        // Обрезка: проверка допустимости выбора\n        if (isValid(state, choice)) {\n            // Попытка: сделать выбор и обновить состояние\n            makeChoice(state, choice);\n            backtrack(state, choices, res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            undoChoice(state, choice);\n        }\n    }\n}\n
        /* Каркас алгоритма поиска с возвратом */\nvoid Backtrack(State state, List<Choice> choices, List<State> res) {\n    // Проверка, является ли текущее состояние решением\n    if (IsSolution(state)) {\n        // Запись решения\n        RecordSolution(state, res);\n        // Дальше не продолжаем поиск\n        return;\n    }\n    // Перебор всех возможных выборов\n    foreach (Choice choice in choices) {\n        // Обрезка: проверка допустимости выбора\n        if (IsValid(state, choice)) {\n            // Попытка: сделать выбор и обновить состояние\n            MakeChoice(state, choice);\n            Backtrack(state, choices, res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            UndoChoice(state, choice);\n        }\n    }\n}\n
        /* Каркас алгоритма поиска с возвратом */\nfunc backtrack(state *State, choices []Choice, res *[]State) {\n    // Проверка, является ли текущее состояние решением\n    if isSolution(state) {\n        // Запись решения\n        recordSolution(state, res)\n        // Дальше не продолжаем поиск\n        return\n    }\n    // Перебор всех возможных выборов\n    for _, choice := range choices {\n        // Обрезка: проверка допустимости выбора\n        if isValid(state, choice) {\n            // Попытка: сделать выбор и обновить состояние\n            makeChoice(state, choice)\n            backtrack(state, choices, res)\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            undoChoice(state, choice)\n        }\n    }\n}\n
        /* Каркас алгоритма поиска с возвратом */\nfunc backtrack(state: inout State, choices: [Choice], res: inout [State]) {\n    // Проверка, является ли текущее состояние решением\n    if isSolution(state: state) {\n        // Запись решения\n        recordSolution(state: state, res: &res)\n        // Дальше не продолжаем поиск\n        return\n    }\n    // Перебор всех возможных выборов\n    for choice in choices {\n        // Обрезка: проверка допустимости выбора\n        if isValid(state: state, choice: choice) {\n            // Попытка: сделать выбор и обновить состояние\n            makeChoice(state: &state, choice: choice)\n            backtrack(state: &state, choices: choices, res: &res)\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            undoChoice(state: &state, choice: choice)\n        }\n    }\n}\n
        /* Каркас алгоритма поиска с возвратом */\nfunction backtrack(state, choices, res) {\n    // Проверка, является ли текущее состояние решением\n    if (isSolution(state)) {\n        // Запись решения\n        recordSolution(state, res);\n        // Дальше не продолжаем поиск\n        return;\n    }\n    // Перебор всех возможных выборов\n    for (let choice of choices) {\n        // Обрезка: проверка допустимости выбора\n        if (isValid(state, choice)) {\n            // Попытка: сделать выбор и обновить состояние\n            makeChoice(state, choice);\n            backtrack(state, choices, res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            undoChoice(state, choice);\n        }\n    }\n}\n
        /* Каркас алгоритма поиска с возвратом */\nfunction backtrack(state: State, choices: Choice[], res: State[]): void {\n    // Проверка, является ли текущее состояние решением\n    if (isSolution(state)) {\n        // Запись решения\n        recordSolution(state, res);\n        // Дальше не продолжаем поиск\n        return;\n    }\n    // Перебор всех возможных выборов\n    for (let choice of choices) {\n        // Обрезка: проверка допустимости выбора\n        if (isValid(state, choice)) {\n            // Попытка: сделать выбор и обновить состояние\n            makeChoice(state, choice);\n            backtrack(state, choices, res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            undoChoice(state, choice);\n        }\n    }\n}\n
        /* Каркас алгоритма поиска с возвратом */\nvoid backtrack(State state, List<Choice>, List<State> res) {\n  // Проверка, является ли текущее состояние решением\n  if (isSolution(state)) {\n    // Запись решения\n    recordSolution(state, res);\n    // Дальше не продолжаем поиск\n    return;\n  }\n  // Перебор всех возможных выборов\n  for (Choice choice in choices) {\n    // Обрезка: проверка допустимости выбора\n    if (isValid(state, choice)) {\n      // Попытка: сделать выбор и обновить состояние\n      makeChoice(state, choice);\n      backtrack(state, choices, res);\n      // Откат: отменить выбор и восстановить предыдущее состояние\n      undoChoice(state, choice);\n    }\n  }\n}\n
        /* Каркас алгоритма поиска с возвратом */\nfn backtrack(state: &mut State, choices: &Vec<Choice>, res: &mut Vec<State>) {\n    // Проверка, является ли текущее состояние решением\n    if is_solution(state) {\n        // Запись решения\n        record_solution(state, res);\n        // Дальше не продолжаем поиск\n        return;\n    }\n    // Перебор всех возможных выборов\n    for choice in choices {\n        // Обрезка: проверка допустимости выбора\n        if is_valid(state, choice) {\n            // Попытка: сделать выбор и обновить состояние\n            make_choice(state, choice);\n            backtrack(state, choices, res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            undo_choice(state, choice);\n        }\n    }\n}\n
        /* Каркас алгоритма поиска с возвратом */\nvoid backtrack(State *state, Choice *choices, int numChoices, State *res, int numRes) {\n    // Проверка, является ли текущее состояние решением\n    if (isSolution(state)) {\n        // Запись решения\n        recordSolution(state, res, numRes);\n        // Дальше не продолжаем поиск\n        return;\n    }\n    // Перебор всех возможных выборов\n    for (int i = 0; i < numChoices; i++) {\n        // Обрезка: проверка допустимости выбора\n        if (isValid(state, &choices[i])) {\n            // Попытка: сделать выбор и обновить состояние\n            makeChoice(state, &choices[i]);\n            backtrack(state, choices, numChoices, res, numRes);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            undoChoice(state, &choices[i]);\n        }\n    }\n}\n
        /* Каркас алгоритма поиска с возвратом */\nfun backtrack(state: State?, choices: List<Choice?>, res: List<State?>?) {\n    // Проверка, является ли текущее состояние решением\n    if (isSolution(state)) {\n        // Запись решения\n        recordSolution(state, res)\n        // Дальше не продолжаем поиск\n        return\n    }\n    // Перебор всех возможных выборов\n    for (choice in choices) {\n        // Обрезка: проверка допустимости выбора\n        if (isValid(state, choice)) {\n            // Попытка: сделать выбор и обновить состояние\n            makeChoice(state, choice)\n            backtrack(state, choices, res)\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            undoChoice(state, choice)\n        }\n    }\n}\n
        ### Каркас алгоритма поиска с возвратом ###\ndef backtrack(state, choices, res)\n    # Проверка, является ли текущее состояние решением\n    if is_solution?(state)\n        # Запись решения\n        record_solution(state, res)\n        return\n    end\n\n    # Перебор всех возможных выборов\n    for choice in choices\n        # Обрезка: проверка допустимости выбора\n        if is_valid?(state, choice)\n            # Попытка: сделать выбор и обновить состояние\n            make_choice(state, choice)\n            backtrack(state, choices, res)\n            # Откат: отменить выбор и восстановить предыдущее состояние\n            undo_choice(state, choice)\n        end\n    end\nend\n

        Теперь, опираясь на этот каркас, решим пример 3. Состояние state здесь - это путь обхода узлов, выбор choices - левый и правый потомки текущего узла, а результат res - список путей:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_iii_template.py
        def is_solution(state: list[TreeNode]) -> bool:\n    \"\"\"Проверить, является ли текущее состояние решением\"\"\"\n    return state and state[-1].val == 7\n\ndef record_solution(state: list[TreeNode], res: list[list[TreeNode]]):\n    \"\"\"Записать решение\"\"\"\n    res.append(list(state))\n\ndef is_valid(state: list[TreeNode], choice: TreeNode) -> bool:\n    \"\"\"Проверить, допустим ли этот выбор в текущем состоянии\"\"\"\n    return choice is not None and choice.val != 3\n\ndef make_choice(state: list[TreeNode], choice: TreeNode):\n    \"\"\"Обновить состояние\"\"\"\n    state.append(choice)\n\ndef undo_choice(state: list[TreeNode], choice: TreeNode):\n    \"\"\"Восстановить состояние\"\"\"\n    state.pop()\n\ndef backtrack(\n    state: list[TreeNode], choices: list[TreeNode], res: list[list[TreeNode]]\n):\n    \"\"\"Алгоритм бэктрекинга: пример 3\"\"\"\n    # Проверить, является ли текущее состояние решением\n    if is_solution(state):\n        # Записать решение\n        record_solution(state, res)\n    # Перебор всех вариантов выбора\n    for choice in choices:\n        # Отсечение: проверить допустимость выбора\n        if is_valid(state, choice):\n            # Попытка: сделать выбор и обновить состояние\n            make_choice(state, choice)\n            # Перейти к следующему выбору\n            backtrack(state, [choice.left, choice.right], res)\n            # Откат: отменить выбор и восстановить предыдущее состояние\n            undo_choice(state, choice)\n
        preorder_traversal_iii_template.cpp
        /* Проверить, является ли текущее состояние решением */\nbool isSolution(vector<TreeNode *> &state) {\n    return !state.empty() && state.back()->val == 7;\n}\n\n/* Записать решение */\nvoid recordSolution(vector<TreeNode *> &state, vector<vector<TreeNode *>> &res) {\n    res.push_back(state);\n}\n\n/* Проверить, допустим ли этот выбор в текущем состоянии */\nbool isValid(vector<TreeNode *> &state, TreeNode *choice) {\n    return choice != nullptr && choice->val != 3;\n}\n\n/* Обновить состояние */\nvoid makeChoice(vector<TreeNode *> &state, TreeNode *choice) {\n    state.push_back(choice);\n}\n\n/* Восстановить состояние */\nvoid undoChoice(vector<TreeNode *> &state, TreeNode *choice) {\n    state.pop_back();\n}\n\n/* Алгоритм бэктрекинга: пример 3 */\nvoid backtrack(vector<TreeNode *> &state, vector<TreeNode *> &choices, vector<vector<TreeNode *>> &res) {\n    // Проверить, является ли текущее состояние решением\n    if (isSolution(state)) {\n        // Записать решение\n        recordSolution(state, res);\n    }\n    // Перебор всех вариантов выбора\n    for (TreeNode *choice : choices) {\n        // Отсечение: проверить допустимость выбора\n        if (isValid(state, choice)) {\n            // Попытка: сделать выбор и обновить состояние\n            makeChoice(state, choice);\n            // Перейти к следующему выбору\n            vector<TreeNode *> nextChoices{choice->left, choice->right};\n            backtrack(state, nextChoices, res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            undoChoice(state, choice);\n        }\n    }\n}\n
        preorder_traversal_iii_template.java
        /* Проверить, является ли текущее состояние решением */\nboolean isSolution(List<TreeNode> state) {\n    return !state.isEmpty() && state.get(state.size() - 1).val == 7;\n}\n\n/* Записать решение */\nvoid recordSolution(List<TreeNode> state, List<List<TreeNode>> res) {\n    res.add(new ArrayList<>(state));\n}\n\n/* Проверить, допустим ли этот выбор в текущем состоянии */\nboolean isValid(List<TreeNode> state, TreeNode choice) {\n    return choice != null && choice.val != 3;\n}\n\n/* Обновить состояние */\nvoid makeChoice(List<TreeNode> state, TreeNode choice) {\n    state.add(choice);\n}\n\n/* Восстановить состояние */\nvoid undoChoice(List<TreeNode> state, TreeNode choice) {\n    state.remove(state.size() - 1);\n}\n\n/* Алгоритм бэктрекинга: пример 3 */\nvoid backtrack(List<TreeNode> state, List<TreeNode> choices, List<List<TreeNode>> res) {\n    // Проверить, является ли текущее состояние решением\n    if (isSolution(state)) {\n        // Записать решение\n        recordSolution(state, res);\n    }\n    // Перебор всех вариантов выбора\n    for (TreeNode choice : choices) {\n        // Отсечение: проверить допустимость выбора\n        if (isValid(state, choice)) {\n            // Попытка: сделать выбор и обновить состояние\n            makeChoice(state, choice);\n            // Перейти к следующему выбору\n            backtrack(state, Arrays.asList(choice.left, choice.right), res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            undoChoice(state, choice);\n        }\n    }\n}\n
        preorder_traversal_iii_template.cs
        /* Проверить, является ли текущее состояние решением */\nbool IsSolution(List<TreeNode> state) {\n    return state.Count != 0 && state[^1].val == 7;\n}\n\n/* Записать решение */\nvoid RecordSolution(List<TreeNode> state, List<List<TreeNode>> res) {\n    res.Add(new List<TreeNode>(state));\n}\n\n/* Проверить, допустим ли этот выбор в текущем состоянии */\nbool IsValid(List<TreeNode> state, TreeNode choice) {\n    return choice != null && choice.val != 3;\n}\n\n/* Обновить состояние */\nvoid MakeChoice(List<TreeNode> state, TreeNode choice) {\n    state.Add(choice);\n}\n\n/* Восстановить состояние */\nvoid UndoChoice(List<TreeNode> state, TreeNode choice) {\n    state.RemoveAt(state.Count - 1);\n}\n\n/* Алгоритм бэктрекинга: пример 3 */\nvoid Backtrack(List<TreeNode> state, List<TreeNode> choices, List<List<TreeNode>> res) {\n    // Проверить, является ли текущее состояние решением\n    if (IsSolution(state)) {\n        // Записать решение\n        RecordSolution(state, res);\n    }\n    // Перебор всех вариантов выбора\n    foreach (TreeNode choice in choices) {\n        // Отсечение: проверить допустимость выбора\n        if (IsValid(state, choice)) {\n            // Попытка: сделать выбор и обновить состояние\n            MakeChoice(state, choice);\n            // Перейти к следующему выбору\n            Backtrack(state, [choice.left!, choice.right!], res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            UndoChoice(state, choice);\n        }\n    }\n}\n
        preorder_traversal_iii_template.go
        /* Проверить, является ли текущее состояние решением */\nfunc isSolution(state *[]*TreeNode) bool {\n    return len(*state) != 0 && (*state)[len(*state)-1].Val == 7\n}\n\n/* Записать решение */\nfunc recordSolution(state *[]*TreeNode, res *[][]*TreeNode) {\n    *res = append(*res, append([]*TreeNode{}, *state...))\n}\n\n/* Проверить, допустим ли этот выбор в текущем состоянии */\nfunc isValid(state *[]*TreeNode, choice *TreeNode) bool {\n    return choice != nil && choice.Val != 3\n}\n\n/* Обновить состояние */\nfunc makeChoice(state *[]*TreeNode, choice *TreeNode) {\n    *state = append(*state, choice)\n}\n\n/* Восстановить состояние */\nfunc undoChoice(state *[]*TreeNode, choice *TreeNode) {\n    *state = (*state)[:len(*state)-1]\n}\n\n/* Алгоритм бэктрекинга: пример 3 */\nfunc backtrackIII(state *[]*TreeNode, choices *[]*TreeNode, res *[][]*TreeNode) {\n    // Проверить, является ли текущее состояние решением\n    if isSolution(state) {\n        // Записать решение\n        recordSolution(state, res)\n    }\n    // Перебор всех вариантов выбора\n    for _, choice := range *choices {\n        // Отсечение: проверить допустимость выбора\n        if isValid(state, choice) {\n            // Попытка: сделать выбор и обновить состояние\n            makeChoice(state, choice)\n            // Перейти к следующему выбору\n            temp := make([]*TreeNode, 0)\n            temp = append(temp, choice.Left, choice.Right)\n            backtrackIII(state, &temp, res)\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            undoChoice(state, choice)\n        }\n    }\n}\n
        preorder_traversal_iii_template.swift
        /* Проверить, является ли текущее состояние решением */\nfunc isSolution(state: [TreeNode]) -> Bool {\n    !state.isEmpty && state.last!.val == 7\n}\n\n/* Записать решение */\nfunc recordSolution(state: [TreeNode], res: inout [[TreeNode]]) {\n    res.append(state)\n}\n\n/* Проверить, допустим ли этот выбор в текущем состоянии */\nfunc isValid(state: [TreeNode], choice: TreeNode?) -> Bool {\n    choice != nil && choice!.val != 3\n}\n\n/* Обновить состояние */\nfunc makeChoice(state: inout [TreeNode], choice: TreeNode) {\n    state.append(choice)\n}\n\n/* Восстановить состояние */\nfunc undoChoice(state: inout [TreeNode], choice: TreeNode) {\n    state.removeLast()\n}\n\n/* Алгоритм бэктрекинга: пример 3 */\nfunc backtrack(state: inout [TreeNode], choices: [TreeNode], res: inout [[TreeNode]]) {\n    // Проверить, является ли текущее состояние решением\n    if isSolution(state: state) {\n        recordSolution(state: state, res: &res)\n    }\n    // Перебор всех вариантов выбора\n    for choice in choices {\n        // Отсечение: проверить допустимость выбора\n        if isValid(state: state, choice: choice) {\n            // Попытка: сделать выбор и обновить состояние\n            makeChoice(state: &state, choice: choice)\n            // Перейти к следующему выбору\n            backtrack(state: &state, choices: [choice.left, choice.right].compactMap { $0 }, res: &res)\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            undoChoice(state: &state, choice: choice)\n        }\n    }\n}\n
        preorder_traversal_iii_template.js
        /* Проверить, является ли текущее состояние решением */\nfunction isSolution(state) {\n    return state && state[state.length - 1]?.val === 7;\n}\n\n/* Записать решение */\nfunction recordSolution(state, res) {\n    res.push([...state]);\n}\n\n/* Проверить, допустим ли этот выбор в текущем состоянии */\nfunction isValid(state, choice) {\n    return choice !== null && choice.val !== 3;\n}\n\n/* Обновить состояние */\nfunction makeChoice(state, choice) {\n    state.push(choice);\n}\n\n/* Восстановить состояние */\nfunction undoChoice(state) {\n    state.pop();\n}\n\n/* Алгоритм бэктрекинга: пример 3 */\nfunction backtrack(state, choices, res) {\n    // Проверить, является ли текущее состояние решением\n    if (isSolution(state)) {\n        // Записать решение\n        recordSolution(state, res);\n    }\n    // Перебор всех вариантов выбора\n    for (const choice of choices) {\n        // Отсечение: проверить допустимость выбора\n        if (isValid(state, choice)) {\n            // Попытка: сделать выбор и обновить состояние\n            makeChoice(state, choice);\n            // Перейти к следующему выбору\n            backtrack(state, [choice.left, choice.right], res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            undoChoice(state);\n        }\n    }\n}\n
        preorder_traversal_iii_template.ts
        /* Проверить, является ли текущее состояние решением */\nfunction isSolution(state: TreeNode[]): boolean {\n    return state && state[state.length - 1]?.val === 7;\n}\n\n/* Записать решение */\nfunction recordSolution(state: TreeNode[], res: TreeNode[][]): void {\n    res.push([...state]);\n}\n\n/* Проверить, допустим ли этот выбор в текущем состоянии */\nfunction isValid(state: TreeNode[], choice: TreeNode): boolean {\n    return choice !== null && choice.val !== 3;\n}\n\n/* Обновить состояние */\nfunction makeChoice(state: TreeNode[], choice: TreeNode): void {\n    state.push(choice);\n}\n\n/* Восстановить состояние */\nfunction undoChoice(state: TreeNode[]): void {\n    state.pop();\n}\n\n/* Алгоритм бэктрекинга: пример 3 */\nfunction backtrack(\n    state: TreeNode[],\n    choices: TreeNode[],\n    res: TreeNode[][]\n): void {\n    // Проверить, является ли текущее состояние решением\n    if (isSolution(state)) {\n        // Записать решение\n        recordSolution(state, res);\n    }\n    // Перебор всех вариантов выбора\n    for (const choice of choices) {\n        // Отсечение: проверить допустимость выбора\n        if (isValid(state, choice)) {\n            // Попытка: сделать выбор и обновить состояние\n            makeChoice(state, choice);\n            // Перейти к следующему выбору\n            backtrack(state, [choice.left, choice.right], res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            undoChoice(state);\n        }\n    }\n}\n
        preorder_traversal_iii_template.dart
        /* Проверить, является ли текущее состояние решением */\nbool isSolution(List<TreeNode> state) {\n  return state.isNotEmpty && state.last.val == 7;\n}\n\n/* Записать решение */\nvoid recordSolution(List<TreeNode> state, List<List<TreeNode>> res) {\n  res.add(List.from(state));\n}\n\n/* Проверить, допустим ли этот выбор в текущем состоянии */\nbool isValid(List<TreeNode> state, TreeNode? choice) {\n  return choice != null && choice.val != 3;\n}\n\n/* Обновить состояние */\nvoid makeChoice(List<TreeNode> state, TreeNode? choice) {\n  state.add(choice!);\n}\n\n/* Восстановить состояние */\nvoid undoChoice(List<TreeNode> state, TreeNode? choice) {\n  state.removeLast();\n}\n\n/* Алгоритм бэктрекинга: пример 3 */\nvoid backtrack(\n  List<TreeNode> state,\n  List<TreeNode?> choices,\n  List<List<TreeNode>> res,\n) {\n  // Проверить, является ли текущее состояние решением\n  if (isSolution(state)) {\n    // Записать решение\n    recordSolution(state, res);\n  }\n  // Перебор всех вариантов выбора\n  for (TreeNode? choice in choices) {\n    // Отсечение: проверить допустимость выбора\n    if (isValid(state, choice)) {\n      // Попытка: сделать выбор и обновить состояние\n      makeChoice(state, choice);\n      // Перейти к следующему выбору\n      backtrack(state, [choice!.left, choice.right], res);\n      // Откат: отменить выбор и восстановить предыдущее состояние\n      undoChoice(state, choice);\n    }\n  }\n}\n
        preorder_traversal_iii_template.rs
        /* Проверить, является ли текущее состояние решением */\nfn is_solution(state: &mut Vec<Rc<RefCell<TreeNode>>>) -> bool {\n    return !state.is_empty() && state.last().unwrap().borrow().val == 7;\n}\n\n/* Записать решение */\nfn record_solution(\n    state: &mut Vec<Rc<RefCell<TreeNode>>>,\n    res: &mut Vec<Vec<Rc<RefCell<TreeNode>>>>,\n) {\n    res.push(state.clone());\n}\n\n/* Проверить, допустим ли этот выбор в текущем состоянии */\nfn is_valid(_: &mut Vec<Rc<RefCell<TreeNode>>>, choice: Option<&Rc<RefCell<TreeNode>>>) -> bool {\n    return choice.is_some() && choice.unwrap().borrow().val != 3;\n}\n\n/* Обновить состояние */\nfn make_choice(state: &mut Vec<Rc<RefCell<TreeNode>>>, choice: Rc<RefCell<TreeNode>>) {\n    state.push(choice);\n}\n\n/* Восстановить состояние */\nfn undo_choice(state: &mut Vec<Rc<RefCell<TreeNode>>>, _: Rc<RefCell<TreeNode>>) {\n    state.pop();\n}\n\n/* Алгоритм бэктрекинга: пример 3 */\nfn backtrack(\n    state: &mut Vec<Rc<RefCell<TreeNode>>>,\n    choices: &Vec<Option<&Rc<RefCell<TreeNode>>>>,\n    res: &mut Vec<Vec<Rc<RefCell<TreeNode>>>>,\n) {\n    // Проверить, является ли текущее состояние решением\n    if is_solution(state) {\n        // Записать решение\n        record_solution(state, res);\n    }\n    // Перебор всех вариантов выбора\n    for &choice in choices.iter() {\n        // Отсечение: проверить допустимость выбора\n        if is_valid(state, choice) {\n            // Попытка: сделать выбор и обновить состояние\n            make_choice(state, choice.unwrap().clone());\n            // Перейти к следующему выбору\n            backtrack(\n                state,\n                &vec![\n                    choice.unwrap().borrow().left.as_ref(),\n                    choice.unwrap().borrow().right.as_ref(),\n                ],\n                res,\n            );\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            undo_choice(state, choice.unwrap().clone());\n        }\n    }\n}\n
        preorder_traversal_iii_template.c
        /* Проверить, является ли текущее состояние решением */\nbool isSolution(void) {\n    return pathSize > 0 && path[pathSize - 1]->val == 7;\n}\n\n/* Записать решение */\nvoid recordSolution(void) {\n    for (int i = 0; i < pathSize; i++) {\n        res[resSize][i] = path[i];\n    }\n    resSize++;\n}\n\n/* Проверить, допустим ли этот выбор в текущем состоянии */\nbool isValid(TreeNode *choice) {\n    return choice != NULL && choice->val != 3;\n}\n\n/* Обновить состояние */\nvoid makeChoice(TreeNode *choice) {\n    path[pathSize++] = choice;\n}\n\n/* Восстановить состояние */\nvoid undoChoice(void) {\n    pathSize--;\n}\n\n/* Алгоритм бэктрекинга: пример 3 */\nvoid backtrack(TreeNode *choices[2]) {\n    // Проверить, является ли текущее состояние решением\n    if (isSolution()) {\n        // Записать решение\n        recordSolution();\n    }\n    // Перебор всех вариантов выбора\n    for (int i = 0; i < 2; i++) {\n        TreeNode *choice = choices[i];\n        // Отсечение: проверить допустимость выбора\n        if (isValid(choice)) {\n            // Попытка: сделать выбор и обновить состояние\n            makeChoice(choice);\n            // Перейти к следующему выбору\n            TreeNode *nextChoices[2] = {choice->left, choice->right};\n            backtrack(nextChoices);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            undoChoice();\n        }\n    }\n}\n
        preorder_traversal_iii_template.kt
        /* Проверить, является ли текущее состояние решением */\nfun isSolution(state: MutableList<TreeNode?>): Boolean {\n    return state.isNotEmpty() && state[state.size - 1]?._val == 7\n}\n\n/* Записать решение */\nfun recordSolution(state: MutableList<TreeNode?>?, res: MutableList<MutableList<TreeNode?>?>) {\n    res.add(state!!.toMutableList())\n}\n\n/* Проверить, допустим ли этот выбор в текущем состоянии */\nfun isValid(state: MutableList<TreeNode?>?, choice: TreeNode?): Boolean {\n    return choice != null && choice._val != 3\n}\n\n/* Обновить состояние */\nfun makeChoice(state: MutableList<TreeNode?>, choice: TreeNode?) {\n    state.add(choice)\n}\n\n/* Восстановить состояние */\nfun undoChoice(state: MutableList<TreeNode?>, choice: TreeNode?) {\n    state.removeLast()\n}\n\n/* Алгоритм бэктрекинга: пример 3 */\nfun backtrack(\n    state: MutableList<TreeNode?>,\n    choices: MutableList<TreeNode?>,\n    res: MutableList<MutableList<TreeNode?>?>\n) {\n    // Проверить, является ли текущее состояние решением\n    if (isSolution(state)) {\n        // Записать решение\n        recordSolution(state, res)\n    }\n    // Перебор всех вариантов выбора\n    for (choice in choices) {\n        // Отсечение: проверить допустимость выбора\n        if (isValid(state, choice)) {\n            // Попытка: сделать выбор и обновить состояние\n            makeChoice(state, choice)\n            // Перейти к следующему выбору\n            backtrack(state, mutableListOf(choice!!.left, choice.right), res)\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            undoChoice(state, choice)\n        }\n    }\n}\n
        preorder_traversal_iii_template.rb
        ### Проверка, является ли текущее состояние решением ###\ndef is_solution?(state)\n  !state.empty? && state.last.val == 7\nend\n\n### Записать решение ###\ndef record_solution(state, res)\n  res << state.dup\nend\n\n### Проверка допустимости этого выбора в текущем состоянии ###\ndef is_valid?(state, choice)\n  choice && choice.val != 3\nend\n\n### Обновить состояние ###\ndef make_choice(state, choice)\n  state << choice\nend\n\n### Восстановить состояние ###\ndef undo_choice(state, choice)\n  state.pop\nend\n\n### Алгоритм бэктрекинга: пример 3 ###\ndef backtrack(state, choices, res)\n  # Проверить, является ли текущее состояние решением\n  record_solution(state, res) if is_solution?(state)\n\n  # Перебор всех вариантов выбора\n  for choice in choices\n    # Отсечение: проверить допустимость выбора\n    if is_valid?(state, choice)\n      # Попытка: сделать выбор и обновить состояние\n      make_choice(state, choice)\n      # Перейти к следующему выбору\n      backtrack(state, [choice.left, choice.right], res)\n      # Откат: отменить выбор и восстановить предыдущее состояние\n      undo_choice(state, choice)\n    end\n  end\nend\n
        Визуализация кода

        Во весь экран >

        Согласно условию задачи, после нахождения узла со значением \\(7\\) мы должны продолжать поиск, поэтому оператор return после записи решения нужно удалить. На рисунке 13-4 сравниваются процессы поиска в случаях, когда return сохраняется и когда он удаляется.

        Рисунок 13-4   Сравнение поиска при сохранении и удалении return

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

        ","path":["Глава 13. Поиск с возвратом","13.1   Алгоритм поиска с возвратом"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1314","level":2,"title":"13.1.4   Часто используемые термины","text":"

        Чтобы яснее анализировать алгоритмические задачи, подытожим значения часто используемых терминов поиска с возвратом и сопоставим их с примером 3, как показано в таблице 13-1.

        Таблица 13-1   Часто используемые термины алгоритма поиска с возвратом

        Термин Определение Пример 3 Решение (solution) Решение - это ответ, удовлетворяющий условиям задачи; решений может быть одно или несколько Все пути от корня до узла \\(7\\) , удовлетворяющие ограничениям Ограничение (constraint) Ограничение определяет допустимость решения и обычно используется для обрезки Путь не содержит узлы со значением \\(3\\) Состояние (state) Состояние описывает ситуацию задачи в некоторый момент времени, включая уже сделанные выборы Текущий путь посещенных узлов, то есть список узлов path Попытка (attempt) Попытка - это исследование пространства решений на основе доступных выборов, включая выбор, обновление состояния и проверку, является ли состояние решением Рекурсивный переход к левому или правому потомку, добавление узла в path и проверка, равно ли значение узла \\(7\\) Откат (backtracking) Откат означает отмену предыдущих выборов и возврат к более раннему состоянию при встрече состояния, не удовлетворяющего ограничениям Завершение поиска при проходе через лист, окончании посещения узла или встрече узла со значением \\(3\\) , то есть возврат из функции Обрезка (pruning) Обрезка - это способ избегать бессмысленных путей поиска на основе свойств задачи и ее ограничений, повышающий эффективность При встрече узла со значением \\(3\\) поиск по этой ветви прекращается

        Tip

        Такие понятия, как задача, решение и состояние, являются общими и встречаются не только в поиске с возвратом, но и в \"разделяй и властвуй\", динамическом программировании, жадных алгоритмах и других темах.

        ","path":["Глава 13. Поиск с возвратом","13.1   Алгоритм поиска с возвратом"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1315","level":2,"title":"13.1.5   Преимущества и ограничения","text":"

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

        Однако при работе с большими или сложными задачами эффективность поиска с возвратом может оказаться неприемлемой.

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

        Тем не менее поиск с возвратом по-прежнему остается лучшим решением для некоторых поисковых задач и задач удовлетворения ограничений. В таких задачах заранее невозможно предсказать, какие выборы приведут к эффективному решению, поэтому приходится перебирать все возможные варианты. В этой ситуации ключевым становится вопрос оптимизации эффективности , и для этого обычно используют две стратегии.

        • Обрезка: избегать поиска по тем путям, которые заведомо не приведут к решению, тем самым экономя время и память.
        • Эвристический поиск: вводить во время поиска дополнительные стратегии или оценки, чтобы в первую очередь исследовать пути, наиболее вероятно ведущие к эффективному решению.
        ","path":["Глава 13. Поиск с возвратом","13.1   Алгоритм поиска с возвратом"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1316","level":2,"title":"13.1.6   Типичные задачи поиска с возвратом","text":"

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

        Поисковые задачи: целью таких задач является поиск решений, удовлетворяющих определенным условиям.

        • Задача о перестановках: дано множество, требуется найти все возможные перестановки его элементов.
        • Задача о сумме подмножеств: даны множество и целевая сумма; нужно найти все подмножества, сумма элементов которых равна целевой.
        • Задача о Ханойской башне: даны три стержня и набор дисков разного размера; требуется перенести все диски с одного стержня на другой, перемещая за раз только один диск и не помещая больший диск на меньший.

        Задачи удовлетворения ограничений: целью таких задач является поиск решений, удовлетворяющих всем ограничениям.

        • Задача о \\(n\\) ферзях: разместить \\(n\\) ферзей на шахматной доске размера \\(n \\times n\\) так, чтобы они не атаковали друг друга.
        • Судоку: заполнить сетку \\(9 \\times 9\\) числами от \\(1\\) до \\(9\\) так, чтобы в каждой строке, каждом столбце и каждом блоке \\(3 \\times 3\\) числа не повторялись.
        • Задача раскраски графа: дан неориентированный граф; требуется раскрасить его вершины минимальным числом цветов так, чтобы соседние вершины имели разные цвета.

        Задачи комбинаторной оптимизации: целью таких задач является поиск оптимального решения в некотором комбинаторном пространстве при заданных ограничениях.

        • Задача о рюкзаке 0-1: даны набор предметов и рюкзак; у каждого предмета есть ценность и вес, и нужно выбрать предметы так, чтобы при ограниченной вместимости рюкзака суммарная ценность была максимальной.
        • Задача коммивояжера: начиная из некоторой вершины графа, требуется посетить все остальные вершины ровно по одному разу и вернуться в исходную вершину, найдя при этом кратчайший путь.
        • Задача о максимальной клике: дан неориентированный граф; требуется найти в нем максимальный полный подграф, то есть подграф, в котором любая пара вершин соединена ребром.

        Стоит отметить: для многих задач комбинаторной оптимизации поиск с возвратом не является оптимальным способом решения.

        • Задача о рюкзаке 0-1 обычно решается с помощью динамического программирования, что дает более высокую временную эффективность.
        • Задача коммивояжера является известной NP-Hard задачей; для ее решения часто используют генетические алгоритмы, муравьиные алгоритмы и другие методы.
        • Задача о максимальной клике является классической задачей теории графов и может решаться жадными и другими эвристическими алгоритмами.
        ","path":["Глава 13. Поиск с возвратом","13.1   Алгоритм поиска с возвратом"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/","level":1,"title":"13.4   Задача о n ферзях","text":"

        Question

        Согласно правилам шахмат ферзь может атаковать фигуры, находящиеся с ним на одной строке, в одном столбце или на одной диагонали. Даны \\(n\\) ферзей и шахматная доска размера \\(n \\times n\\) ; требуется найти такие расстановки, при которых ни одна пара ферзей не может атаковать друг друга.

        Как показано на рисунке 13-15, при \\(n = 4\\) существует два решения. С точки зрения поиска с возвратом доска размера \\(n \\times n\\) содержит \\(n^2\\) клеток, которые образуют все возможные выборы choices . По мере поочередного размещения ферзей состояние доски непрерывно меняется, и текущее содержимое доски образует состояние state .

        Рисунок 13-15   Решения задачи о 4 ферзях

        На рисунке 13-16 показаны три ограничения этой задачи: несколько ферзей не могут находиться на одной строке, в одном столбце или на одной диагонали. При этом нужно помнить, что диагонали бывают двух типов: главная \\ и побочная / .

        Рисунок 13-16   Ограничения задачи о n ферзях

        ","path":["Глава 13. Поиск с возвратом","13.4   Задача о n ферзях"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/#1","level":3,"title":"1.   Построчная стратегия размещения","text":"

        Число ферзей и число строк доски одинаково и равно \\(n\\) , поэтому легко получить следующий вывод: в каждой строке доски разрешено и нужно разместить ровно одного ферзя.

        Иначе говоря, можно использовать построчную стратегию: начиная с первой строки, размещать по одному ферзю в каждой строке, пока не будет достигнута последняя.

        На рисунке 13-17 показан процесс построчного размещения для задачи о 4 ферзях. Из-за ограничений размера изображения на нем раскрыта только одна ветвь поиска для первой строки, а все варианты, не удовлетворяющие ограничениям по столбцам и диагоналям, были отсечены.

        Рисунок 13-17   Построчная стратегия размещения

        По своей сути построчная стратегия сама по себе выполняет роль обрезки , потому что заранее исключает все ветви поиска, в которых в одной строке оказалось бы несколько ферзей.

        ","path":["Глава 13. Поиск с возвратом","13.4   Задача о n ферзях"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/#2","level":3,"title":"2.   Обрезка по столбцам и диагоналям","text":"

        Чтобы удовлетворить ограничению по столбцам, можно использовать булев массив cols длины \\(n\\) , который записывает, есть ли ферзь в каждом столбце. Перед каждым размещением мы используем cols для отсечения столбцов, уже занятых ферзями, а затем динамически обновляем состояние cols во время отката.

        Tip

        Обратите внимание: начало координат матрицы находится в левом верхнем углу, при этом индексы строк растут сверху вниз, а индексы столбцов - слева направо.

        Как теперь обработать ограничения по диагоналям? Пусть клетка на доске имеет координаты \\((row, col)\\) . Выбрав некоторую главную диагональ в матрице, можно заметить, что разность индексов строки и столбца одинакова для всех клеток этой диагонали, то есть для всех клеток главной диагонали значение \\(row - col\\) постоянно.

        Это означает, что если для двух клеток выполняется равенство \\(row_1 - col_1 = row_2 - col_2\\) , то они обязательно лежат на одной и той же главной диагонали. Используя это правило, можно с помощью массива diags1 , показанного на рисунке 13-18, отмечать наличие ферзя на каждой главной диагонали.

        Аналогично для всех клеток побочной диагонали значение \\(row + col\\) является постоянным. Поэтому для обработки ограничений по побочным диагоналям можно использовать еще один массив diags2 .

        Рисунок 13-18   Обработка ограничений по столбцам и диагоналям

        ","path":["Глава 13. Поиск с возвратом","13.4   Задача о n ферзях"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/#3","level":3,"title":"3.   Реализация кода","text":"

        Заметим, что в квадратной матрице размера \\(n\\) диапазон значений \\(row - col\\) равен \\([-n + 1, n - 1]\\) , а диапазон значений \\(row + col\\) равен \\([0, 2n - 2]\\) . Следовательно, число главных и побочных диагоналей равно \\(2n - 1\\) , а значит, длины массивов diags1 и diags2 тоже равны \\(2n - 1\\) .

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby n_queens.py
        def backtrack(\n    row: int,\n    n: int,\n    state: list[list[str]],\n    res: list[list[list[str]]],\n    cols: list[bool],\n    diags1: list[bool],\n    diags2: list[bool],\n):\n    \"\"\"Алгоритм бэктрекинга: n ферзей\"\"\"\n    # Когда все строки уже обработаны, записать решение\n    if row == n:\n        res.append([list(row) for row in state])\n        return\n    # Обойти все столбцы\n    for col in range(n):\n        # Вычислить главную и побочную диагонали, соответствующие этой клетке\n        diag1 = row - col + n - 1\n        diag2 = row + col\n        # Отсечение: в столбце, главной диагонали и побочной диагонали этой клетки не должно быть ферзей\n        if not cols[col] and not diags1[diag1] and not diags2[diag2]:\n            # Попытка: поставить ферзя в эту клетку\n            state[row][col] = \"Q\"\n            cols[col] = diags1[diag1] = diags2[diag2] = True\n            # Перейти к размещению следующей строки\n            backtrack(row + 1, n, state, res, cols, diags1, diags2)\n            # Откат: восстановить эту клетку как пустую\n            state[row][col] = \"#\"\n            cols[col] = diags1[diag1] = diags2[diag2] = False\n\ndef n_queens(n: int) -> list[list[list[str]]]:\n    \"\"\"Решить задачу о n ферзях\"\"\"\n    # Инициализировать доску размера n*n, где 'Q' обозначает ферзя, а '#' — пустую клетку\n    state = [[\"#\" for _ in range(n)] for _ in range(n)]\n    cols = [False] * n  # Отмечать, есть ли ферзь в столбце\n    diags1 = [False] * (2 * n - 1)  # Отмечать наличие ферзя на главной диагонали\n    diags2 = [False] * (2 * n - 1)  # Отмечать наличие ферзя на побочной диагонали\n    res = []\n    backtrack(0, n, state, res, cols, diags1, diags2)\n\n    return res\n
        n_queens.cpp
        /* Алгоритм бэктрекинга: n ферзей */\nvoid backtrack(int row, int n, vector<vector<string>> &state, vector<vector<vector<string>>> &res, vector<bool> &cols,\n               vector<bool> &diags1, vector<bool> &diags2) {\n    // Когда все строки уже обработаны, записать решение\n    if (row == n) {\n        res.push_back(state);\n        return;\n    }\n    // Обойти все столбцы\n    for (int col = 0; col < n; col++) {\n        // Вычислить главную и побочную диагонали, соответствующие этой клетке\n        int diag1 = row - col + n - 1;\n        int diag2 = row + col;\n        // Отсечение: в столбце, главной диагонали и побочной диагонали этой клетки не должно быть ферзей\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Попытка: поставить ферзя в эту клетку\n            state[row][col] = \"Q\";\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // Перейти к размещению следующей строки\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // Откат: восстановить эту клетку как пустую\n            state[row][col] = \"#\";\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* Решить задачу о n ферзях */\nvector<vector<vector<string>>> nQueens(int n) {\n    // Инициализировать доску размера n*n, где 'Q' обозначает ферзя, а '#' — пустую клетку\n    vector<vector<string>> state(n, vector<string>(n, \"#\"));\n    vector<bool> cols(n, false);           // Отмечать, есть ли ферзь в столбце\n    vector<bool> diags1(2 * n - 1, false); // Отмечать наличие ферзя на главной диагонали\n    vector<bool> diags2(2 * n - 1, false); // Отмечать наличие ферзя на побочной диагонали\n    vector<vector<vector<string>>> res;\n\n    backtrack(0, n, state, res, cols, diags1, diags2);\n\n    return res;\n}\n
        n_queens.java
        /* Алгоритм бэктрекинга: n ферзей */\nvoid backtrack(int row, int n, List<List<String>> state, List<List<List<String>>> res,\n        boolean[] cols, boolean[] diags1, boolean[] diags2) {\n    // Когда все строки уже обработаны, записать решение\n    if (row == n) {\n        List<List<String>> copyState = new ArrayList<>();\n        for (List<String> sRow : state) {\n            copyState.add(new ArrayList<>(sRow));\n        }\n        res.add(copyState);\n        return;\n    }\n    // Обойти все столбцы\n    for (int col = 0; col < n; col++) {\n        // Вычислить главную и побочную диагонали, соответствующие этой клетке\n        int diag1 = row - col + n - 1;\n        int diag2 = row + col;\n        // Отсечение: в столбце, главной диагонали и побочной диагонали этой клетки не должно быть ферзей\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Попытка: поставить ферзя в эту клетку\n            state.get(row).set(col, \"Q\");\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // Перейти к размещению следующей строки\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // Откат: восстановить эту клетку как пустую\n            state.get(row).set(col, \"#\");\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* Решить задачу о n ферзях */\nList<List<List<String>>> nQueens(int n) {\n    // Инициализировать доску размера n*n, где 'Q' обозначает ферзя, а '#' — пустую клетку\n    List<List<String>> state = new ArrayList<>();\n    for (int i = 0; i < n; i++) {\n        List<String> row = new ArrayList<>();\n        for (int j = 0; j < n; j++) {\n            row.add(\"#\");\n        }\n        state.add(row);\n    }\n    boolean[] cols = new boolean[n]; // Отмечать, есть ли ферзь в столбце\n    boolean[] diags1 = new boolean[2 * n - 1]; // Отмечать наличие ферзя на главной диагонали\n    boolean[] diags2 = new boolean[2 * n - 1]; // Отмечать наличие ферзя на побочной диагонали\n    List<List<List<String>>> res = new ArrayList<>();\n\n    backtrack(0, n, state, res, cols, diags1, diags2);\n\n    return res;\n}\n
        n_queens.cs
        /* Алгоритм бэктрекинга: n ферзей */\nvoid Backtrack(int row, int n, List<List<string>> state, List<List<List<string>>> res,\n        bool[] cols, bool[] diags1, bool[] diags2) {\n    // Когда все строки уже обработаны, записать решение\n    if (row == n) {\n        List<List<string>> copyState = [];\n        foreach (List<string> sRow in state) {\n            copyState.Add(new List<string>(sRow));\n        }\n        res.Add(copyState);\n        return;\n    }\n    // Обойти все столбцы\n    for (int col = 0; col < n; col++) {\n        // Вычислить главную и побочную диагонали, соответствующие этой клетке\n        int diag1 = row - col + n - 1;\n        int diag2 = row + col;\n        // Отсечение: в столбце, главной диагонали и побочной диагонали этой клетки не должно быть ферзей\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Попытка: поставить ферзя в эту клетку\n            state[row][col] = \"Q\";\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // Перейти к размещению следующей строки\n            Backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // Откат: восстановить эту клетку как пустую\n            state[row][col] = \"#\";\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* Решить задачу о n ферзях */\nList<List<List<string>>> NQueens(int n) {\n    // Инициализировать доску размера n*n, где 'Q' обозначает ферзя, а '#' — пустую клетку\n    List<List<string>> state = [];\n    for (int i = 0; i < n; i++) {\n        List<string> row = [];\n        for (int j = 0; j < n; j++) {\n            row.Add(\"#\");\n        }\n        state.Add(row);\n    }\n    bool[] cols = new bool[n]; // Отмечать, есть ли ферзь в столбце\n    bool[] diags1 = new bool[2 * n - 1]; // Отмечать наличие ферзя на главной диагонали\n    bool[] diags2 = new bool[2 * n - 1]; // Отмечать наличие ферзя на побочной диагонали\n    List<List<List<string>>> res = [];\n\n    Backtrack(0, n, state, res, cols, diags1, diags2);\n\n    return res;\n}\n
        n_queens.go
        /* Алгоритм бэктрекинга: n ферзей */\nfunc backtrack(row, n int, state *[][]string, res *[][][]string, cols, diags1, diags2 *[]bool) {\n    // Когда все строки уже обработаны, записать решение\n    if row == n {\n        newState := make([][]string, len(*state))\n        for i, _ := range newState {\n            newState[i] = make([]string, len((*state)[0]))\n            copy(newState[i], (*state)[i])\n\n        }\n        *res = append(*res, newState)\n        return\n    }\n    // Обойти все столбцы\n    for col := 0; col < n; col++ {\n        // Вычислить главную и побочную диагонали, соответствующие этой клетке\n        diag1 := row - col + n - 1\n        diag2 := row + col\n        // Отсечение: в столбце, главной диагонали и побочной диагонали этой клетки не должно быть ферзей\n        if !(*cols)[col] && !(*diags1)[diag1] && !(*diags2)[diag2] {\n            // Попытка: поставить ферзя в эту клетку\n            (*state)[row][col] = \"Q\"\n            (*cols)[col], (*diags1)[diag1], (*diags2)[diag2] = true, true, true\n            // Перейти к размещению следующей строки\n            backtrack(row+1, n, state, res, cols, diags1, diags2)\n            // Откат: восстановить эту клетку как пустую\n            (*state)[row][col] = \"#\"\n            (*cols)[col], (*diags1)[diag1], (*diags2)[diag2] = false, false, false\n        }\n    }\n}\n\n/* Решить задачу о n ферзях */\nfunc nQueens(n int) [][][]string {\n    // Инициализировать доску размера n*n, где 'Q' обозначает ферзя, а '#' — пустую клетку\n    state := make([][]string, n)\n    for i := 0; i < n; i++ {\n        row := make([]string, n)\n        for i := 0; i < n; i++ {\n            row[i] = \"#\"\n        }\n        state[i] = row\n    }\n    // Отмечать, есть ли ферзь в столбце\n    cols := make([]bool, n)\n    diags1 := make([]bool, 2*n-1)\n    diags2 := make([]bool, 2*n-1)\n    res := make([][][]string, 0)\n    backtrack(0, n, &state, &res, &cols, &diags1, &diags2)\n    return res\n}\n
        n_queens.swift
        /* Алгоритм бэктрекинга: n ферзей */\nfunc backtrack(row: Int, n: Int, state: inout [[String]], res: inout [[[String]]], cols: inout [Bool], diags1: inout [Bool], diags2: inout [Bool]) {\n    // Когда все строки уже обработаны, записать решение\n    if row == n {\n        res.append(state)\n        return\n    }\n    // Обойти все столбцы\n    for col in 0 ..< n {\n        // Вычислить главную и побочную диагонали, соответствующие этой клетке\n        let diag1 = row - col + n - 1\n        let diag2 = row + col\n        // Отсечение: в столбце, главной диагонали и побочной диагонали этой клетки не должно быть ферзей\n        if !cols[col] && !diags1[diag1] && !diags2[diag2] {\n            // Попытка: поставить ферзя в эту клетку\n            state[row][col] = \"Q\"\n            cols[col] = true\n            diags1[diag1] = true\n            diags2[diag2] = true\n            // Перейти к размещению следующей строки\n            backtrack(row: row + 1, n: n, state: &state, res: &res, cols: &cols, diags1: &diags1, diags2: &diags2)\n            // Откат: восстановить эту клетку как пустую\n            state[row][col] = \"#\"\n            cols[col] = false\n            diags1[diag1] = false\n            diags2[diag2] = false\n        }\n    }\n}\n\n/* Решить задачу о n ферзях */\nfunc nQueens(n: Int) -> [[[String]]] {\n    // Инициализировать доску размера n*n, где 'Q' обозначает ферзя, а '#' — пустую клетку\n    var state = Array(repeating: Array(repeating: \"#\", count: n), count: n)\n    var cols = Array(repeating: false, count: n) // Отмечать, есть ли ферзь в столбце\n    var diags1 = Array(repeating: false, count: 2 * n - 1) // Отмечать наличие ферзя на главной диагонали\n    var diags2 = Array(repeating: false, count: 2 * n - 1) // Отмечать наличие ферзя на побочной диагонали\n    var res: [[[String]]] = []\n\n    backtrack(row: 0, n: n, state: &state, res: &res, cols: &cols, diags1: &diags1, diags2: &diags2)\n\n    return res\n}\n
        n_queens.js
        /* Алгоритм бэктрекинга: n ферзей */\nfunction backtrack(row, n, state, res, cols, diags1, diags2) {\n    // Когда все строки уже обработаны, записать решение\n    if (row === n) {\n        res.push(state.map((row) => row.slice()));\n        return;\n    }\n    // Обойти все столбцы\n    for (let col = 0; col < n; col++) {\n        // Вычислить главную и побочную диагонали, соответствующие этой клетке\n        const diag1 = row - col + n - 1;\n        const diag2 = row + col;\n        // Отсечение: в столбце, главной диагонали и побочной диагонали этой клетки не должно быть ферзей\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Попытка: поставить ферзя в эту клетку\n            state[row][col] = 'Q';\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // Перейти к размещению следующей строки\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // Откат: восстановить эту клетку как пустую\n            state[row][col] = '#';\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* Решить задачу о n ферзях */\nfunction nQueens(n) {\n    // Инициализировать доску размера n*n, где 'Q' обозначает ферзя, а '#' — пустую клетку\n    const state = Array.from({ length: n }, () => Array(n).fill('#'));\n    const cols = Array(n).fill(false); // Отмечать, есть ли ферзь в столбце\n    const diags1 = Array(2 * n - 1).fill(false); // Отмечать наличие ферзя на главной диагонали\n    const diags2 = Array(2 * n - 1).fill(false); // Отмечать наличие ферзя на побочной диагонали\n    const res = [];\n\n    backtrack(0, n, state, res, cols, diags1, diags2);\n    return res;\n}\n
        n_queens.ts
        /* Алгоритм бэктрекинга: n ферзей */\nfunction backtrack(\n    row: number,\n    n: number,\n    state: string[][],\n    res: string[][][],\n    cols: boolean[],\n    diags1: boolean[],\n    diags2: boolean[]\n): void {\n    // Когда все строки уже обработаны, записать решение\n    if (row === n) {\n        res.push(state.map((row) => row.slice()));\n        return;\n    }\n    // Обойти все столбцы\n    for (let col = 0; col < n; col++) {\n        // Вычислить главную и побочную диагонали, соответствующие этой клетке\n        const diag1 = row - col + n - 1;\n        const diag2 = row + col;\n        // Отсечение: в столбце, главной диагонали и побочной диагонали этой клетки не должно быть ферзей\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Попытка: поставить ферзя в эту клетку\n            state[row][col] = 'Q';\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // Перейти к размещению следующей строки\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // Откат: восстановить эту клетку как пустую\n            state[row][col] = '#';\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* Решить задачу о n ферзях */\nfunction nQueens(n: number): string[][][] {\n    // Инициализировать доску размера n*n, где 'Q' обозначает ферзя, а '#' — пустую клетку\n    const state = Array.from({ length: n }, () => Array(n).fill('#'));\n    const cols = Array(n).fill(false); // Отмечать, есть ли ферзь в столбце\n    const diags1 = Array(2 * n - 1).fill(false); // Отмечать наличие ферзя на главной диагонали\n    const diags2 = Array(2 * n - 1).fill(false); // Отмечать наличие ферзя на побочной диагонали\n    const res: string[][][] = [];\n\n    backtrack(0, n, state, res, cols, diags1, diags2);\n    return res;\n}\n
        n_queens.dart
        /* Алгоритм бэктрекинга: n ферзей */\nvoid backtrack(\n  int row,\n  int n,\n  List<List<String>> state,\n  List<List<List<String>>> res,\n  List<bool> cols,\n  List<bool> diags1,\n  List<bool> diags2,\n) {\n  // Когда все строки уже обработаны, записать решение\n  if (row == n) {\n    List<List<String>> copyState = [];\n    for (List<String> sRow in state) {\n      copyState.add(List.from(sRow));\n    }\n    res.add(copyState);\n    return;\n  }\n  // Обойти все столбцы\n  for (int col = 0; col < n; col++) {\n    // Вычислить главную и побочную диагонали, соответствующие этой клетке\n    int diag1 = row - col + n - 1;\n    int diag2 = row + col;\n    // Отсечение: в столбце, главной диагонали и побочной диагонали этой клетки не должно быть ферзей\n    if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n      // Попытка: поставить ферзя в эту клетку\n      state[row][col] = \"Q\";\n      cols[col] = true;\n      diags1[diag1] = true;\n      diags2[diag2] = true;\n      // Перейти к размещению следующей строки\n      backtrack(row + 1, n, state, res, cols, diags1, diags2);\n      // Откат: восстановить эту клетку как пустую\n      state[row][col] = \"#\";\n      cols[col] = false;\n      diags1[diag1] = false;\n      diags2[diag2] = false;\n    }\n  }\n}\n\n/* Решить задачу о n ферзях */\nList<List<List<String>>> nQueens(int n) {\n  // Инициализировать доску размера n*n, где 'Q' обозначает ферзя, а '#' — пустую клетку\n  List<List<String>> state = List.generate(n, (index) => List.filled(n, \"#\"));\n  List<bool> cols = List.filled(n, false); // Отмечать, есть ли ферзь в столбце\n  List<bool> diags1 = List.filled(2 * n - 1, false); // Отмечать наличие ферзя на главной диагонали\n  List<bool> diags2 = List.filled(2 * n - 1, false); // Отмечать наличие ферзя на побочной диагонали\n  List<List<List<String>>> res = [];\n\n  backtrack(0, n, state, res, cols, diags1, diags2);\n\n  return res;\n}\n
        n_queens.rs
        /* Алгоритм бэктрекинга: n ферзей */\nfn backtrack(\n    row: usize,\n    n: usize,\n    state: &mut Vec<Vec<String>>,\n    res: &mut Vec<Vec<Vec<String>>>,\n    cols: &mut [bool],\n    diags1: &mut [bool],\n    diags2: &mut [bool],\n) {\n    // Когда все строки уже обработаны, записать решение\n    if row == n {\n        res.push(state.clone());\n        return;\n    }\n    // Обойти все столбцы\n    for col in 0..n {\n        // Вычислить главную и побочную диагонали, соответствующие этой клетке\n        let diag1 = row + n - 1 - col;\n        let diag2 = row + col;\n        // Отсечение: в столбце, главной диагонали и побочной диагонали этой клетки не должно быть ферзей\n        if !cols[col] && !diags1[diag1] && !diags2[diag2] {\n            // Попытка: поставить ферзя в эту клетку\n            state[row][col] = \"Q\".into();\n            (cols[col], diags1[diag1], diags2[diag2]) = (true, true, true);\n            // Перейти к размещению следующей строки\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // Откат: восстановить эту клетку как пустую\n            state[row][col] = \"#\".into();\n            (cols[col], diags1[diag1], diags2[diag2]) = (false, false, false);\n        }\n    }\n}\n\n/* Решить задачу о n ферзях */\nfn n_queens(n: usize) -> Vec<Vec<Vec<String>>> {\n    // Инициализировать доску размера n*n, где 'Q' обозначает ферзя, а '#' — пустую клетку\n    let mut state: Vec<Vec<String>> = vec![vec![\"#\".to_string(); n]; n];\n    let mut cols = vec![false; n]; // Отмечать, есть ли ферзь в столбце\n    let mut diags1 = vec![false; 2 * n - 1]; // Отмечать наличие ферзя на главной диагонали\n    let mut diags2 = vec![false; 2 * n - 1]; // Отмечать наличие ферзя на побочной диагонали\n    let mut res: Vec<Vec<Vec<String>>> = Vec::new();\n\n    backtrack(\n        0,\n        n,\n        &mut state,\n        &mut res,\n        &mut cols,\n        &mut diags1,\n        &mut diags2,\n    );\n\n    res\n}\n
        n_queens.c
        /* Алгоритм бэктрекинга: n ферзей */\nvoid backtrack(int row, int n, char state[MAX_SIZE][MAX_SIZE], char ***res, int *resSize, bool cols[MAX_SIZE],\n               bool diags1[2 * MAX_SIZE - 1], bool diags2[2 * MAX_SIZE - 1]) {\n    // Когда все строки уже обработаны, записать решение\n    if (row == n) {\n        res[*resSize] = (char **)malloc(sizeof(char *) * n);\n        for (int i = 0; i < n; ++i) {\n            res[*resSize][i] = (char *)malloc(sizeof(char) * (n + 1));\n            strcpy(res[*resSize][i], state[i]);\n        }\n        (*resSize)++;\n        return;\n    }\n    // Обойти все столбцы\n    for (int col = 0; col < n; col++) {\n        // Вычислить главную и побочную диагонали, соответствующие этой клетке\n        int diag1 = row - col + n - 1;\n        int diag2 = row + col;\n        // Отсечение: в столбце, главной диагонали и побочной диагонали этой клетки не должно быть ферзей\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Попытка: поставить ферзя в эту клетку\n            state[row][col] = 'Q';\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // Перейти к размещению следующей строки\n            backtrack(row + 1, n, state, res, resSize, cols, diags1, diags2);\n            // Откат: восстановить эту клетку как пустую\n            state[row][col] = '#';\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* Решить задачу о n ферзях */\nchar ***nQueens(int n, int *returnSize) {\n    char state[MAX_SIZE][MAX_SIZE];\n    // Инициализировать доску размера n*n, где 'Q' обозначает ферзя, а '#' — пустую клетку\n    for (int i = 0; i < n; ++i) {\n        for (int j = 0; j < n; ++j) {\n            state[i][j] = '#';\n        }\n        state[i][n] = '\\0';\n    }\n    bool cols[MAX_SIZE] = {false};           // Отмечать, есть ли ферзь в столбце\n    bool diags1[2 * MAX_SIZE - 1] = {false}; // Отмечать наличие ферзя на главной диагонали\n    bool diags2[2 * MAX_SIZE - 1] = {false}; // Отмечать наличие ферзя на побочной диагонали\n\n    char ***res = (char ***)malloc(sizeof(char **) * MAX_SIZE);\n    *returnSize = 0;\n    backtrack(0, n, state, res, returnSize, cols, diags1, diags2);\n    return res;\n}\n
        n_queens.kt
        /* Алгоритм бэктрекинга: n ферзей */\nfun backtrack(\n    row: Int,\n    n: Int,\n    state: MutableList<MutableList<String>>,\n    res: MutableList<MutableList<MutableList<String>>?>,\n    cols: BooleanArray,\n    diags1: BooleanArray,\n    diags2: BooleanArray\n) {\n    // Когда все строки уже обработаны, записать решение\n    if (row == n) {\n        val copyState = mutableListOf<MutableList<String>>()\n        for (sRow in state) {\n            copyState.add(sRow.toMutableList())\n        }\n        res.add(copyState)\n        return\n    }\n    // Обойти все столбцы\n    for (col in 0..<n) {\n        // Вычислить главную и побочную диагонали, соответствующие этой клетке\n        val diag1 = row - col + n - 1\n        val diag2 = row + col\n        // Отсечение: в столбце, главной диагонали и побочной диагонали этой клетки не должно быть ферзей\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Попытка: поставить ферзя в эту клетку\n            state[row][col] = \"Q\"\n            diags2[diag2] = true\n            diags1[diag1] = diags2[diag2]\n            cols[col] = diags1[diag1]\n            // Перейти к размещению следующей строки\n            backtrack(row + 1, n, state, res, cols, diags1, diags2)\n            // Откат: восстановить эту клетку как пустую\n            state[row][col] = \"#\"\n            diags2[diag2] = false\n            diags1[diag1] = diags2[diag2]\n            cols[col] = diags1[diag1]\n        }\n    }\n}\n\n/* Решить задачу о n ферзях */\nfun nQueens(n: Int): MutableList<MutableList<MutableList<String>>?> {\n    // Инициализировать доску размера n*n, где 'Q' обозначает ферзя, а '#' — пустую клетку\n    val state = mutableListOf<MutableList<String>>()\n    for (i in 0..<n) {\n        val row = mutableListOf<String>()\n        for (j in 0..<n) {\n            row.add(\"#\")\n        }\n        state.add(row)\n    }\n    val cols = BooleanArray(n) // Отмечать, есть ли ферзь в столбце\n    val diags1 = BooleanArray(2 * n - 1) // Отмечать наличие ферзя на главной диагонали\n    val diags2 = BooleanArray(2 * n - 1) // Отмечать наличие ферзя на побочной диагонали\n    val res = mutableListOf<MutableList<MutableList<String>>?>()\n\n    backtrack(0, n, state, res, cols, diags1, diags2)\n\n    return res\n}\n
        n_queens.rb
        ### Алгоритм бэктрекинга: n ферзей ###\ndef backtrack(row, n, state, res, cols, diags1, diags2)\n  # Когда все строки уже обработаны, записать решение\n  if row == n\n    res << state.map { |row| row.dup }\n    return\n  end\n\n  # Обойти все столбцы\n  for col in 0...n\n    # Вычислить главную и побочную диагонали, соответствующие этой клетке\n    diag1 = row - col + n - 1\n    diag2 = row + col\n    # Отсечение: в столбце, главной диагонали и побочной диагонали этой клетки не должно быть ферзей\n    if !cols[col] && !diags1[diag1] && !diags2[diag2]\n      # Попытка: поставить ферзя в эту клетку\n      state[row][col] = \"Q\"\n      cols[col] = diags1[diag1] = diags2[diag2] = true\n      # Перейти к размещению следующей строки\n      backtrack(row + 1, n, state, res, cols, diags1, diags2)\n      # Откат: восстановить эту клетку как пустую\n      state[row][col] = \"#\"\n      cols[col] = diags1[diag1] = diags2[diag2] = false\n    end\n  end\nend\n\n### Решить задачу о n ферзях ###\ndef n_queens(n)\n  # Инициализировать доску размера n*n, где 'Q' обозначает ферзя, а '#' — пустую клетку\n  state = Array.new(n) { Array.new(n, \"#\") }\n  cols = Array.new(n, false) # Отмечать, есть ли ферзь в столбце\n  diags1 = Array.new(2 * n - 1, false) # Отмечать наличие ферзя на главной диагонали\n  diags2 = Array.new(2 * n - 1, false) # Отмечать наличие ферзя на побочной диагонали\n  res = []\n  backtrack(0, n, state, res, cols, diags1, diags2)\n\n  res\nend\n
        Визуализация кода

        Во весь экран >

        Если размещать ферзей построчно \\(n\\) раз, учитывая ограничение по столбцам, то начиная с первой строки и заканчивая последней мы получаем соответственно \\(n\\), \\(n-1\\), \\(\\dots\\), \\(2\\), \\(1\\) вариантов выбора, что дает \\(O(n!)\\) времени. При записи решения нужно скопировать матрицу state и добавить ее в res , а копирование требует \\(O(n^2)\\) времени. Следовательно, общая временная сложность равна \\(O(n! \\cdot n^2)\\) . На практике обрезка по диагональным ограничениям дополнительно сильно уменьшает пространство поиска, поэтому фактическая эффективность часто лучше этой оценки.

        Массив state использует \\(O(n^2)\\) пространства, а массивы cols , diags1 и diags2 используют по \\(O(n)\\) пространства. Максимальная глубина рекурсии равна \\(n\\) , что требует \\(O(n)\\) памяти стека. Следовательно, пространственная сложность равна \\(O(n^2)\\) .

        ","path":["Глава 13. Поиск с возвратом","13.4   Задача о n ферзях"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/","level":1,"title":"13.2   Задача о перестановках","text":"

        Задача о перестановках является типичным применением алгоритма поиска с возвратом. Ее определение состоит в том, чтобы для данного множества элементов (например, массива или строки) найти все возможные перестановки этих элементов.

        В таблице 13-2 приведено несколько примеров входных массивов и соответствующих им перестановок.

        Таблица 13-2   Примеры перестановок

        Входной массив Все перестановки \\([1]\\) \\([1]\\) \\([1, 2]\\) \\([1, 2], [2, 1]\\) \\([1, 2, 3]\\) \\([1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]\\)","path":["Глава 13. Поиск с возвратом","13.2   Задача о перестановках"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1321","level":2,"title":"13.2.1   Случай без равных элементов","text":"

        Question

        Дан массив целых чисел, в котором нет повторяющихся элементов. Верните все возможные перестановки.

        С точки зрения поиска с возвратом процесс построения перестановок можно представить как результат последовательности выборов. Пусть входной массив равен \\([1, 2, 3]\\) ; если мы сначала выберем \\(1\\) , затем \\(3\\) , а потом \\(2\\) , то получим перестановку \\([1, 3, 2]\\) . Откат здесь означает отмену одного из выборов с последующей попыткой других вариантов.

        С точки зрения кода поиска с возвратом множество кандидатов choices состоит из всех элементов входного массива, а состояние state - из элементов, уже выбранных к текущему моменту. Поскольку каждый элемент разрешено выбирать только один раз, все элементы в state должны быть уникальны.

        Как показано на рисунке 13-5, процесс поиска можно развернуть в дерево рекурсии, где каждый узел представляет текущее состояние state . Начиная от корня, после трех раундов выбора мы попадаем в листья, и каждый лист соответствует одной перестановке.

        Рисунок 13-5   Дерево рекурсии для перестановок

        ","path":["Глава 13. Поиск с возвратом","13.2   Задача о перестановках"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1","level":3,"title":"1.   Обрезка повторного выбора","text":"

        Чтобы гарантировать, что каждый элемент выбирается только один раз, введем булев массив selected , где selected[i] обозначает, был ли уже выбран choices[i] , и на его основе выполним следующую обрезку.

        • После того как сделан выбор choice[i] , мы присваиваем selected[i] значение \\(\\text{True}\\) , тем самым отмечая, что этот элемент уже выбран.
        • При обходе списка вариантов choices пропускаем все уже выбранные элементы, то есть выполняем обрезку.

        Как показано на рисунке 13-6, если в первом раунде мы выберем 1 , во втором - 3 , а в третьем - 2 , то во втором раунде нужно отсечь ветвь элемента 1 , а в третьем - ветви элементов 1 и 3 .

        Рисунок 13-6   Пример обрезки в задаче о перестановках

        Из рисунка видно, что такая обрезка уменьшает размер пространства поиска с \\(O(n^n)\\) до \\(O(n!)\\) .

        ","path":["Глава 13. Поиск с возвратом","13.2   Задача о перестановках"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#2","level":3,"title":"2.   Реализация кода","text":"

        После прояснения всей логики можно просто \"заполнить пропуски\" в шаблоне поиска с возвратом. Чтобы сократить общий объем кода, мы не будем отдельно реализовывать каждую функцию из каркаса, а раскроем их прямо внутри backtrack() :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby permutations_i.py
        def backtrack(\n    state: list[int], choices: list[int], selected: list[bool], res: list[list[int]]\n):\n    \"\"\"Алгоритм бэктрекинга: все перестановки I\"\"\"\n    # Когда длина состояния равна числу элементов, записать решение\n    if len(state) == len(choices):\n        res.append(list(state))\n        return\n    # Перебор всех вариантов выбора\n    for i, choice in enumerate(choices):\n        # Отсечение: нельзя выбирать один и тот же элемент повторно\n        if not selected[i]:\n            # Попытка: сделать выбор и обновить состояние\n            selected[i] = True\n            state.append(choice)\n            # Перейти к следующему выбору\n            backtrack(state, choices, selected, res)\n            # Откат: отменить выбор и восстановить предыдущее состояние\n            selected[i] = False\n            state.pop()\n\ndef permutations_i(nums: list[int]) -> list[list[int]]:\n    \"\"\"Все перестановки I\"\"\"\n    res = []\n    backtrack(state=[], choices=nums, selected=[False] * len(nums), res=res)\n    return res\n
        permutations_i.cpp
        /* Алгоритм бэктрекинга: все перестановки I */\nvoid backtrack(vector<int> &state, const vector<int> &choices, vector<bool> &selected, vector<vector<int>> &res) {\n    // Когда длина состояния равна числу элементов, записать решение\n    if (state.size() == choices.size()) {\n        res.push_back(state);\n        return;\n    }\n    // Перебор всех вариантов выбора\n    for (int i = 0; i < choices.size(); i++) {\n        int choice = choices[i];\n        // Отсечение: нельзя выбирать один и тот же элемент повторно\n        if (!selected[i]) {\n            // Попытка: сделать выбор и обновить состояние\n            selected[i] = true;\n            state.push_back(choice);\n            // Перейти к следующему выбору\n            backtrack(state, choices, selected, res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            selected[i] = false;\n            state.pop_back();\n        }\n    }\n}\n\n/* Все перестановки I */\nvector<vector<int>> permutationsI(vector<int> nums) {\n    vector<int> state;\n    vector<bool> selected(nums.size(), false);\n    vector<vector<int>> res;\n    backtrack(state, nums, selected, res);\n    return res;\n}\n
        permutations_i.java
        /* Алгоритм бэктрекинга: все перестановки I */\nvoid backtrack(List<Integer> state, int[] choices, boolean[] selected, List<List<Integer>> res) {\n    // Когда длина состояния равна числу элементов, записать решение\n    if (state.size() == choices.length) {\n        res.add(new ArrayList<Integer>(state));\n        return;\n    }\n    // Перебор всех вариантов выбора\n    for (int i = 0; i < choices.length; i++) {\n        int choice = choices[i];\n        // Отсечение: нельзя выбирать один и тот же элемент повторно\n        if (!selected[i]) {\n            // Попытка: сделать выбор и обновить состояние\n            selected[i] = true;\n            state.add(choice);\n            // Перейти к следующему выбору\n            backtrack(state, choices, selected, res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            selected[i] = false;\n            state.remove(state.size() - 1);\n        }\n    }\n}\n\n/* Все перестановки I */\nList<List<Integer>> permutationsI(int[] nums) {\n    List<List<Integer>> res = new ArrayList<List<Integer>>();\n    backtrack(new ArrayList<Integer>(), nums, new boolean[nums.length], res);\n    return res;\n}\n
        permutations_i.cs
        /* Алгоритм бэктрекинга: все перестановки I */\nvoid Backtrack(List<int> state, int[] choices, bool[] selected, List<List<int>> res) {\n    // Когда длина состояния равна числу элементов, записать решение\n    if (state.Count == choices.Length) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // Перебор всех вариантов выбора\n    for (int i = 0; i < choices.Length; i++) {\n        int choice = choices[i];\n        // Отсечение: нельзя выбирать один и тот же элемент повторно\n        if (!selected[i]) {\n            // Попытка: сделать выбор и обновить состояние\n            selected[i] = true;\n            state.Add(choice);\n            // Перейти к следующему выбору\n            Backtrack(state, choices, selected, res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            selected[i] = false;\n            state.RemoveAt(state.Count - 1);\n        }\n    }\n}\n\n/* Все перестановки I */\nList<List<int>> PermutationsI(int[] nums) {\n    List<List<int>> res = [];\n    Backtrack([], nums, new bool[nums.Length], res);\n    return res;\n}\n
        permutations_i.go
        /* Алгоритм бэктрекинга: все перестановки I */\nfunc backtrackI(state *[]int, choices *[]int, selected *[]bool, res *[][]int) {\n    // Когда длина состояния равна числу элементов, записать решение\n    if len(*state) == len(*choices) {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n    }\n    // Перебор всех вариантов выбора\n    for i := 0; i < len(*choices); i++ {\n        choice := (*choices)[i]\n        // Отсечение: нельзя выбирать один и тот же элемент повторно\n        if !(*selected)[i] {\n            // Попытка: сделать выбор и обновить состояние\n            (*selected)[i] = true\n            *state = append(*state, choice)\n            // Перейти к следующему выбору\n            backtrackI(state, choices, selected, res)\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            (*selected)[i] = false\n            *state = (*state)[:len(*state)-1]\n        }\n    }\n}\n\n/* Все перестановки I */\nfunc permutationsI(nums []int) [][]int {\n    res := make([][]int, 0)\n    state := make([]int, 0)\n    selected := make([]bool, len(nums))\n    backtrackI(&state, &nums, &selected, &res)\n    return res\n}\n
        permutations_i.swift
        /* Алгоритм бэктрекинга: все перестановки I */\nfunc backtrack(state: inout [Int], choices: [Int], selected: inout [Bool], res: inout [[Int]]) {\n    // Когда длина состояния равна числу элементов, записать решение\n    if state.count == choices.count {\n        res.append(state)\n        return\n    }\n    // Перебор всех вариантов выбора\n    for (i, choice) in choices.enumerated() {\n        // Отсечение: нельзя выбирать один и тот же элемент повторно\n        if !selected[i] {\n            // Попытка: сделать выбор и обновить состояние\n            selected[i] = true\n            state.append(choice)\n            // Перейти к следующему выбору\n            backtrack(state: &state, choices: choices, selected: &selected, res: &res)\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            selected[i] = false\n            state.removeLast()\n        }\n    }\n}\n\n/* Все перестановки I */\nfunc permutationsI(nums: [Int]) -> [[Int]] {\n    var state: [Int] = []\n    var selected = Array(repeating: false, count: nums.count)\n    var res: [[Int]] = []\n    backtrack(state: &state, choices: nums, selected: &selected, res: &res)\n    return res\n}\n
        permutations_i.js
        /* Алгоритм бэктрекинга: все перестановки I */\nfunction backtrack(state, choices, selected, res) {\n    // Когда длина состояния равна числу элементов, записать решение\n    if (state.length === choices.length) {\n        res.push([...state]);\n        return;\n    }\n    // Перебор всех вариантов выбора\n    choices.forEach((choice, i) => {\n        // Отсечение: нельзя выбирать один и тот же элемент повторно\n        if (!selected[i]) {\n            // Попытка: сделать выбор и обновить состояние\n            selected[i] = true;\n            state.push(choice);\n            // Перейти к следующему выбору\n            backtrack(state, choices, selected, res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            selected[i] = false;\n            state.pop();\n        }\n    });\n}\n\n/* Все перестановки I */\nfunction permutationsI(nums) {\n    const res = [];\n    backtrack([], nums, Array(nums.length).fill(false), res);\n    return res;\n}\n
        permutations_i.ts
        /* Алгоритм бэктрекинга: все перестановки I */\nfunction backtrack(\n    state: number[],\n    choices: number[],\n    selected: boolean[],\n    res: number[][]\n): void {\n    // Когда длина состояния равна числу элементов, записать решение\n    if (state.length === choices.length) {\n        res.push([...state]);\n        return;\n    }\n    // Перебор всех вариантов выбора\n    choices.forEach((choice, i) => {\n        // Отсечение: нельзя выбирать один и тот же элемент повторно\n        if (!selected[i]) {\n            // Попытка: сделать выбор и обновить состояние\n            selected[i] = true;\n            state.push(choice);\n            // Перейти к следующему выбору\n            backtrack(state, choices, selected, res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            selected[i] = false;\n            state.pop();\n        }\n    });\n}\n\n/* Все перестановки I */\nfunction permutationsI(nums: number[]): number[][] {\n    const res: number[][] = [];\n    backtrack([], nums, Array(nums.length).fill(false), res);\n    return res;\n}\n
        permutations_i.dart
        /* Алгоритм бэктрекинга: все перестановки I */\nvoid backtrack(\n  List<int> state,\n  List<int> choices,\n  List<bool> selected,\n  List<List<int>> res,\n) {\n  // Когда длина состояния равна числу элементов, записать решение\n  if (state.length == choices.length) {\n    res.add(List.from(state));\n    return;\n  }\n  // Перебор всех вариантов выбора\n  for (int i = 0; i < choices.length; i++) {\n    int choice = choices[i];\n    // Отсечение: нельзя выбирать один и тот же элемент повторно\n    if (!selected[i]) {\n      // Попытка: сделать выбор и обновить состояние\n      selected[i] = true;\n      state.add(choice);\n      // Перейти к следующему выбору\n      backtrack(state, choices, selected, res);\n      // Откат: отменить выбор и восстановить предыдущее состояние\n      selected[i] = false;\n      state.removeLast();\n    }\n  }\n}\n\n/* Все перестановки I */\nList<List<int>> permutationsI(List<int> nums) {\n  List<List<int>> res = [];\n  backtrack([], nums, List.filled(nums.length, false), res);\n  return res;\n}\n
        permutations_i.rs
        /* Алгоритм бэктрекинга: все перестановки I */\nfn backtrack(mut state: Vec<i32>, choices: &[i32], selected: &mut [bool], res: &mut Vec<Vec<i32>>) {\n    // Когда длина состояния равна числу элементов, записать решение\n    if state.len() == choices.len() {\n        res.push(state);\n        return;\n    }\n    // Перебор всех вариантов выбора\n    for i in 0..choices.len() {\n        let choice = choices[i];\n        // Отсечение: нельзя выбирать один и тот же элемент повторно\n        if !selected[i] {\n            // Попытка: сделать выбор и обновить состояние\n            selected[i] = true;\n            state.push(choice);\n            // Перейти к следующему выбору\n            backtrack(state.clone(), choices, selected, res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            selected[i] = false;\n            state.pop();\n        }\n    }\n}\n\n/* Все перестановки I */\nfn permutations_i(nums: &mut [i32]) -> Vec<Vec<i32>> {\n    let mut res = Vec::new(); // Состояние (подмножество)\n    backtrack(Vec::new(), nums, &mut vec![false; nums.len()], &mut res);\n    res\n}\n
        permutations_i.c
        /* Алгоритм бэктрекинга: все перестановки I */\nvoid backtrack(int *state, int stateSize, int *choices, int choicesSize, bool *selected, int **res, int *resSize) {\n    // Когда длина состояния равна числу элементов, записать решение\n    if (stateSize == choicesSize) {\n        res[*resSize] = (int *)malloc(choicesSize * sizeof(int));\n        for (int i = 0; i < choicesSize; i++) {\n            res[*resSize][i] = state[i];\n        }\n        (*resSize)++;\n        return;\n    }\n    // Перебор всех вариантов выбора\n    for (int i = 0; i < choicesSize; i++) {\n        int choice = choices[i];\n        // Отсечение: нельзя выбирать один и тот же элемент повторно\n        if (!selected[i]) {\n            // Попытка: сделать выбор и обновить состояние\n            selected[i] = true;\n            state[stateSize] = choice;\n            // Перейти к следующему выбору\n            backtrack(state, stateSize + 1, choices, choicesSize, selected, res, resSize);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            selected[i] = false;\n        }\n    }\n}\n\n/* Все перестановки I */\nint **permutationsI(int *nums, int numsSize, int *returnSize) {\n    int *state = (int *)malloc(numsSize * sizeof(int));\n    bool *selected = (bool *)malloc(numsSize * sizeof(bool));\n    for (int i = 0; i < numsSize; i++) {\n        selected[i] = false;\n    }\n    int **res = (int **)malloc(MAX_SIZE * sizeof(int *));\n    *returnSize = 0;\n\n    backtrack(state, 0, nums, numsSize, selected, res, returnSize);\n\n    free(state);\n    free(selected);\n\n    return res;\n}\n
        permutations_i.kt
        /* Алгоритм бэктрекинга: все перестановки I */\nfun backtrack(\n    state: MutableList<Int>,\n    choices: IntArray,\n    selected: BooleanArray,\n    res: MutableList<MutableList<Int>?>\n) {\n    // Когда длина состояния равна числу элементов, записать решение\n    if (state.size == choices.size) {\n        res.add(state.toMutableList())\n        return\n    }\n    // Перебор всех вариантов выбора\n    for (i in choices.indices) {\n        val choice = choices[i]\n        // Отсечение: нельзя выбирать один и тот же элемент повторно\n        if (!selected[i]) {\n            // Попытка: сделать выбор и обновить состояние\n            selected[i] = true\n            state.add(choice)\n            // Перейти к следующему выбору\n            backtrack(state, choices, selected, res)\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            selected[i] = false\n            state.removeAt(state.size - 1)\n        }\n    }\n}\n\n/* Все перестановки I */\nfun permutationsI(nums: IntArray): MutableList<MutableList<Int>?> {\n    val res = mutableListOf<MutableList<Int>?>()\n    backtrack(mutableListOf(), nums, BooleanArray(nums.size), res)\n    return res\n}\n
        permutations_i.rb
        ### Алгоритм бэктрекинга: все перестановки I ###\ndef backtrack(state, choices, selected, res)\n  # Когда длина состояния равна числу элементов, записать решение\n  if state.length == choices.length\n    res << state.dup\n    return\n  end\n\n  # Перебор всех вариантов выбора\n  choices.each_with_index do |choice, i|\n    # Отсечение: нельзя выбирать один и тот же элемент повторно\n    unless selected[i]\n      # Попытка: сделать выбор и обновить состояние\n      selected[i] = true\n      state << choice\n      # Перейти к следующему выбору\n      backtrack(state, choices, selected, res)\n      # Откат: отменить выбор и восстановить предыдущее состояние\n      selected[i] = false\n      state.pop\n    end\n  end\nend\n\n### Все перестановки I ###\ndef permutations_i(nums)\n  res = []\n  backtrack([], nums, Array.new(nums.length, false), res)\n  res\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 13. Поиск с возвратом","13.2   Задача о перестановках"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1322","level":2,"title":"13.2.2   Учет равных элементов","text":"

        Question

        Дан массив целых чисел, который может содержать повторяющиеся элементы. Верните все неповторяющиеся перестановки.

        Пусть входной массив равен \\([1, 1, 2]\\) . Чтобы различать два одинаковых элемента \\(1\\) , будем обозначать второй из них как \\(\\hat{1}\\) .

        Как показано на рисунке 13-7, описанный выше метод создаст результат, половина которого окажется дублирующейся.

        Рисунок 13-7   Повторяющиеся перестановки

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

        ","path":["Глава 13. Поиск с возвратом","13.2   Задача о перестановках"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1_1","level":3,"title":"1.   Обрезка равных элементов","text":"

        Посмотрите на рисунок 13-8: в первом раунде выбрать \\(1\\) или выбрать \\(\\hat{1}\\) - это одно и то же, а значит, все перестановки, полученные из этих двух выборов, будут дублироваться. Поэтому ветвь \\(\\hat{1}\\) нужно отсечь.

        Точно так же, если в первом раунде выбрать \\(2\\) , то во втором раунде выборы \\(1\\) и \\(\\hat{1}\\) снова создадут дублирующиеся ветви, поэтому и в этом случае ветвь \\(\\hat{1}\\) нужно отсечь.

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

        Рисунок 13-8   Обрезка повторяющихся перестановок

        ","path":["Глава 13. Поиск с возвратом","13.2   Задача о перестановках"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#2_1","level":3,"title":"2.   Реализация кода","text":"

        На основе решения из предыдущей задачи можно на каждом раунде выбора заводить хеш-множество duplicated , которое будет записывать элементы, уже встречавшиеся в этом раунде, и отсекать повторы:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby permutations_ii.py
        def backtrack(\n    state: list[int], choices: list[int], selected: list[bool], res: list[list[int]]\n):\n    \"\"\"Алгоритм бэктрекинга: все перестановки II\"\"\"\n    # Когда длина состояния равна числу элементов, записать решение\n    if len(state) == len(choices):\n        res.append(list(state))\n        return\n    # Перебор всех вариантов выбора\n    duplicated = set[int]()\n    for i, choice in enumerate(choices):\n        # Отсечение: нельзя выбирать один и тот же элемент повторно и нельзя повторно выбирать равные элементы\n        if not selected[i] and choice not in duplicated:\n            # Попытка: сделать выбор и обновить состояние\n            duplicated.add(choice)  # Записать значения уже выбранных элементов\n            selected[i] = True\n            state.append(choice)\n            # Перейти к следующему выбору\n            backtrack(state, choices, selected, res)\n            # Откат: отменить выбор и восстановить предыдущее состояние\n            selected[i] = False\n            state.pop()\n\ndef permutations_ii(nums: list[int]) -> list[list[int]]:\n    \"\"\"Все перестановки II\"\"\"\n    res = []\n    backtrack(state=[], choices=nums, selected=[False] * len(nums), res=res)\n    return res\n
        permutations_ii.cpp
        /* Алгоритм бэктрекинга: все перестановки II */\nvoid backtrack(vector<int> &state, const vector<int> &choices, vector<bool> &selected, vector<vector<int>> &res) {\n    // Когда длина состояния равна числу элементов, записать решение\n    if (state.size() == choices.size()) {\n        res.push_back(state);\n        return;\n    }\n    // Перебор всех вариантов выбора\n    unordered_set<int> duplicated;\n    for (int i = 0; i < choices.size(); i++) {\n        int choice = choices[i];\n        // Отсечение: нельзя выбирать один и тот же элемент повторно и нельзя повторно выбирать равные элементы\n        if (!selected[i] && duplicated.find(choice) == duplicated.end()) {\n            // Попытка: сделать выбор и обновить состояние\n            duplicated.emplace(choice); // Записать значения уже выбранных элементов\n            selected[i] = true;\n            state.push_back(choice);\n            // Перейти к следующему выбору\n            backtrack(state, choices, selected, res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            selected[i] = false;\n            state.pop_back();\n        }\n    }\n}\n\n/* Все перестановки II */\nvector<vector<int>> permutationsII(vector<int> nums) {\n    vector<int> state;\n    vector<bool> selected(nums.size(), false);\n    vector<vector<int>> res;\n    backtrack(state, nums, selected, res);\n    return res;\n}\n
        permutations_ii.java
        /* Алгоритм бэктрекинга: все перестановки II */\nvoid backtrack(List<Integer> state, int[] choices, boolean[] selected, List<List<Integer>> res) {\n    // Когда длина состояния равна числу элементов, записать решение\n    if (state.size() == choices.length) {\n        res.add(new ArrayList<Integer>(state));\n        return;\n    }\n    // Перебор всех вариантов выбора\n    Set<Integer> duplicated = new HashSet<Integer>();\n    for (int i = 0; i < choices.length; i++) {\n        int choice = choices[i];\n        // Отсечение: нельзя выбирать один и тот же элемент повторно и нельзя повторно выбирать равные элементы\n        if (!selected[i] && !duplicated.contains(choice)) {\n            // Попытка: сделать выбор и обновить состояние\n            duplicated.add(choice); // Записать значения уже выбранных элементов\n            selected[i] = true;\n            state.add(choice);\n            // Перейти к следующему выбору\n            backtrack(state, choices, selected, res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            selected[i] = false;\n            state.remove(state.size() - 1);\n        }\n    }\n}\n\n/* Все перестановки II */\nList<List<Integer>> permutationsII(int[] nums) {\n    List<List<Integer>> res = new ArrayList<List<Integer>>();\n    backtrack(new ArrayList<Integer>(), nums, new boolean[nums.length], res);\n    return res;\n}\n
        permutations_ii.cs
        /* Алгоритм бэктрекинга: все перестановки II */\nvoid Backtrack(List<int> state, int[] choices, bool[] selected, List<List<int>> res) {\n    // Когда длина состояния равна числу элементов, записать решение\n    if (state.Count == choices.Length) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // Перебор всех вариантов выбора\n    HashSet<int> duplicated = [];\n    for (int i = 0; i < choices.Length; i++) {\n        int choice = choices[i];\n        // Отсечение: нельзя выбирать один и тот же элемент повторно и нельзя повторно выбирать равные элементы\n        if (!selected[i] && !duplicated.Contains(choice)) {\n            // Попытка: сделать выбор и обновить состояние\n            duplicated.Add(choice); // Записать значения уже выбранных элементов\n            selected[i] = true;\n            state.Add(choice);\n            // Перейти к следующему выбору\n            Backtrack(state, choices, selected, res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            selected[i] = false;\n            state.RemoveAt(state.Count - 1);\n        }\n    }\n}\n\n/* Все перестановки II */\nList<List<int>> PermutationsII(int[] nums) {\n    List<List<int>> res = [];\n    Backtrack([], nums, new bool[nums.Length], res);\n    return res;\n}\n
        permutations_ii.go
        /* Алгоритм бэктрекинга: все перестановки II */\nfunc backtrackII(state *[]int, choices *[]int, selected *[]bool, res *[][]int) {\n    // Когда длина состояния равна числу элементов, записать решение\n    if len(*state) == len(*choices) {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n    }\n    // Перебор всех вариантов выбора\n    duplicated := make(map[int]struct{}, 0)\n    for i := 0; i < len(*choices); i++ {\n        choice := (*choices)[i]\n        // Отсечение: нельзя выбирать один и тот же элемент повторно и нельзя повторно выбирать равные элементы\n        if _, ok := duplicated[choice]; !ok && !(*selected)[i] {\n            // Попробовать: сделать выбор, обновить состояние\n            // Записать значение уже выбранного элемента\n            duplicated[choice] = struct{}{}\n            (*selected)[i] = true\n            *state = append(*state, choice)\n            // Перейти к следующему выбору\n            backtrackII(state, choices, selected, res)\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            (*selected)[i] = false\n            *state = (*state)[:len(*state)-1]\n        }\n    }\n}\n\n/* Все перестановки II */\nfunc permutationsII(nums []int) [][]int {\n    res := make([][]int, 0)\n    state := make([]int, 0)\n    selected := make([]bool, len(nums))\n    backtrackII(&state, &nums, &selected, &res)\n    return res\n}\n
        permutations_ii.swift
        /* Алгоритм бэктрекинга: все перестановки II */\nfunc backtrack(state: inout [Int], choices: [Int], selected: inout [Bool], res: inout [[Int]]) {\n    // Когда длина состояния равна числу элементов, записать решение\n    if state.count == choices.count {\n        res.append(state)\n        return\n    }\n    // Перебор всех вариантов выбора\n    var duplicated: Set<Int> = []\n    for (i, choice) in choices.enumerated() {\n        // Отсечение: нельзя выбирать один и тот же элемент повторно и нельзя повторно выбирать равные элементы\n        if !selected[i], !duplicated.contains(choice) {\n            // Попытка: сделать выбор и обновить состояние\n            duplicated.insert(choice) // Записать значения уже выбранных элементов\n            selected[i] = true\n            state.append(choice)\n            // Перейти к следующему выбору\n            backtrack(state: &state, choices: choices, selected: &selected, res: &res)\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            selected[i] = false\n            state.removeLast()\n        }\n    }\n}\n\n/* Все перестановки II */\nfunc permutationsII(nums: [Int]) -> [[Int]] {\n    var state: [Int] = []\n    var selected = Array(repeating: false, count: nums.count)\n    var res: [[Int]] = []\n    backtrack(state: &state, choices: nums, selected: &selected, res: &res)\n    return res\n}\n
        permutations_ii.js
        /* Алгоритм бэктрекинга: все перестановки II */\nfunction backtrack(state, choices, selected, res) {\n    // Когда длина состояния равна числу элементов, записать решение\n    if (state.length === choices.length) {\n        res.push([...state]);\n        return;\n    }\n    // Перебор всех вариантов выбора\n    const duplicated = new Set();\n    choices.forEach((choice, i) => {\n        // Отсечение: нельзя выбирать один и тот же элемент повторно и нельзя повторно выбирать равные элементы\n        if (!selected[i] && !duplicated.has(choice)) {\n            // Попытка: сделать выбор и обновить состояние\n            duplicated.add(choice); // Записать значения уже выбранных элементов\n            selected[i] = true;\n            state.push(choice);\n            // Перейти к следующему выбору\n            backtrack(state, choices, selected, res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            selected[i] = false;\n            state.pop();\n        }\n    });\n}\n\n/* Все перестановки II */\nfunction permutationsII(nums) {\n    const res = [];\n    backtrack([], nums, Array(nums.length).fill(false), res);\n    return res;\n}\n
        permutations_ii.ts
        /* Алгоритм бэктрекинга: все перестановки II */\nfunction backtrack(\n    state: number[],\n    choices: number[],\n    selected: boolean[],\n    res: number[][]\n): void {\n    // Когда длина состояния равна числу элементов, записать решение\n    if (state.length === choices.length) {\n        res.push([...state]);\n        return;\n    }\n    // Перебор всех вариантов выбора\n    const duplicated = new Set();\n    choices.forEach((choice, i) => {\n        // Отсечение: нельзя выбирать один и тот же элемент повторно и нельзя повторно выбирать равные элементы\n        if (!selected[i] && !duplicated.has(choice)) {\n            // Попытка: сделать выбор и обновить состояние\n            duplicated.add(choice); // Записать значения уже выбранных элементов\n            selected[i] = true;\n            state.push(choice);\n            // Перейти к следующему выбору\n            backtrack(state, choices, selected, res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            selected[i] = false;\n            state.pop();\n        }\n    });\n}\n\n/* Все перестановки II */\nfunction permutationsII(nums: number[]): number[][] {\n    const res: number[][] = [];\n    backtrack([], nums, Array(nums.length).fill(false), res);\n    return res;\n}\n
        permutations_ii.dart
        /* Алгоритм бэктрекинга: все перестановки II */\nvoid backtrack(\n  List<int> state,\n  List<int> choices,\n  List<bool> selected,\n  List<List<int>> res,\n) {\n  // Когда длина состояния равна числу элементов, записать решение\n  if (state.length == choices.length) {\n    res.add(List.from(state));\n    return;\n  }\n  // Перебор всех вариантов выбора\n  Set<int> duplicated = {};\n  for (int i = 0; i < choices.length; i++) {\n    int choice = choices[i];\n    // Отсечение: нельзя выбирать один и тот же элемент повторно и нельзя повторно выбирать равные элементы\n    if (!selected[i] && !duplicated.contains(choice)) {\n      // Попытка: сделать выбор и обновить состояние\n      duplicated.add(choice); // Записать значения уже выбранных элементов\n      selected[i] = true;\n      state.add(choice);\n      // Перейти к следующему выбору\n      backtrack(state, choices, selected, res);\n      // Откат: отменить выбор и восстановить предыдущее состояние\n      selected[i] = false;\n      state.removeLast();\n    }\n  }\n}\n\n/* Все перестановки II */\nList<List<int>> permutationsII(List<int> nums) {\n  List<List<int>> res = [];\n  backtrack([], nums, List.filled(nums.length, false), res);\n  return res;\n}\n
        permutations_ii.rs
        /* Алгоритм бэктрекинга: все перестановки II */\nfn backtrack(mut state: Vec<i32>, choices: &[i32], selected: &mut [bool], res: &mut Vec<Vec<i32>>) {\n    // Когда длина состояния равна числу элементов, записать решение\n    if state.len() == choices.len() {\n        res.push(state);\n        return;\n    }\n    // Перебор всех вариантов выбора\n    let mut duplicated = HashSet::<i32>::new();\n    for i in 0..choices.len() {\n        let choice = choices[i];\n        // Отсечение: нельзя выбирать один и тот же элемент повторно и нельзя повторно выбирать равные элементы\n        if !selected[i] && !duplicated.contains(&choice) {\n            // Попытка: сделать выбор и обновить состояние\n            duplicated.insert(choice); // Записать значения уже выбранных элементов\n            selected[i] = true;\n            state.push(choice);\n            // Перейти к следующему выбору\n            backtrack(state.clone(), choices, selected, res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            selected[i] = false;\n            state.pop();\n        }\n    }\n}\n\n/* Все перестановки II */\nfn permutations_ii(nums: &mut [i32]) -> Vec<Vec<i32>> {\n    let mut res = Vec::new();\n    backtrack(Vec::new(), nums, &mut vec![false; nums.len()], &mut res);\n    res\n}\n
        permutations_ii.c
        /* Алгоритм бэктрекинга: все перестановки II */\nvoid backtrack(int *state, int stateSize, int *choices, int choicesSize, bool *selected, int **res, int *resSize) {\n    // Когда длина состояния равна числу элементов, записать решение\n    if (stateSize == choicesSize) {\n        res[*resSize] = (int *)malloc(choicesSize * sizeof(int));\n        for (int i = 0; i < choicesSize; i++) {\n            res[*resSize][i] = state[i];\n        }\n        (*resSize)++;\n        return;\n    }\n    // Перебор всех вариантов выбора\n    bool duplicated[MAX_SIZE] = {false};\n    for (int i = 0; i < choicesSize; i++) {\n        int choice = choices[i];\n        // Отсечение: нельзя выбирать один и тот же элемент повторно и нельзя повторно выбирать равные элементы\n        if (!selected[i] && !duplicated[choice]) {\n            // Попытка: сделать выбор и обновить состояние\n            duplicated[choice] = true; // Записать значения уже выбранных элементов\n            selected[i] = true;\n            state[stateSize] = choice;\n            // Перейти к следующему выбору\n            backtrack(state, stateSize + 1, choices, choicesSize, selected, res, resSize);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            selected[i] = false;\n        }\n    }\n}\n\n/* Все перестановки II */\nint **permutationsII(int *nums, int numsSize, int *returnSize) {\n    int *state = (int *)malloc(numsSize * sizeof(int));\n    bool *selected = (bool *)malloc(numsSize * sizeof(bool));\n    for (int i = 0; i < numsSize; i++) {\n        selected[i] = false;\n    }\n    int **res = (int **)malloc(MAX_SIZE * sizeof(int *));\n    *returnSize = 0;\n\n    backtrack(state, 0, nums, numsSize, selected, res, returnSize);\n\n    free(state);\n    free(selected);\n\n    return res;\n}\n
        permutations_ii.kt
        /* Алгоритм бэктрекинга: все перестановки II */\nfun backtrack(\n    state: MutableList<Int>,\n    choices: IntArray,\n    selected: BooleanArray,\n    res: MutableList<MutableList<Int>?>\n) {\n    // Когда длина состояния равна числу элементов, записать решение\n    if (state.size == choices.size) {\n        res.add(state.toMutableList())\n        return\n    }\n    // Перебор всех вариантов выбора\n    val duplicated = HashSet<Int>()\n    for (i in choices.indices) {\n        val choice = choices[i]\n        // Отсечение: нельзя выбирать один и тот же элемент повторно и нельзя повторно выбирать равные элементы\n        if (!selected[i] && !duplicated.contains(choice)) {\n            // Попытка: сделать выбор и обновить состояние\n            duplicated.add(choice) // Записать значения уже выбранных элементов\n            selected[i] = true\n            state.add(choice)\n            // Перейти к следующему выбору\n            backtrack(state, choices, selected, res)\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            selected[i] = false\n            state.removeAt(state.size - 1)\n        }\n    }\n}\n\n/* Все перестановки II */\nfun permutationsII(nums: IntArray): MutableList<MutableList<Int>?> {\n    val res = mutableListOf<MutableList<Int>?>()\n    backtrack(mutableListOf(), nums, BooleanArray(nums.size), res)\n    return res\n}\n
        permutations_ii.rb
        ### Алгоритм бэктрекинга: все перестановки II ###\ndef backtrack(state, choices, selected, res)\n  # Когда длина состояния равна числу элементов, записать решение\n  if state.length == choices.length\n    res << state.dup\n    return\n  end\n\n  # Перебор всех вариантов выбора\n  duplicated = Set.new\n  choices.each_with_index do |choice, i|\n    # Отсечение: нельзя выбирать один и тот же элемент повторно и нельзя повторно выбирать равные элементы\n    if !selected[i] && !duplicated.include?(choice)\n      # Попытка: сделать выбор и обновить состояние\n      duplicated.add(choice)\n      selected[i] = true\n      state << choice\n      # Перейти к следующему выбору\n      backtrack(state, choices, selected, res)\n      # Откат: отменить выбор и восстановить предыдущее состояние\n      selected[i] = false\n      state.pop\n    end\n  end\nend\n\n### Все перестановки II ###\ndef permutations_ii(nums)\n  res = []\n  backtrack([], nums, Array.new(nums.length, false), res)\n  res\nend\n
        Визуализация кода

        Во весь экран >

        Если предположить, что все элементы попарно различны, то из \\(n\\) элементов можно получить \\(n!\\) перестановок; при записи результата требуется копировать список длины \\(n\\) , что занимает \\(O(n)\\) времени. Следовательно, временная сложность равна \\(O(n!n)\\) .

        Максимальная глубина рекурсии равна \\(n\\) , что требует \\(O(n)\\) стековой памяти. Массив selected занимает \\(O(n)\\) пространства. Одновременно может существовать до \\(n\\) хеш-множеств duplicated , что дает \\(O(n^2)\\) памяти. Следовательно, пространственная сложность равна \\(O(n^2)\\) .

        ","path":["Глава 13. Поиск с возвратом","13.2   Задача о перестановках"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#3","level":3,"title":"3.   Сравнение двух видов обрезки","text":"

        Обратите внимание: хотя и selected , и duplicated используются для обрезки, их цели различаются.

        • Обрезка повторного выбора: во всем процессе поиска существует только один selected . Он записывает, какие элементы уже входят в текущее состояние, и нужен для того, чтобы один и тот же элемент не появлялся в state дважды.
        • Обрезка равных элементов: каждый раунд выбора (каждый вызов backtrack) содержит собственный duplicated . Он записывает, какие элементы уже выбирались в текущем раунде (for цикле), и нужен для того, чтобы равные элементы выбирались только один раз.

        На рисунке 13-9 показана область действия двух условий обрезки. Помните, что каждый узел дерева соответствует одному выбору, а путь от корня до листа образует одну перестановку.

        Рисунок 13-9   Область действия двух условий обрезки

        ","path":["Глава 13. Поиск с возвратом","13.2   Задача о перестановках"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/","level":1,"title":"13.3   Задача о сумме подмножеств","text":"","path":["Глава 13. Поиск с возвратом","13.3   Задача о сумме подмножеств"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1331","level":2,"title":"13.3.1   Случай без повторяющихся элементов","text":"

        Question

        Дан массив положительных целых чисел nums и целое положительное значение target . Найдите все возможные комбинации, сумма элементов которых равна target . Во входном массиве нет повторяющихся элементов, и каждый элемент можно выбирать неограниченное число раз. Верните эти комбинации в виде списка; в результате не должно быть повторяющихся комбинаций.

        Например, для входного множества \\(\\{3, 4, 5\\}\\) и целевого значения \\(9\\) решениями будут \\(\\{3, 3, 3\\}\\) и \\(\\{4, 5\\}\\) . При этом важно учитывать два обстоятельства.

        • Элементы входного множества можно выбирать повторно неограниченное число раз.
        • Подмножество не различает порядок элементов, поэтому \\(\\{4, 5\\}\\) и \\(\\{5, 4\\}\\) считаются одним и тем же подмножеством.
        ","path":["Глава 13. Поиск с возвратом","13.3   Задача о сумме подмножеств"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1","level":3,"title":"1.   Отталкиваемся от решения задачи о перестановках","text":"

        Как и в задаче о перестановках, можно представлять построение подмножеств как результат последовательности выборов и во время выбора динамически обновлять \"сумму элементов\"; когда эта сумма становится равной target , соответствующее подмножество записывается в список результатов.

        Однако в отличие от задачи о перестановках в этой задаче элементы множества можно выбирать неограниченное число раз, поэтому нам не нужен булев список selected для записи того, был ли выбран элемент. Можно слегка изменить код для перестановок и получить первоначальную версию решения:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby subset_sum_i_naive.py
        def backtrack(\n    state: list[int],\n    target: int,\n    total: int,\n    choices: list[int],\n    res: list[list[int]],\n):\n    \"\"\"Алгоритм бэктрекинга: сумма подмножеств I\"\"\"\n    # Если сумма подмножества равна target, записать решение\n    if total == target:\n        res.append(list(state))\n        return\n    # Перебор всех вариантов выбора\n    for i in range(len(choices)):\n        # Отсечение: если сумма подмножества превышает target, пропустить этот выбор\n        if total + choices[i] > target:\n            continue\n        # Попытка: сделать выбор и обновить элемент и total\n        state.append(choices[i])\n        # Перейти к следующему выбору\n        backtrack(state, target, total + choices[i], choices, res)\n        # Откат: отменить выбор и восстановить предыдущее состояние\n        state.pop()\n\ndef subset_sum_i_naive(nums: list[int], target: int) -> list[list[int]]:\n    \"\"\"Решить задачу суммы подмножеств I (с повторяющимися подмножествами)\"\"\"\n    state = []  # Состояние (подмножество)\n    total = 0  # Сумма подмножеств\n    res = []  # Список результатов (список подмножеств)\n    backtrack(state, target, total, nums, res)\n    return res\n
        subset_sum_i_naive.cpp
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nvoid backtrack(vector<int> &state, int target, int total, vector<int> &choices, vector<vector<int>> &res) {\n    // Если сумма подмножества равна target, записать решение\n    if (total == target) {\n        res.push_back(state);\n        return;\n    }\n    // Перебор всех вариантов выбора\n    for (size_t i = 0; i < choices.size(); i++) {\n        // Отсечение: если сумма подмножества превышает target, пропустить этот выбор\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // Попытка: сделать выбор и обновить элемент и total\n        state.push_back(choices[i]);\n        // Перейти к следующему выбору\n        backtrack(state, target, total + choices[i], choices, res);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.pop_back();\n    }\n}\n\n/* Решить задачу суммы подмножеств I (с повторяющимися подмножествами) */\nvector<vector<int>> subsetSumINaive(vector<int> &nums, int target) {\n    vector<int> state;       // Состояние (подмножество)\n    int total = 0;           // Сумма подмножеств\n    vector<vector<int>> res; // Список результатов (список подмножеств)\n    backtrack(state, target, total, nums, res);\n    return res;\n}\n
        subset_sum_i_naive.java
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nvoid backtrack(List<Integer> state, int target, int total, int[] choices, List<List<Integer>> res) {\n    // Если сумма подмножества равна target, записать решение\n    if (total == target) {\n        res.add(new ArrayList<>(state));\n        return;\n    }\n    // Перебор всех вариантов выбора\n    for (int i = 0; i < choices.length; i++) {\n        // Отсечение: если сумма подмножества превышает target, пропустить этот выбор\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // Попытка: сделать выбор и обновить элемент и total\n        state.add(choices[i]);\n        // Перейти к следующему выбору\n        backtrack(state, target, total + choices[i], choices, res);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.remove(state.size() - 1);\n    }\n}\n\n/* Решить задачу суммы подмножеств I (с повторяющимися подмножествами) */\nList<List<Integer>> subsetSumINaive(int[] nums, int target) {\n    List<Integer> state = new ArrayList<>(); // Состояние (подмножество)\n    int total = 0; // Сумма подмножеств\n    List<List<Integer>> res = new ArrayList<>(); // Список результатов (список подмножеств)\n    backtrack(state, target, total, nums, res);\n    return res;\n}\n
        subset_sum_i_naive.cs
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nvoid Backtrack(List<int> state, int target, int total, int[] choices, List<List<int>> res) {\n    // Если сумма подмножества равна target, записать решение\n    if (total == target) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // Перебор всех вариантов выбора\n    for (int i = 0; i < choices.Length; i++) {\n        // Отсечение: если сумма подмножества превышает target, пропустить этот выбор\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // Попытка: сделать выбор и обновить элемент и total\n        state.Add(choices[i]);\n        // Перейти к следующему выбору\n        Backtrack(state, target, total + choices[i], choices, res);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.RemoveAt(state.Count - 1);\n    }\n}\n\n/* Решить задачу суммы подмножеств I (с повторяющимися подмножествами) */\nList<List<int>> SubsetSumINaive(int[] nums, int target) {\n    List<int> state = []; // Состояние (подмножество)\n    int total = 0; // Сумма подмножеств\n    List<List<int>> res = []; // Список результатов (список подмножеств)\n    Backtrack(state, target, total, nums, res);\n    return res;\n}\n
        subset_sum_i_naive.go
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nfunc backtrackSubsetSumINaive(total, target int, state, choices *[]int, res *[][]int) {\n    // Если сумма подмножества равна target, записать решение\n    if target == total {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n        return\n    }\n    // Перебор всех вариантов выбора\n    for i := 0; i < len(*choices); i++ {\n        // Отсечение: если сумма подмножества превышает target, пропустить этот выбор\n        if total+(*choices)[i] > target {\n            continue\n        }\n        // Попытка: сделать выбор и обновить элемент и total\n        *state = append(*state, (*choices)[i])\n        // Перейти к следующему выбору\n        backtrackSubsetSumINaive(total+(*choices)[i], target, state, choices, res)\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        *state = (*state)[:len(*state)-1]\n    }\n}\n\n/* Решить задачу суммы подмножеств I (с повторяющимися подмножествами) */\nfunc subsetSumINaive(nums []int, target int) [][]int {\n    state := make([]int, 0) // Состояние (подмножество)\n    total := 0              // Сумма подмножеств\n    res := make([][]int, 0) // Список результатов (список подмножеств)\n    backtrackSubsetSumINaive(total, target, &state, &nums, &res)\n    return res\n}\n
        subset_sum_i_naive.swift
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nfunc backtrack(state: inout [Int], target: Int, total: Int, choices: [Int], res: inout [[Int]]) {\n    // Если сумма подмножества равна target, записать решение\n    if total == target {\n        res.append(state)\n        return\n    }\n    // Перебор всех вариантов выбора\n    for i in choices.indices {\n        // Отсечение: если сумма подмножества превышает target, пропустить этот выбор\n        if total + choices[i] > target {\n            continue\n        }\n        // Попытка: сделать выбор и обновить элемент и total\n        state.append(choices[i])\n        // Перейти к следующему выбору\n        backtrack(state: &state, target: target, total: total + choices[i], choices: choices, res: &res)\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.removeLast()\n    }\n}\n\n/* Решить задачу суммы подмножеств I (с повторяющимися подмножествами) */\nfunc subsetSumINaive(nums: [Int], target: Int) -> [[Int]] {\n    var state: [Int] = [] // Состояние (подмножество)\n    let total = 0 // Сумма подмножеств\n    var res: [[Int]] = [] // Список результатов (список подмножеств)\n    backtrack(state: &state, target: target, total: total, choices: nums, res: &res)\n    return res\n}\n
        subset_sum_i_naive.js
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nfunction backtrack(state, target, total, choices, res) {\n    // Если сумма подмножества равна target, записать решение\n    if (total === target) {\n        res.push([...state]);\n        return;\n    }\n    // Перебор всех вариантов выбора\n    for (let i = 0; i < choices.length; i++) {\n        // Отсечение: если сумма подмножества превышает target, пропустить этот выбор\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // Попытка: сделать выбор и обновить элемент и total\n        state.push(choices[i]);\n        // Перейти к следующему выбору\n        backtrack(state, target, total + choices[i], choices, res);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.pop();\n    }\n}\n\n/* Решить задачу суммы подмножеств I (с повторяющимися подмножествами) */\nfunction subsetSumINaive(nums, target) {\n    const state = []; // Состояние (подмножество)\n    const total = 0; // Сумма подмножеств\n    const res = []; // Список результатов (список подмножеств)\n    backtrack(state, target, total, nums, res);\n    return res;\n}\n
        subset_sum_i_naive.ts
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nfunction backtrack(\n    state: number[],\n    target: number,\n    total: number,\n    choices: number[],\n    res: number[][]\n): void {\n    // Если сумма подмножества равна target, записать решение\n    if (total === target) {\n        res.push([...state]);\n        return;\n    }\n    // Перебор всех вариантов выбора\n    for (let i = 0; i < choices.length; i++) {\n        // Отсечение: если сумма подмножества превышает target, пропустить этот выбор\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // Попытка: сделать выбор и обновить элемент и total\n        state.push(choices[i]);\n        // Перейти к следующему выбору\n        backtrack(state, target, total + choices[i], choices, res);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.pop();\n    }\n}\n\n/* Решить задачу суммы подмножеств I (с повторяющимися подмножествами) */\nfunction subsetSumINaive(nums: number[], target: number): number[][] {\n    const state = []; // Состояние (подмножество)\n    const total = 0; // Сумма подмножеств\n    const res = []; // Список результатов (список подмножеств)\n    backtrack(state, target, total, nums, res);\n    return res;\n}\n
        subset_sum_i_naive.dart
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nvoid backtrack(\n  List<int> state,\n  int target,\n  int total,\n  List<int> choices,\n  List<List<int>> res,\n) {\n  // Если сумма подмножества равна target, записать решение\n  if (total == target) {\n    res.add(List.from(state));\n    return;\n  }\n  // Перебор всех вариантов выбора\n  for (int i = 0; i < choices.length; i++) {\n    // Отсечение: если сумма подмножества превышает target, пропустить этот выбор\n    if (total + choices[i] > target) {\n      continue;\n    }\n    // Попытка: сделать выбор и обновить элемент и total\n    state.add(choices[i]);\n    // Перейти к следующему выбору\n    backtrack(state, target, total + choices[i], choices, res);\n    // Откат: отменить выбор и восстановить предыдущее состояние\n    state.removeLast();\n  }\n}\n\n/* Решить задачу суммы подмножеств I (с повторяющимися подмножествами) */\nList<List<int>> subsetSumINaive(List<int> nums, int target) {\n  List<int> state = []; // Состояние (подмножество)\n  int total = 0; // Сумма элементов\n  List<List<int>> res = []; // Список результатов (список подмножеств)\n  backtrack(state, target, total, nums, res);\n  return res;\n}\n
        subset_sum_i_naive.rs
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nfn backtrack(\n    state: &mut Vec<i32>,\n    target: i32,\n    total: i32,\n    choices: &[i32],\n    res: &mut Vec<Vec<i32>>,\n) {\n    // Если сумма подмножества равна target, записать решение\n    if total == target {\n        res.push(state.clone());\n        return;\n    }\n    // Перебор всех вариантов выбора\n    for i in 0..choices.len() {\n        // Отсечение: если сумма подмножества превышает target, пропустить этот выбор\n        if total + choices[i] > target {\n            continue;\n        }\n        // Попытка: сделать выбор и обновить элемент и total\n        state.push(choices[i]);\n        // Перейти к следующему выбору\n        backtrack(state, target, total + choices[i], choices, res);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.pop();\n    }\n}\n\n/* Решить задачу суммы подмножеств I (с повторяющимися подмножествами) */\nfn subset_sum_i_naive(nums: &[i32], target: i32) -> Vec<Vec<i32>> {\n    let mut state = Vec::new(); // Состояние (подмножество)\n    let total = 0; // Сумма подмножеств\n    let mut res = Vec::new(); // Список результатов (список подмножеств)\n    backtrack(&mut state, target, total, nums, &mut res);\n    res\n}\n
        subset_sum_i_naive.c
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nvoid backtrack(int target, int total, int *choices, int choicesSize) {\n    // Если сумма подмножества равна target, записать решение\n    if (total == target) {\n        for (int i = 0; i < stateSize; i++) {\n            res[resSize][i] = state[i];\n        }\n        resColSizes[resSize++] = stateSize;\n        return;\n    }\n    // Перебор всех вариантов выбора\n    for (int i = 0; i < choicesSize; i++) {\n        // Отсечение: если сумма подмножества превышает target, пропустить этот выбор\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // Попытка: сделать выбор и обновить элемент и total\n        state[stateSize++] = choices[i];\n        // Перейти к следующему выбору\n        backtrack(target, total + choices[i], choices, choicesSize);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        stateSize--;\n    }\n}\n\n/* Решить задачу суммы подмножеств I (с повторяющимися подмножествами) */\nvoid subsetSumINaive(int *nums, int numsSize, int target) {\n    resSize = 0; // Инициализировать число решений нулем\n    backtrack(target, 0, nums, numsSize);\n}\n
        subset_sum_i_naive.kt
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nfun backtrack(\n    state: MutableList<Int>,\n    target: Int,\n    total: Int,\n    choices: IntArray,\n    res: MutableList<MutableList<Int>?>\n) {\n    // Если сумма подмножества равна target, записать решение\n    if (total == target) {\n        res.add(state.toMutableList())\n        return\n    }\n    // Перебор всех вариантов выбора\n    for (i in choices.indices) {\n        // Отсечение: если сумма подмножества превышает target, пропустить этот выбор\n        if (total + choices[i] > target) {\n            continue\n        }\n        // Попытка: сделать выбор и обновить элемент и total\n        state.add(choices[i])\n        // Перейти к следующему выбору\n        backtrack(state, target, total + choices[i], choices, res)\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.removeAt(state.size - 1)\n    }\n}\n\n/* Решить задачу суммы подмножеств I (с повторяющимися подмножествами) */\nfun subsetSumINaive(nums: IntArray, target: Int): MutableList<MutableList<Int>?> {\n    val state = mutableListOf<Int>() // Состояние (подмножество)\n    val total = 0 // Сумма подмножеств\n    val res = mutableListOf<MutableList<Int>?>() // Список результатов (список подмножеств)\n    backtrack(state, target, total, nums, res)\n    return res\n}\n
        subset_sum_i_naive.rb
        ### Алгоритм бэктрекинга: сумма подмножеств I ###\ndef backtrack(state, target, total, choices, res)\n  # Если сумма подмножества равна target, записать решение\n  if total == target\n    res << state.dup\n    return\n  end\n\n  # Перебор всех вариантов выбора\n  for i in 0...choices.length\n    # Отсечение: если сумма подмножества превышает target, пропустить этот выбор\n    next if total + choices[i] > target\n    # Попытка: сделать выбор и обновить элемент и total\n    state << choices[i]\n    # Перейти к следующему выбору\n    backtrack(state, target, total + choices[i], choices, res)\n    # Откат: отменить выбор и восстановить предыдущее состояние\n    state.pop\n  end\nend\n\n### Алгоритм бэктрекинга: сумма подмножеств I ###\ndef backtrack(state, target, total, choices, res)\n  # Если сумма подмножества равна target, записать решение\n  if total == target\n    res << state.dup\n    return\n  end\n\n  # Перебор всех вариантов выбора\n  for i in 0...choices.length\n    # Отсечение: если сумма подмножества превышает target, пропустить этот выбор\n    next if total + choices[i] > target\n    # Попытка: сделать выбор и обновить элемент и total\n    state << choices[i]\n    # Перейти к следующему выбору\n    backtrack(state, target, total + choices[i], choices, res)\n    # Откат: отменить выбор и восстановить предыдущее состояние\n    state.pop\n  end\nend\n\n# ## Решить задачу суммы подмножеств I (с повторяющимися подмножествами) ###\ndef subset_sum_i_naive(nums, target)\n  state = [] # Состояние (подмножество)\n  total = 0 # Сумма подмножеств\n  res = [] # Список результатов (список подмножеств)\n  backtrack(state, target, total, nums, res)\n  res\nend\n
        Визуализация кода

        Во весь экран >

        Если подать на этот код массив \\([3, 4, 5]\\) и целевое значение \\(9\\) , то на выходе мы получим \\([3, 3, 3], [4, 5], [5, 4]\\) . Хотя все подмножества с суммой \\(9\\) успешно найдены, среди них все же присутствуют дубликаты: \\([4, 5]\\) и \\([5, 4]\\) .

        Причина в том, что процесс поиска различает порядок выбора, тогда как для подмножеств порядок не важен. Как показано на рисунке 13-10, сначала выбрать \\(4\\) , а затем \\(5\\) , и сначала выбрать \\(5\\) , а затем \\(4\\) - это разные ветви поиска, но им соответствует одно и то же подмножество.

        Рисунок 13-10   Поиск подмножеств и обрезка по выходу за границу

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

        • Когда массив содержит много элементов, а особенно когда target велик, процесс поиска порождает огромное число повторяющихся подмножеств.
        • Сравнение подмножеств (то есть массивов) само по себе довольно затратно: сначала приходится сортировать массивы, а затем поэлементно сравнивать их.
        ","path":["Глава 13. Поиск с возвратом","13.3   Задача о сумме подмножеств"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#2","level":3,"title":"2.   Обрезка повторяющихся подмножеств","text":"

        Поэтому стоит выполнять устранение дубликатов прямо во время поиска, с помощью обрезки. Посмотрите на рисунок 13-11: повторяющиеся подмножества возникают тогда, когда элементы массива выбираются в разном порядке, например так.

        1. Если в первом и втором раундах выбрать соответственно \\(3\\) и \\(4\\) , то будут сгенерированы все подмножества, содержащие эти два элемента, и их можно обозначить как \\([3, 4, \\dots]\\) .
        2. После этого, если в первом раунде выбрать \\(4\\) , то во втором раунде нужно пропустить \\(3\\) , потому что подмножества \\([4, 3, \\dots]\\) полностью дублируют подмножества, уже построенные на шаге 1. .

        Во время поиска варианты на каждом уровне пробуются по одному слева направо, поэтому чем правее ветвь, тем больше ветвей оказывается отсечено.

        1. В первых двух раундах выбираются \\(3\\) и \\(5\\) , что дает подмножества \\([3, 5, \\dots]\\) .
        2. В первых двух раундах выбираются \\(4\\) и \\(5\\) , что дает подмножества \\([4, 5, \\dots]\\) .
        3. Если же в первом раунде выбрать \\(5\\) , то во втором раунде нужно пропустить \\(3\\) и \\(4\\) , потому что подмножества \\([5, 3, \\dots]\\) и \\([5, 4, \\dots]\\) полностью дублируют случаи, описанные в шагах 1. и 2. .

        Рисунок 13-11   Повторяющиеся подмножества из-за разного порядка выбора

        В общем виде, если входной массив имеет вид \\([x_1, x_2, \\dots, x_n]\\) , а последовательность выборов в ходе поиска равна \\([x_{i_1}, x_{i_2}, \\dots, x_{i_m}]\\) , то она должна удовлетворять условию \\(i_1 \\leq i_2 \\leq \\dots \\leq i_m\\) ; все последовательности выборов, не удовлетворяющие этому условию, приводят к дубликатам и должны отсекаться.

        ","path":["Глава 13. Поиск с возвратом","13.3   Задача о сумме подмножеств"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#3","level":3,"title":"3.   Реализация кода","text":"

        Чтобы реализовать такую обрезку, инициализируем переменную start , которая будет указывать начальную точку обхода. После выбора элемента \\(x_i\\) следующий раунд начинается с индекса \\(i\\). Благодаря этому последовательность выборов всегда удовлетворяет условию \\(i_1 \\leq i_2 \\leq \\dots \\leq i_m\\) , а значит, каждое подмножество создается только один раз.

        Помимо этого, мы внесем в код еще два улучшения.

        • Перед началом поиска отсортируем массив nums . Тогда при обходе всех вариантов можно сразу прервать цикл, как только сумма подмножества превысит target , потому что все последующие элементы будут еще больше и их сумма тоже превысит target .
        • Откажемся от отдельной переменной суммы total и будем учитывать сумму через вычитание из target ; когда target станет равным \\(0\\) , решение фиксируется.
        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby subset_sum_i.py
        def backtrack(\n    state: list[int], target: int, choices: list[int], start: int, res: list[list[int]]\n):\n    \"\"\"Алгоритм бэктрекинга: сумма подмножеств I\"\"\"\n    # Если сумма подмножества равна target, записать решение\n    if target == 0:\n        res.append(list(state))\n        return\n    # Обойти все варианты выбора\n    # Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    for i in range(start, len(choices)):\n        # Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        # Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if target - choices[i] < 0:\n            break\n        # Попытка: сделать выбор и обновить target и start\n        state.append(choices[i])\n        # Перейти к следующему выбору\n        backtrack(state, target - choices[i], choices, i, res)\n        # Откат: отменить выбор и восстановить предыдущее состояние\n        state.pop()\n\ndef subset_sum_i(nums: list[int], target: int) -> list[list[int]]:\n    \"\"\"Решить задачу суммы подмножеств I\"\"\"\n    state = []  # Состояние (подмножество)\n    nums.sort()  # Отсортировать nums\n    start = 0  # Стартовая вершина обхода\n    res = []  # Список результатов (список подмножеств)\n    backtrack(state, target, nums, start, res)\n    return res\n
        subset_sum_i.cpp
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nvoid backtrack(vector<int> &state, int target, vector<int> &choices, int start, vector<vector<int>> &res) {\n    // Если сумма подмножества равна target, записать решение\n    if (target == 0) {\n        res.push_back(state);\n        return;\n    }\n    // Обойти все варианты выбора\n    // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    for (int i = start; i < choices.size(); i++) {\n        // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Попытка: сделать выбор и обновить target и start\n        state.push_back(choices[i]);\n        // Перейти к следующему выбору\n        backtrack(state, target - choices[i], choices, i, res);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.pop_back();\n    }\n}\n\n/* Решить задачу суммы подмножеств I */\nvector<vector<int>> subsetSumI(vector<int> &nums, int target) {\n    vector<int> state;              // Состояние (подмножество)\n    sort(nums.begin(), nums.end()); // Отсортировать nums\n    int start = 0;                  // Стартовая вершина обхода\n    vector<vector<int>> res;        // Список результатов (список подмножеств)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
        subset_sum_i.java
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nvoid backtrack(List<Integer> state, int target, int[] choices, int start, List<List<Integer>> res) {\n    // Если сумма подмножества равна target, записать решение\n    if (target == 0) {\n        res.add(new ArrayList<>(state));\n        return;\n    }\n    // Обойти все варианты выбора\n    // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    for (int i = start; i < choices.length; i++) {\n        // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Попытка: сделать выбор и обновить target и start\n        state.add(choices[i]);\n        // Перейти к следующему выбору\n        backtrack(state, target - choices[i], choices, i, res);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.remove(state.size() - 1);\n    }\n}\n\n/* Решить задачу суммы подмножеств I */\nList<List<Integer>> subsetSumI(int[] nums, int target) {\n    List<Integer> state = new ArrayList<>(); // Состояние (подмножество)\n    Arrays.sort(nums); // Отсортировать nums\n    int start = 0; // Стартовая вершина обхода\n    List<List<Integer>> res = new ArrayList<>(); // Список результатов (список подмножеств)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
        subset_sum_i.cs
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nvoid Backtrack(List<int> state, int target, int[] choices, int start, List<List<int>> res) {\n    // Если сумма подмножества равна target, записать решение\n    if (target == 0) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // Обойти все варианты выбора\n    // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    for (int i = start; i < choices.Length; i++) {\n        // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Попытка: сделать выбор и обновить target и start\n        state.Add(choices[i]);\n        // Перейти к следующему выбору\n        Backtrack(state, target - choices[i], choices, i, res);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.RemoveAt(state.Count - 1);\n    }\n}\n\n/* Решить задачу суммы подмножеств I */\nList<List<int>> SubsetSumI(int[] nums, int target) {\n    List<int> state = []; // Состояние (подмножество)\n    Array.Sort(nums); // Отсортировать nums\n    int start = 0; // Стартовая вершина обхода\n    List<List<int>> res = []; // Список результатов (список подмножеств)\n    Backtrack(state, target, nums, start, res);\n    return res;\n}\n
        subset_sum_i.go
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nfunc backtrackSubsetSumI(start, target int, state, choices *[]int, res *[][]int) {\n    // Если сумма подмножества равна target, записать решение\n    if target == 0 {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n        return\n    }\n    // Обойти все варианты выбора\n    // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    for i := start; i < len(*choices); i++ {\n        // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if target-(*choices)[i] < 0 {\n            break\n        }\n        // Попытка: сделать выбор и обновить target и start\n        *state = append(*state, (*choices)[i])\n        // Перейти к следующему выбору\n        backtrackSubsetSumI(i, target-(*choices)[i], state, choices, res)\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        *state = (*state)[:len(*state)-1]\n    }\n}\n\n/* Решить задачу суммы подмножеств I */\nfunc subsetSumI(nums []int, target int) [][]int {\n    state := make([]int, 0) // Состояние (подмножество)\n    sort.Ints(nums)         // Отсортировать nums\n    start := 0              // Стартовая вершина обхода\n    res := make([][]int, 0) // Список результатов (список подмножеств)\n    backtrackSubsetSumI(start, target, &state, &nums, &res)\n    return res\n}\n
        subset_sum_i.swift
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nfunc backtrack(state: inout [Int], target: Int, choices: [Int], start: Int, res: inout [[Int]]) {\n    // Если сумма подмножества равна target, записать решение\n    if target == 0 {\n        res.append(state)\n        return\n    }\n    // Обойти все варианты выбора\n    // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    for i in choices.indices.dropFirst(start) {\n        // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if target - choices[i] < 0 {\n            break\n        }\n        // Попытка: сделать выбор и обновить target и start\n        state.append(choices[i])\n        // Перейти к следующему выбору\n        backtrack(state: &state, target: target - choices[i], choices: choices, start: i, res: &res)\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.removeLast()\n    }\n}\n\n/* Решить задачу суммы подмножеств I */\nfunc subsetSumI(nums: [Int], target: Int) -> [[Int]] {\n    var state: [Int] = [] // Состояние (подмножество)\n    let nums = nums.sorted() // Отсортировать nums\n    let start = 0 // Стартовая вершина обхода\n    var res: [[Int]] = [] // Список результатов (список подмножеств)\n    backtrack(state: &state, target: target, choices: nums, start: start, res: &res)\n    return res\n}\n
        subset_sum_i.js
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nfunction backtrack(state, target, choices, start, res) {\n    // Если сумма подмножества равна target, записать решение\n    if (target === 0) {\n        res.push([...state]);\n        return;\n    }\n    // Обойти все варианты выбора\n    // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    for (let i = start; i < choices.length; i++) {\n        // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Попытка: сделать выбор и обновить target и start\n        state.push(choices[i]);\n        // Перейти к следующему выбору\n        backtrack(state, target - choices[i], choices, i, res);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.pop();\n    }\n}\n\n/* Решить задачу суммы подмножеств I */\nfunction subsetSumI(nums, target) {\n    const state = []; // Состояние (подмножество)\n    nums.sort((a, b) => a - b); // Отсортировать nums\n    const start = 0; // Стартовая вершина обхода\n    const res = []; // Список результатов (список подмножеств)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
        subset_sum_i.ts
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nfunction backtrack(\n    state: number[],\n    target: number,\n    choices: number[],\n    start: number,\n    res: number[][]\n): void {\n    // Если сумма подмножества равна target, записать решение\n    if (target === 0) {\n        res.push([...state]);\n        return;\n    }\n    // Обойти все варианты выбора\n    // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    for (let i = start; i < choices.length; i++) {\n        // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Попытка: сделать выбор и обновить target и start\n        state.push(choices[i]);\n        // Перейти к следующему выбору\n        backtrack(state, target - choices[i], choices, i, res);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.pop();\n    }\n}\n\n/* Решить задачу суммы подмножеств I */\nfunction subsetSumI(nums: number[], target: number): number[][] {\n    const state = []; // Состояние (подмножество)\n    nums.sort((a, b) => a - b); // Отсортировать nums\n    const start = 0; // Стартовая вершина обхода\n    const res = []; // Список результатов (список подмножеств)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
        subset_sum_i.dart
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nvoid backtrack(\n  List<int> state,\n  int target,\n  List<int> choices,\n  int start,\n  List<List<int>> res,\n) {\n  // Если сумма подмножества равна target, записать решение\n  if (target == 0) {\n    res.add(List.from(state));\n    return;\n  }\n  // Обойти все варианты выбора\n  // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n  for (int i = start; i < choices.length; i++) {\n    // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n    // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n    if (target - choices[i] < 0) {\n      break;\n    }\n    // Попытка: сделать выбор и обновить target и start\n    state.add(choices[i]);\n    // Перейти к следующему выбору\n    backtrack(state, target - choices[i], choices, i, res);\n    // Откат: отменить выбор и восстановить предыдущее состояние\n    state.removeLast();\n  }\n}\n\n/* Решить задачу суммы подмножеств I */\nList<List<int>> subsetSumI(List<int> nums, int target) {\n  List<int> state = []; // Состояние (подмножество)\n  nums.sort(); // Отсортировать nums\n  int start = 0; // Стартовая вершина обхода\n  List<List<int>> res = []; // Список результатов (список подмножеств)\n  backtrack(state, target, nums, start, res);\n  return res;\n}\n
        subset_sum_i.rs
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nfn backtrack(\n    state: &mut Vec<i32>,\n    target: i32,\n    choices: &[i32],\n    start: usize,\n    res: &mut Vec<Vec<i32>>,\n) {\n    // Если сумма подмножества равна target, записать решение\n    if target == 0 {\n        res.push(state.clone());\n        return;\n    }\n    // Обойти все варианты выбора\n    // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    for i in start..choices.len() {\n        // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if target - choices[i] < 0 {\n            break;\n        }\n        // Попытка: сделать выбор и обновить target и start\n        state.push(choices[i]);\n        // Перейти к следующему выбору\n        backtrack(state, target - choices[i], choices, i, res);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.pop();\n    }\n}\n\n/* Решить задачу суммы подмножеств I */\nfn subset_sum_i(nums: &mut [i32], target: i32) -> Vec<Vec<i32>> {\n    let mut state = Vec::new(); // Состояние (подмножество)\n    nums.sort(); // Отсортировать nums\n    let start = 0; // Стартовая вершина обхода\n    let mut res = Vec::new(); // Список результатов (список подмножеств)\n    backtrack(&mut state, target, nums, start, &mut res);\n    res\n}\n
        subset_sum_i.c
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nvoid backtrack(int target, int *choices, int choicesSize, int start) {\n    // Если сумма подмножества равна target, записать решение\n    if (target == 0) {\n        for (int i = 0; i < stateSize; ++i) {\n            res[resSize][i] = state[i];\n        }\n        resColSizes[resSize++] = stateSize;\n        return;\n    }\n    // Обойти все варианты выбора\n    // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    for (int i = start; i < choicesSize; i++) {\n        // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Попытка: сделать выбор и обновить target и start\n        state[stateSize] = choices[i];\n        stateSize++;\n        // Перейти к следующему выбору\n        backtrack(target - choices[i], choices, choicesSize, i);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        stateSize--;\n    }\n}\n\n/* Решить задачу суммы подмножеств I */\nvoid subsetSumI(int *nums, int numsSize, int target) {\n    qsort(nums, numsSize, sizeof(int), cmp); // Отсортировать nums\n    int start = 0;                           // Стартовая вершина обхода\n    backtrack(target, nums, numsSize, start);\n}\n
        subset_sum_i.kt
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nfun backtrack(\n    state: MutableList<Int>,\n    target: Int,\n    choices: IntArray,\n    start: Int,\n    res: MutableList<MutableList<Int>?>\n) {\n    // Если сумма подмножества равна target, записать решение\n    if (target == 0) {\n        res.add(state.toMutableList())\n        return\n    }\n    // Обойти все варианты выбора\n    // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    for (i in start..<choices.size) {\n        // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if (target - choices[i] < 0) {\n            break\n        }\n        // Попытка: сделать выбор и обновить target и start\n        state.add(choices[i])\n        // Перейти к следующему выбору\n        backtrack(state, target - choices[i], choices, i, res)\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.removeAt(state.size - 1)\n    }\n}\n\n/* Решить задачу суммы подмножеств I */\nfun subsetSumI(nums: IntArray, target: Int): MutableList<MutableList<Int>?> {\n    val state = mutableListOf<Int>() // Состояние (подмножество)\n    nums.sort() // Отсортировать nums\n    val start = 0 // Стартовая вершина обхода\n    val res = mutableListOf<MutableList<Int>?>() // Список результатов (список подмножеств)\n    backtrack(state, target, nums, start, res)\n    return res\n}\n
        subset_sum_i.rb
        ### Алгоритм бэктрекинга: сумма подмножеств I ###\ndef backtrack(state, target, choices, start, res)\n  # Если сумма подмножества равна target, записать решение\n  if target.zero?\n    res << state.dup\n    return\n  end\n  # Обойти все варианты выбора\n  # Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n  for i in start...choices.length\n    # Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n    # Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n    break if target - choices[i] < 0\n    # Попытка: сделать выбор и обновить target и start\n    state << choices[i]\n    # Перейти к следующему выбору\n    backtrack(state, target - choices[i], choices, i, res)\n    # Откат: отменить выбор и восстановить предыдущее состояние\n    state.pop\n  end\nend\n\n### Решить задачу суммы подмножеств I ###\ndef subset_sum_i(nums, target)\n  state = [] # Состояние (подмножество)\n  nums.sort! # Отсортировать nums\n  start = 0 # Стартовая вершина обхода\n  res = [] # Список результатов (список подмножеств)\n  backtrack(state, target, nums, start, res)\n  res\nend\n
        Визуализация кода

        Во весь экран >

        На рисунке 13-12 показан полный процесс поиска с возвратом для массива \\([3, 4, 5]\\) и целевого значения \\(9\\) .

        Рисунок 13-12   Процесс поиска с возвратом для задачи о сумме подмножеств I

        ","path":["Глава 13. Поиск с возвратом","13.3   Задача о сумме подмножеств"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1332","level":2,"title":"13.3.2   Учет повторяющихся элементов","text":"

        Question

        Дан массив положительных целых чисел nums и целое положительное значение target . Найдите все возможные комбинации, сумма элементов которых равна target . Во входном массиве могут присутствовать повторяющиеся элементы, и каждый элемент разрешено выбирать только один раз. Верните эти комбинации в виде списка; в результате не должно быть повторяющихся комбинаций.

        По сравнению с предыдущей задачей во входном массиве теперь могут присутствовать повторяющиеся элементы, и это создает новую проблему. Например, если дан массив \\([4, \\hat{4}, 5]\\) и целевое значение \\(9\\) , то существующий код вернет результат \\([4, 5], [\\hat{4}, 5]\\) , то есть с повторяющимся подмножеством.

        Причина появления дублей в том, что равные элементы выбираются несколько раз в одном и том же раунде. На рисунке 13-13 в первом раунде существует три варианта выбора, и два из них равны \\(4\\) ; из-за этого появляются две дублирующиеся ветви поиска и, соответственно, повторяющиеся подмножества. Точно так же два элемента \\(4\\) во втором раунде тоже порождают дубликаты.

        Рисунок 13-13   Повторяющиеся подмножества из-за равных элементов

        ","path":["Глава 13. Поиск с возвратом","13.3   Задача о сумме подмножеств"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1_1","level":3,"title":"1.   Обрезка равных элементов","text":"

        Чтобы решить эту проблему, нужно ограничить выбор равных элементов так, чтобы в каждом раунде каждый из них выбирался только один раз. Реализуется это довольно естественно: поскольку массив отсортирован, равные элементы стоят рядом. Значит, если в текущем раунде текущий элемент равен соседнему слева, то этот вариант уже был рассмотрен, и текущий элемент нужно пропустить.

        Одновременно по условию этой задачи каждый элемент массива можно выбрать только один раз. К счастью, это ограничение тоже можно реализовать через переменную start : после выбора элемента \\(x_i\\) следующий раунд начинается с индекса \\(i + 1\\) . Так мы одновременно убираем повторяющиеся подмножества и исключаем повторный выбор одного и того же элемента.

        ","path":["Глава 13. Поиск с возвратом","13.3   Задача о сумме подмножеств"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#2_1","level":3,"title":"2.   Реализация кода","text":"PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby subset_sum_ii.py
        def backtrack(\n    state: list[int], target: int, choices: list[int], start: int, res: list[list[int]]\n):\n    \"\"\"Алгоритм бэктрекинга: сумма подмножеств II\"\"\"\n    # Если сумма подмножества равна target, записать решение\n    if target == 0:\n        res.append(list(state))\n        return\n    # Обойти все варианты выбора\n    # Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    # Отсечение 3: начинать обход с start, чтобы избежать повторного выбора одного и того же элемента\n    for i in range(start, len(choices)):\n        # Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        # Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if target - choices[i] < 0:\n            break\n        # Отсечение 4: если этот элемент равен элементу слева, значит ветвь поиска повторяется, ее нужно сразу пропустить\n        if i > start and choices[i] == choices[i - 1]:\n            continue\n        # Попытка: сделать выбор и обновить target и start\n        state.append(choices[i])\n        # Перейти к следующему выбору\n        backtrack(state, target - choices[i], choices, i + 1, res)\n        # Откат: отменить выбор и восстановить предыдущее состояние\n        state.pop()\n\ndef subset_sum_ii(nums: list[int], target: int) -> list[list[int]]:\n    \"\"\"Решить задачу суммы подмножеств II\"\"\"\n    state = []  # Состояние (подмножество)\n    nums.sort()  # Отсортировать nums\n    start = 0  # Стартовая вершина обхода\n    res = []  # Список результатов (список подмножеств)\n    backtrack(state, target, nums, start, res)\n    return res\n
        subset_sum_ii.cpp
        /* Алгоритм бэктрекинга: сумма подмножеств II */\nvoid backtrack(vector<int> &state, int target, vector<int> &choices, int start, vector<vector<int>> &res) {\n    // Если сумма подмножества равна target, записать решение\n    if (target == 0) {\n        res.push_back(state);\n        return;\n    }\n    // Обойти все варианты выбора\n    // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    // Отсечение 3: начинать обход с start, чтобы избежать повторного выбора одного и того же элемента\n    for (int i = start; i < choices.size(); i++) {\n        // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Отсечение 4: если этот элемент равен элементу слева, значит ветвь поиска повторяется, ее нужно сразу пропустить\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue;\n        }\n        // Попытка: сделать выбор и обновить target и start\n        state.push_back(choices[i]);\n        // Перейти к следующему выбору\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.pop_back();\n    }\n}\n\n/* Решить задачу суммы подмножеств II */\nvector<vector<int>> subsetSumII(vector<int> &nums, int target) {\n    vector<int> state;              // Состояние (подмножество)\n    sort(nums.begin(), nums.end()); // Отсортировать nums\n    int start = 0;                  // Стартовая вершина обхода\n    vector<vector<int>> res;        // Список результатов (список подмножеств)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
        subset_sum_ii.java
        /* Алгоритм бэктрекинга: сумма подмножеств II */\nvoid backtrack(List<Integer> state, int target, int[] choices, int start, List<List<Integer>> res) {\n    // Если сумма подмножества равна target, записать решение\n    if (target == 0) {\n        res.add(new ArrayList<>(state));\n        return;\n    }\n    // Обойти все варианты выбора\n    // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    // Отсечение 3: начинать обход с start, чтобы избежать повторного выбора одного и того же элемента\n    for (int i = start; i < choices.length; i++) {\n        // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Отсечение 4: если этот элемент равен элементу слева, значит ветвь поиска повторяется, ее нужно сразу пропустить\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue;\n        }\n        // Попытка: сделать выбор и обновить target и start\n        state.add(choices[i]);\n        // Перейти к следующему выбору\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.remove(state.size() - 1);\n    }\n}\n\n/* Решить задачу суммы подмножеств II */\nList<List<Integer>> subsetSumII(int[] nums, int target) {\n    List<Integer> state = new ArrayList<>(); // Состояние (подмножество)\n    Arrays.sort(nums); // Отсортировать nums\n    int start = 0; // Стартовая вершина обхода\n    List<List<Integer>> res = new ArrayList<>(); // Список результатов (список подмножеств)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
        subset_sum_ii.cs
        /* Алгоритм бэктрекинга: сумма подмножеств II */\nvoid Backtrack(List<int> state, int target, int[] choices, int start, List<List<int>> res) {\n    // Если сумма подмножества равна target, записать решение\n    if (target == 0) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // Обойти все варианты выбора\n    // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    // Отсечение 3: начинать обход с start, чтобы избежать повторного выбора одного и того же элемента\n    for (int i = start; i < choices.Length; i++) {\n        // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Отсечение 4: если этот элемент равен элементу слева, значит ветвь поиска повторяется, ее нужно сразу пропустить\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue;\n        }\n        // Попытка: сделать выбор и обновить target и start\n        state.Add(choices[i]);\n        // Перейти к следующему выбору\n        Backtrack(state, target - choices[i], choices, i + 1, res);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.RemoveAt(state.Count - 1);\n    }\n}\n\n/* Решить задачу суммы подмножеств II */\nList<List<int>> SubsetSumII(int[] nums, int target) {\n    List<int> state = []; // Состояние (подмножество)\n    Array.Sort(nums); // Отсортировать nums\n    int start = 0; // Стартовая вершина обхода\n    List<List<int>> res = []; // Список результатов (список подмножеств)\n    Backtrack(state, target, nums, start, res);\n    return res;\n}\n
        subset_sum_ii.go
        /* Алгоритм бэктрекинга: сумма подмножеств II */\nfunc backtrackSubsetSumII(start, target int, state, choices *[]int, res *[][]int) {\n    // Если сумма подмножества равна target, записать решение\n    if target == 0 {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n        return\n    }\n    // Обойти все варианты выбора\n    // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    // Отсечение 3: начинать обход с start, чтобы избежать повторного выбора одного и того же элемента\n    for i := start; i < len(*choices); i++ {\n        // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if target-(*choices)[i] < 0 {\n            break\n        }\n        // Отсечение 4: если этот элемент равен элементу слева, значит ветвь поиска повторяется, ее нужно сразу пропустить\n        if i > start && (*choices)[i] == (*choices)[i-1] {\n            continue\n        }\n        // Попытка: сделать выбор и обновить target и start\n        *state = append(*state, (*choices)[i])\n        // Перейти к следующему выбору\n        backtrackSubsetSumII(i+1, target-(*choices)[i], state, choices, res)\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        *state = (*state)[:len(*state)-1]\n    }\n}\n\n/* Решить задачу суммы подмножеств II */\nfunc subsetSumII(nums []int, target int) [][]int {\n    state := make([]int, 0) // Состояние (подмножество)\n    sort.Ints(nums)         // Отсортировать nums\n    start := 0              // Стартовая вершина обхода\n    res := make([][]int, 0) // Список результатов (список подмножеств)\n    backtrackSubsetSumII(start, target, &state, &nums, &res)\n    return res\n}\n
        subset_sum_ii.swift
        /* Алгоритм бэктрекинга: сумма подмножеств II */\nfunc backtrack(state: inout [Int], target: Int, choices: [Int], start: Int, res: inout [[Int]]) {\n    // Если сумма подмножества равна target, записать решение\n    if target == 0 {\n        res.append(state)\n        return\n    }\n    // Обойти все варианты выбора\n    // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    // Отсечение 3: начинать обход с start, чтобы избежать повторного выбора одного и того же элемента\n    for i in choices.indices.dropFirst(start) {\n        // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if target - choices[i] < 0 {\n            break\n        }\n        // Отсечение 4: если этот элемент равен элементу слева, значит ветвь поиска повторяется, ее нужно сразу пропустить\n        if i > start, choices[i] == choices[i - 1] {\n            continue\n        }\n        // Попытка: сделать выбор и обновить target и start\n        state.append(choices[i])\n        // Перейти к следующему выбору\n        backtrack(state: &state, target: target - choices[i], choices: choices, start: i + 1, res: &res)\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.removeLast()\n    }\n}\n\n/* Решить задачу суммы подмножеств II */\nfunc subsetSumII(nums: [Int], target: Int) -> [[Int]] {\n    var state: [Int] = [] // Состояние (подмножество)\n    let nums = nums.sorted() // Отсортировать nums\n    let start = 0 // Стартовая вершина обхода\n    var res: [[Int]] = [] // Список результатов (список подмножеств)\n    backtrack(state: &state, target: target, choices: nums, start: start, res: &res)\n    return res\n}\n
        subset_sum_ii.js
        /* Алгоритм бэктрекинга: сумма подмножеств II */\nfunction backtrack(state, target, choices, start, res) {\n    // Если сумма подмножества равна target, записать решение\n    if (target === 0) {\n        res.push([...state]);\n        return;\n    }\n    // Обойти все варианты выбора\n    // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    // Отсечение 3: начинать обход с start, чтобы избежать повторного выбора одного и того же элемента\n    for (let i = start; i < choices.length; i++) {\n        // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Отсечение 4: если этот элемент равен элементу слева, значит ветвь поиска повторяется, ее нужно сразу пропустить\n        if (i > start && choices[i] === choices[i - 1]) {\n            continue;\n        }\n        // Попытка: сделать выбор и обновить target и start\n        state.push(choices[i]);\n        // Перейти к следующему выбору\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.pop();\n    }\n}\n\n/* Решить задачу суммы подмножеств II */\nfunction subsetSumII(nums, target) {\n    const state = []; // Состояние (подмножество)\n    nums.sort((a, b) => a - b); // Отсортировать nums\n    const start = 0; // Стартовая вершина обхода\n    const res = []; // Список результатов (список подмножеств)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
        subset_sum_ii.ts
        /* Алгоритм бэктрекинга: сумма подмножеств II */\nfunction backtrack(\n    state: number[],\n    target: number,\n    choices: number[],\n    start: number,\n    res: number[][]\n): void {\n    // Если сумма подмножества равна target, записать решение\n    if (target === 0) {\n        res.push([...state]);\n        return;\n    }\n    // Обойти все варианты выбора\n    // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    // Отсечение 3: начинать обход с start, чтобы избежать повторного выбора одного и того же элемента\n    for (let i = start; i < choices.length; i++) {\n        // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Отсечение 4: если этот элемент равен элементу слева, значит ветвь поиска повторяется, ее нужно сразу пропустить\n        if (i > start && choices[i] === choices[i - 1]) {\n            continue;\n        }\n        // Попытка: сделать выбор и обновить target и start\n        state.push(choices[i]);\n        // Перейти к следующему выбору\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.pop();\n    }\n}\n\n/* Решить задачу суммы подмножеств II */\nfunction subsetSumII(nums: number[], target: number): number[][] {\n    const state = []; // Состояние (подмножество)\n    nums.sort((a, b) => a - b); // Отсортировать nums\n    const start = 0; // Стартовая вершина обхода\n    const res = []; // Список результатов (список подмножеств)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
        subset_sum_ii.dart
        /* Алгоритм бэктрекинга: сумма подмножеств II */\nvoid backtrack(\n  List<int> state,\n  int target,\n  List<int> choices,\n  int start,\n  List<List<int>> res,\n) {\n  // Если сумма подмножества равна target, записать решение\n  if (target == 0) {\n    res.add(List.from(state));\n    return;\n  }\n  // Обойти все варианты выбора\n  // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n  // Отсечение 3: начинать обход с start, чтобы избежать повторного выбора одного и того же элемента\n  for (int i = start; i < choices.length; i++) {\n    // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n    // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n    if (target - choices[i] < 0) {\n      break;\n    }\n    // Отсечение 4: если этот элемент равен элементу слева, значит ветвь поиска повторяется, ее нужно сразу пропустить\n    if (i > start && choices[i] == choices[i - 1]) {\n      continue;\n    }\n    // Попытка: сделать выбор и обновить target и start\n    state.add(choices[i]);\n    // Перейти к следующему выбору\n    backtrack(state, target - choices[i], choices, i + 1, res);\n    // Откат: отменить выбор и восстановить предыдущее состояние\n    state.removeLast();\n  }\n}\n\n/* Решить задачу суммы подмножеств II */\nList<List<int>> subsetSumII(List<int> nums, int target) {\n  List<int> state = []; // Состояние (подмножество)\n  nums.sort(); // Отсортировать nums\n  int start = 0; // Стартовая вершина обхода\n  List<List<int>> res = []; // Список результатов (список подмножеств)\n  backtrack(state, target, nums, start, res);\n  return res;\n}\n
        subset_sum_ii.rs
        /* Алгоритм бэктрекинга: сумма подмножеств II */\nfn backtrack(\n    state: &mut Vec<i32>,\n    target: i32,\n    choices: &[i32],\n    start: usize,\n    res: &mut Vec<Vec<i32>>,\n) {\n    // Если сумма подмножества равна target, записать решение\n    if target == 0 {\n        res.push(state.clone());\n        return;\n    }\n    // Обойти все варианты выбора\n    // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    // Отсечение 3: начинать обход с start, чтобы избежать повторного выбора одного и того же элемента\n    for i in start..choices.len() {\n        // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if target - choices[i] < 0 {\n            break;\n        }\n        // Отсечение 4: если этот элемент равен элементу слева, значит ветвь поиска повторяется, ее нужно сразу пропустить\n        if i > start && choices[i] == choices[i - 1] {\n            continue;\n        }\n        // Попытка: сделать выбор и обновить target и start\n        state.push(choices[i]);\n        // Перейти к следующему выбору\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.pop();\n    }\n}\n\n/* Решить задачу суммы подмножеств II */\nfn subset_sum_ii(nums: &mut [i32], target: i32) -> Vec<Vec<i32>> {\n    let mut state = Vec::new(); // Состояние (подмножество)\n    nums.sort(); // Отсортировать nums\n    let start = 0; // Стартовая вершина обхода\n    let mut res = Vec::new(); // Список результатов (список подмножеств)\n    backtrack(&mut state, target, nums, start, &mut res);\n    res\n}\n
        subset_sum_ii.c
        /* Алгоритм бэктрекинга: сумма подмножеств II */\nvoid backtrack(int target, int *choices, int choicesSize, int start) {\n    // Если сумма подмножества равна target, записать решение\n    if (target == 0) {\n        for (int i = 0; i < stateSize; i++) {\n            res[resSize][i] = state[i];\n        }\n        resColSizes[resSize++] = stateSize;\n        return;\n    }\n    // Обойти все варианты выбора\n    // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    // Отсечение 3: начинать обход с start, чтобы избежать повторного выбора одного и того же элемента\n    for (int i = start; i < choicesSize; i++) {\n        // Отсечение 1: если сумма подмножества превышает target, сразу пропустить\n        if (target - choices[i] < 0) {\n            continue;\n        }\n        // Отсечение 4: если этот элемент равен элементу слева, значит ветвь поиска повторяется, ее нужно сразу пропустить\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue;\n        }\n        // Попытка: сделать выбор и обновить target и start\n        state[stateSize] = choices[i];\n        stateSize++;\n        // Перейти к следующему выбору\n        backtrack(target - choices[i], choices, choicesSize, i + 1);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        stateSize--;\n    }\n}\n\n/* Решить задачу суммы подмножеств II */\nvoid subsetSumII(int *nums, int numsSize, int target) {\n    // Отсортировать nums\n    qsort(nums, numsSize, sizeof(int), cmp);\n    // Начать бэктрекинг\n    backtrack(target, nums, numsSize, 0);\n}\n
        subset_sum_ii.kt
        /* Алгоритм бэктрекинга: сумма подмножеств II */\nfun backtrack(\n    state: MutableList<Int>,\n    target: Int,\n    choices: IntArray,\n    start: Int,\n    res: MutableList<MutableList<Int>?>\n) {\n    // Если сумма подмножества равна target, записать решение\n    if (target == 0) {\n        res.add(state.toMutableList())\n        return\n    }\n    // Обойти все варианты выбора\n    // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    // Отсечение 3: начинать обход с start, чтобы избежать повторного выбора одного и того же элемента\n    for (i in start..<choices.size) {\n        // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if (target - choices[i] < 0) {\n            break\n        }\n        // Отсечение 4: если этот элемент равен элементу слева, значит ветвь поиска повторяется, ее нужно сразу пропустить\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue\n        }\n        // Попытка: сделать выбор и обновить target и start\n        state.add(choices[i])\n        // Перейти к следующему выбору\n        backtrack(state, target - choices[i], choices, i + 1, res)\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.removeAt(state.size - 1)\n    }\n}\n\n/* Решить задачу суммы подмножеств II */\nfun subsetSumII(nums: IntArray, target: Int): MutableList<MutableList<Int>?> {\n    val state = mutableListOf<Int>() // Состояние (подмножество)\n    nums.sort() // Отсортировать nums\n    val start = 0 // Стартовая вершина обхода\n    val res = mutableListOf<MutableList<Int>?>() // Список результатов (список подмножеств)\n    backtrack(state, target, nums, start, res)\n    return res\n}\n
        subset_sum_ii.rb
        ### Алгоритм бэктрекинга: сумма подмножеств II ###\ndef backtrack(state, target, choices, start, res)\n  # Если сумма подмножества равна target, записать решение\n  if target.zero?\n    res << state.dup\n    return\n  end\n\n  # Обойти все варианты выбора\n  # Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n  # Отсечение 3: начинать обход с start, чтобы избежать повторного выбора одного и того же элемента\n  for i in start...choices.length\n    # Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n    # Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n    break if target - choices[i] < 0\n    # Отсечение 4: если этот элемент равен элементу слева, значит ветвь поиска повторяется, ее нужно сразу пропустить\n    next if i > start && choices[i] == choices[i - 1]\n    # Попытка: сделать выбор и обновить target и start\n    state << choices[i]\n    # Перейти к следующему выбору\n    backtrack(state, target - choices[i], choices, i + 1, res)\n    # Откат: отменить выбор и восстановить предыдущее состояние\n    state.pop\n  end\nend\n\n### Решить задачу суммы подмножеств II ###\ndef subset_sum_ii(nums, target)\n  state = [] # Состояние (подмножество)\n  nums.sort! # Отсортировать nums\n  start = 0 # Стартовая вершина обхода\n  res = [] # Список результатов (список подмножеств)\n  backtrack(state, target, nums, start, res)\n  res\nend\n
        Визуализация кода

        Во весь экран >

        На рисунке 13-14 показан процесс поиска с возвратом для массива \\([4, 4, 5]\\) и целевого значения \\(9\\) . В нем используются четыре вида обрезки. Попробуйте сопоставить рисунок с комментариями в коде, чтобы понять полный процесс поиска и то, как работает каждый тип обрезки.

        Рисунок 13-14   Процесс поиска с возвратом для задачи о сумме подмножеств II

        ","path":["Глава 13. Поиск с возвратом","13.3   Задача о сумме подмножеств"],"tags":[]},{"location":"chapter_backtracking/summary/","level":1,"title":"13.5   Резюме","text":"","path":["Глава 13. Поиск с возвратом","13.5   Резюме"],"tags":[]},{"location":"chapter_backtracking/summary/#1","level":3,"title":"1.   Ключевые выводы","text":"
        • Алгоритм поиска с возвратом по своей сути является методом полного перебора: он ищет решения путем обхода пространства решений в глубину. Во время поиска он фиксирует решения, удовлетворяющие условиям, пока не найдет все такие решения или пока обход не завершится.
        • Процесс поиска с возвратом состоит из двух частей: попытки и отката. Он с помощью поиска в глубину пробует разные варианты выбора; когда встречается состояние, не удовлетворяющее ограничениям, алгоритм отменяет предыдущий выбор, возвращается к прошлому состоянию и продолжает пробовать другие варианты. Попытка и откат являются двумя противоположными по направлению действиями.
        • Задачи поиска с возвратом обычно содержат несколько ограничений, которые можно использовать для обрезки. Обрезка позволяет заранее завершать ненужные ветви поиска и тем самым значительно повышать эффективность.
        • Алгоритм поиска с возвратом в первую очередь применяется для решения поисковых задач и задач с ограничениями. Задачи комбинаторной оптимизации тоже можно решать с его помощью, но для них часто существуют более эффективные или более подходящие методы.
        • Задача о перестановках нацелена на поиск всех возможных перестановок элементов данного множества. Мы используем массив для записи того, был ли выбран каждый элемент, и отсекаем ветви, где один и тот же элемент выбирается повторно, чтобы гарантировать однократный выбор каждого элемента.
        • В задаче о перестановках, если во множестве присутствуют повторяющиеся элементы, в итоговом результате возникнут повторяющиеся перестановки. Поэтому нужно ограничить выбор равных элементов так, чтобы в каждом раунде каждый из них выбирался только один раз; обычно это реализуется с помощью хеш-множества.
        • Цель задачи о сумме подмножеств - найти все подмножества данного множества, сумма которых равна целевому значению. В множестве порядок элементов не важен, однако процесс поиска порождает результаты во всех возможных порядках, из-за чего появляются повторяющиеся подмножества. Поэтому перед запуском поиска с возвратом мы сортируем данные и вводим переменную, указывающую начальную точку обхода в каждом раунде, чтобы отсечь ветви, создающие дубликаты.
        • В задаче о сумме подмножеств равные элементы массива также порождают повторяющиеся множества. При наличии предварительной сортировки их можно отсекать, проверяя равенство соседних элементов, и тем самым гарантировать, что в каждом раунде равные элементы будут выбираться только один раз.
        • Задача о \\(n\\) ферзях состоит в поиске способов разместить \\(n\\) ферзей на доске размера \\(n \\times n\\) так, чтобы никакие два ферзя не атаковали друг друга. Ограничения этой задачи включают строки, столбцы, главные диагонали и побочные диагонали. Чтобы выполнить ограничение по строкам, используется построчная стратегия размещения, гарантирующая по одному ферзю в каждой строке.
        • Обработка ограничений по столбцам и диагоналям устроена похожим образом. Для ограничения по столбцам используется массив, фиксирующий наличие ферзя в каждом столбце. Для диагоналей используются два массива, записывающие наличие ферзей на главных и побочных диагоналях. Основная сложность здесь состоит в том, чтобы найти закономерность индексов строк и столбцов клеток, лежащих на одной и той же главной или побочной диагонали.
        ","path":["Глава 13. Поиск с возвратом","13.5   Резюме"],"tags":[]},{"location":"chapter_backtracking/summary/#2","level":3,"title":"2.   Вопросы и ответы","text":"

        Q: Как понять связь между поиском с возвратом и рекурсией?

        В целом поиск с возвратом - это скорее \"алгоритмическая стратегия\", а рекурсия больше похожа на \"инструмент\".

        • Алгоритмы поиска с возвратом обычно реализуются на основе рекурсии. Однако поиск с возвратом - это лишь один из вариантов применения рекурсии, а именно ее использование в поисковых задачах.
        • Структура рекурсии отражает парадигму разбиения на подзадачи и часто применяется для решения задач \"разделяй и властвуй\", поиска с возвратом, динамического программирования (мемоизированной рекурсии) и других подобных задач.
        ","path":["Глава 13. Поиск с возвратом","13.5   Резюме"],"tags":[]},{"location":"chapter_computational_complexity/","level":1,"title":"Глава 2.   Анализ сложности","text":"

        Abstract

        Анализ сложности подобен пространственно-временному проводнику в необъятной вселенной алгоритмов.

        Он ведет нас в глубину двух измерений - времени и пространства, помогая искать более изящные решения.

        ","path":["Глава 2. Анализ сложности","Глава 2.   Анализ сложности"],"tags":[]},{"location":"chapter_computational_complexity/#_1","level":2,"title":"Содержание главы","text":"
        • 2.1   Оценка эффективности алгоритмов
        • 2.2   Итерация и рекурсия
        • 2.3   Временная сложность
        • 2.4   Пространственная сложность
        • 2.5   Резюме
        ","path":["Глава 2. Анализ сложности","Глава 2.   Анализ сложности"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/","level":1,"title":"2.2   Итерация и рекурсия","text":"

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

        ","path":["Глава 2. Анализ сложности","2.2   Итерация и рекурсия"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#221","level":2,"title":"2.2.1   Итерация","text":"

        Итерация (iteration) - это структура управления, которая позволяет повторно выполнять определенную задачу. В итерации программа повторяет выполнение определенного участка кода, пока выполняется определенное условие.

        ","path":["Глава 2. Анализ сложности","2.2   Итерация и рекурсия"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#1-for","level":3,"title":"1.   Цикл for","text":"

        Цикл for - одна из наиболее распространенных форм итерации, которая подходит для использования, когда количество итераций известно заранее.

        Следующая функция реализует суммирование \\(1 + 2 + \\dots + n\\) с использованием цикла for , а результат суммирования сохраняется в переменной res . Следует отметить, что в Python диапазон range(a, b) соответствует левому закрытому, правому открытому интервалу, то есть перебираются значения \\(a, a + 1, \\dots, b-1\\) :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py
        def for_loop(n: int) -> int:\n    \"\"\"Цикл for\"\"\"\n    res = 0\n    # Циклическое суммирование 1, 2, ..., n-1, n\n    for i in range(1, n + 1):\n        res += i\n    return res\n
        iteration.cpp
        /* Цикл for */\nint forLoop(int n) {\n    int res = 0;\n    // Циклическое суммирование 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; ++i) {\n        res += i;\n    }\n    return res;\n}\n
        iteration.java
        /* Цикл for */\nint forLoop(int n) {\n    int res = 0;\n    // Циклическое суммирование 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; i++) {\n        res += i;\n    }\n    return res;\n}\n
        iteration.cs
        /* Цикл for */\nint ForLoop(int n) {\n    int res = 0;\n    // Циклическое суммирование 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; i++) {\n        res += i;\n    }\n    return res;\n}\n
        iteration.go
        /* Цикл for */\nfunc forLoop(n int) int {\n    res := 0\n    // Циклическое суммирование 1, 2, ..., n-1, n\n    for i := 1; i <= n; i++ {\n        res += i\n    }\n    return res\n}\n
        iteration.swift
        /* Цикл for */\nfunc forLoop(n: Int) -> Int {\n    var res = 0\n    // Циклическое суммирование 1, 2, ..., n-1, n\n    for i in 1 ... n {\n        res += i\n    }\n    return res\n}\n
        iteration.js
        /* Цикл for */\nfunction forLoop(n) {\n    let res = 0;\n    // Циклическое суммирование 1, 2, ..., n-1, n\n    for (let i = 1; i <= n; i++) {\n        res += i;\n    }\n    return res;\n}\n
        iteration.ts
        /* Цикл for */\nfunction forLoop(n: number): number {\n    let res = 0;\n    // Циклическое суммирование 1, 2, ..., n-1, n\n    for (let i = 1; i <= n; i++) {\n        res += i;\n    }\n    return res;\n}\n
        iteration.dart
        /* Цикл for */\nint forLoop(int n) {\n  int res = 0;\n  // Циклическое суммирование 1, 2, ..., n-1, n\n  for (int i = 1; i <= n; i++) {\n    res += i;\n  }\n  return res;\n}\n
        iteration.rs
        /* Цикл for */\nfn for_loop(n: i32) -> i32 {\n    let mut res = 0;\n    // Циклическое суммирование 1, 2, ..., n-1, n\n    for i in 1..=n {\n        res += i;\n    }\n    res\n}\n
        iteration.c
        /* Цикл for */\nint forLoop(int n) {\n    int res = 0;\n    // Циклическое суммирование 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; i++) {\n        res += i;\n    }\n    return res;\n}\n
        iteration.kt
        /* Цикл for */\nfun forLoop(n: Int): Int {\n    var res = 0\n    // Циклическое суммирование 1, 2, ..., n-1, n\n    for (i in 1..n) {\n        res += i\n    }\n    return res\n}\n
        iteration.rb
        ### Цикл for ###\ndef for_loop(n)\n  res = 0\n\n  # Циклическое суммирование 1, 2, ..., n-1, n\n  for i in 1..n\n    res += i\n  end\n\n  res\nend\n
        Визуализация кода

        Во весь экран >

        Ниже представлена блок-схема этой функции суммирования.

        Рисунок 2-1   Блок-схема функции суммирования

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

        ","path":["Глава 2. Анализ сложности","2.2   Итерация и рекурсия"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#2-while","level":3,"title":"2.   Цикл while","text":"

        Подобно циклу for , цикл while также представляет собой метод реализации итерации. В цикле while программа перед каждой итерацией проверяет условие: если условие истинно, то выполнение продолжается, иначе цикл завершается.

        Ниже приведен пример реализации суммирования \\(1 + 2 + \\dots + n\\) с использованием цикла while :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py
        def while_loop(n: int) -> int:\n    \"\"\"Цикл while\"\"\"\n    res = 0\n    i = 1  # Инициализация условной переменной\n    # Циклическое суммирование 1, 2, ..., n-1, n\n    while i <= n:\n        res += i\n        i += 1  # Обновить условную переменную\n    return res\n
        iteration.cpp
        /* Цикл while */\nint whileLoop(int n) {\n    int res = 0;\n    int i = 1; // Инициализация условной переменной\n    // Циклическое суммирование 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i++; // Обновить условную переменную\n    }\n    return res;\n}\n
        iteration.java
        /* Цикл while */\nint whileLoop(int n) {\n    int res = 0;\n    int i = 1; // Инициализация условной переменной\n    // Циклическое суммирование 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i++; // Обновить условную переменную\n    }\n    return res;\n}\n
        iteration.cs
        /* Цикл while */\nint WhileLoop(int n) {\n    int res = 0;\n    int i = 1; // Инициализация условной переменной\n    // Циклическое суммирование 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i += 1; // Обновить условную переменную\n    }\n    return res;\n}\n
        iteration.go
        /* Цикл while */\nfunc whileLoop(n int) int {\n    res := 0\n    // Инициализация условной переменной\n    i := 1\n    // Циклическое суммирование 1, 2, ..., n-1, n\n    for i <= n {\n        res += i\n        // Обновить условную переменную\n        i++\n    }\n    return res\n}\n
        iteration.swift
        /* Цикл while */\nfunc whileLoop(n: Int) -> Int {\n    var res = 0\n    var i = 1 // Инициализация условной переменной\n    // Циклическое суммирование 1, 2, ..., n-1, n\n    while i <= n {\n        res += i\n        i += 1 // Обновить условную переменную\n    }\n    return res\n}\n
        iteration.js
        /* Цикл while */\nfunction whileLoop(n) {\n    let res = 0;\n    let i = 1; // Инициализация условной переменной\n    // Циклическое суммирование 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i++; // Обновить условную переменную\n    }\n    return res;\n}\n
        iteration.ts
        /* Цикл while */\nfunction whileLoop(n: number): number {\n    let res = 0;\n    let i = 1; // Инициализация условной переменной\n    // Циклическое суммирование 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i++; // Обновить условную переменную\n    }\n    return res;\n}\n
        iteration.dart
        /* Цикл while */\nint whileLoop(int n) {\n  int res = 0;\n  int i = 1; // Инициализация условной переменной\n  // Циклическое суммирование 1, 2, ..., n-1, n\n  while (i <= n) {\n    res += i;\n    i++; // Обновить условную переменную\n  }\n  return res;\n}\n
        iteration.rs
        /* Цикл while */\nfn while_loop(n: i32) -> i32 {\n    let mut res = 0;\n    let mut i = 1; // Инициализация условной переменной\n\n    // Циклическое суммирование 1, 2, ..., n-1, n\n    while i <= n {\n        res += i;\n        i += 1; // Обновить условную переменную\n    }\n    res\n}\n
        iteration.c
        /* Цикл while */\nint whileLoop(int n) {\n    int res = 0;\n    int i = 1; // Инициализация условной переменной\n    // Циклическое суммирование 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i++; // Обновить условную переменную\n    }\n    return res;\n}\n
        iteration.kt
        /* Цикл while */\nfun whileLoop(n: Int): Int {\n    var res = 0\n    var i = 1 // Инициализация условной переменной\n    // Циклическое суммирование 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i\n        i++ // Обновить условную переменную\n    }\n    return res\n}\n
        iteration.rb
        ### Цикл while ###\ndef while_loop(n)\n  res = 0\n  i = 1 # Инициализация условной переменной\n\n  # Циклическое суммирование 1, 2, ..., n-1, n\n  while i <= n\n    res += i\n    i += 1 # Обновить условную переменную\n  end\n\n  res\nend\n
        Визуализация кода

        Во весь экран >

        **Цикл while обладает большей степенью свободы по сравнению с циклом for **. В цикле while можно свободно управлять инициализацией и обновлением условной переменной.

        Например, в следующем коде условная переменная \\(i\\) обновляется дважды на каждой итерации, что затруднительно сделать с использованием цикла for :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py
        def while_loop_ii(n: int) -> int:\n    \"\"\"Цикл while (двойное обновление)\"\"\"\n    res = 0\n    i = 1  # Инициализация условной переменной\n    # Циклическое суммирование 1, 4, 10, ...\n    while i <= n:\n        res += i\n        # Обновить условную переменную\n        i += 1\n        i *= 2\n    return res\n
        iteration.cpp
        /* Цикл while (двойное обновление) */\nint whileLoopII(int n) {\n    int res = 0;\n    int i = 1; // Инициализация условной переменной\n    // Циклическое суммирование 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // Обновить условную переменную\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
        iteration.java
        /* Цикл while (двойное обновление) */\nint whileLoopII(int n) {\n    int res = 0;\n    int i = 1; // Инициализация условной переменной\n    // Циклическое суммирование 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // Обновить условную переменную\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
        iteration.cs
        /* Цикл while (двойное обновление) */\nint WhileLoopII(int n) {\n    int res = 0;\n    int i = 1; // Инициализация условной переменной\n    // Циклическое суммирование 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // Обновить условную переменную\n        i += 1; \n        i *= 2;\n    }\n    return res;\n}\n
        iteration.go
        /* Цикл while (двойное обновление) */\nfunc whileLoopII(n int) int {\n    res := 0\n    // Инициализация условной переменной\n    i := 1\n    // Циклическое суммирование 1, 4, 10, ...\n    for i <= n {\n        res += i\n        // Обновить условную переменную\n        i++\n        i *= 2\n    }\n    return res\n}\n
        iteration.swift
        /* Цикл while (двойное обновление) */\nfunc whileLoopII(n: Int) -> Int {\n    var res = 0\n    var i = 1 // Инициализация условной переменной\n    // Циклическое суммирование 1, 4, 10, ...\n    while i <= n {\n        res += i\n        // Обновить условную переменную\n        i += 1\n        i *= 2\n    }\n    return res\n}\n
        iteration.js
        /* Цикл while (двойное обновление) */\nfunction whileLoopII(n) {\n    let res = 0;\n    let i = 1; // Инициализация условной переменной\n    // Циклическое суммирование 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // Обновить условную переменную\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
        iteration.ts
        /* Цикл while (двойное обновление) */\nfunction whileLoopII(n: number): number {\n    let res = 0;\n    let i = 1; // Инициализация условной переменной\n    // Циклическое суммирование 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // Обновить условную переменную\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
        iteration.dart
        /* Цикл while (двойное обновление) */\nint whileLoopII(int n) {\n  int res = 0;\n  int i = 1; // Инициализация условной переменной\n  // Циклическое суммирование 1, 4, 10, ...\n  while (i <= n) {\n    res += i;\n    // Обновить условную переменную\n    i++;\n    i *= 2;\n  }\n  return res;\n}\n
        iteration.rs
        /* Цикл while (двойное обновление) */\nfn while_loop_ii(n: i32) -> i32 {\n    let mut res = 0;\n    let mut i = 1; // Инициализация условной переменной\n\n    // Циклическое суммирование 1, 4, 10, ...\n    while i <= n {\n        res += i;\n        // Обновить условную переменную\n        i += 1;\n        i *= 2;\n    }\n    res\n}\n
        iteration.c
        /* Цикл while (двойное обновление) */\nint whileLoopII(int n) {\n    int res = 0;\n    int i = 1; // Инициализация условной переменной\n    // Циклическое суммирование 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // Обновить условную переменную\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
        iteration.kt
        /* Цикл while (двойное обновление) */\nfun whileLoopII(n: Int): Int {\n    var res = 0\n    var i = 1 // Инициализация условной переменной\n    // Циклическое суммирование 1, 4, 10, ...\n    while (i <= n) {\n        res += i\n        // Обновить условную переменную\n        i++\n        i *= 2\n    }\n    return res\n}\n
        iteration.rb
        ### Цикл while ###\ndef while_loop(n)\n  res = 0\n  i = 1 # Инициализация условной переменной\n\n  # Циклическое суммирование 1, 2, ..., n-1, n\n  while i <= n\n    res += i\n    i += 1 # Обновить условную переменную\n  end\n\n  res\nend\n\n# ## Цикл while (двойное обновление) ###\ndef while_loop_ii(n)\n  res = 0\n  i = 1 # Инициализация условной переменной\n\n  # Циклическое суммирование 1, 4, 10, ...\n  while i <= n\n    res += i\n    # Обновить условную переменную\n    i += 1\n    i *= 2\n  end\n\n  res\nend\n
        Визуализация кода

        Во весь экран >

        В целом код с использованием цикла for более компактный, а цикл while более гибкий. Но они оба могут реализовать итерационную структуру. Выбор между ними определяется требованиями конкретной задачи.

        ","path":["Глава 2. Анализ сложности","2.2   Итерация и рекурсия"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#3","level":3,"title":"3.   Вложенные циклы","text":"

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

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py
        def nested_for_loop(n: int) -> str:\n    \"\"\"Двойной цикл for\"\"\"\n    res = \"\"\n    # Цикл по i = 1, 2, ..., n-1, n\n    for i in range(1, n + 1):\n        # Цикл по j = 1, 2, ..., n-1, n\n        for j in range(1, n + 1):\n            res += f\"({i}, {j}), \"\n    return res\n
        iteration.cpp
        /* Двойной цикл for */\nstring nestedForLoop(int n) {\n    ostringstream res;\n    // Цикл по i = 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; ++i) {\n        // Цикл по j = 1, 2, ..., n-1, n\n        for (int j = 1; j <= n; ++j) {\n            res << \"(\" << i << \", \" << j << \"), \";\n        }\n    }\n    return res.str();\n}\n
        iteration.java
        /* Двойной цикл for */\nString nestedForLoop(int n) {\n    StringBuilder res = new StringBuilder();\n    // Цикл по i = 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; i++) {\n        // Цикл по j = 1, 2, ..., n-1, n\n        for (int j = 1; j <= n; j++) {\n            res.append(\"(\" + i + \", \" + j + \"), \");\n        }\n    }\n    return res.toString();\n}\n
        iteration.cs
        /* Двойной цикл for */\nstring NestedForLoop(int n) {\n    StringBuilder res = new();\n    // Цикл по i = 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; i++) {\n        // Цикл по j = 1, 2, ..., n-1, n\n        for (int j = 1; j <= n; j++) {\n            res.Append($\"({i}, {j}), \");\n        }\n    }\n    return res.ToString();\n}\n
        iteration.go
        /* Двойной цикл for */\nfunc nestedForLoop(n int) string {\n    res := \"\"\n    // Цикл по i = 1, 2, ..., n-1, n\n    for i := 1; i <= n; i++ {\n        for j := 1; j <= n; j++ {\n            // Цикл по j = 1, 2, ..., n-1, n\n            res += fmt.Sprintf(\"(%d, %d), \", i, j)\n        }\n    }\n    return res\n}\n
        iteration.swift
        /* Двойной цикл for */\nfunc nestedForLoop(n: Int) -> String {\n    var res = \"\"\n    // Цикл по i = 1, 2, ..., n-1, n\n    for i in 1 ... n {\n        // Цикл по j = 1, 2, ..., n-1, n\n        for j in 1 ... n {\n            res.append(\"(\\(i), \\(j)), \")\n        }\n    }\n    return res\n}\n
        iteration.js
        /* Двойной цикл for */\nfunction nestedForLoop(n) {\n    let res = '';\n    // Цикл по i = 1, 2, ..., n-1, n\n    for (let i = 1; i <= n; i++) {\n        // Цикл по j = 1, 2, ..., n-1, n\n        for (let j = 1; j <= n; j++) {\n            res += `(${i}, ${j}), `;\n        }\n    }\n    return res;\n}\n
        iteration.ts
        /* Двойной цикл for */\nfunction nestedForLoop(n: number): string {\n    let res = '';\n    // Цикл по i = 1, 2, ..., n-1, n\n    for (let i = 1; i <= n; i++) {\n        // Цикл по j = 1, 2, ..., n-1, n\n        for (let j = 1; j <= n; j++) {\n            res += `(${i}, ${j}), `;\n        }\n    }\n    return res;\n}\n
        iteration.dart
        /* Двойной цикл for */\nString nestedForLoop(int n) {\n  String res = \"\";\n  // Цикл по i = 1, 2, ..., n-1, n\n  for (int i = 1; i <= n; i++) {\n    // Цикл по j = 1, 2, ..., n-1, n\n    for (int j = 1; j <= n; j++) {\n      res += \"($i, $j), \";\n    }\n  }\n  return res;\n}\n
        iteration.rs
        /* Двойной цикл for */\nfn nested_for_loop(n: i32) -> String {\n    let mut res = vec![];\n    // Цикл по i = 1, 2, ..., n-1, n\n    for i in 1..=n {\n        // Цикл по j = 1, 2, ..., n-1, n\n        for j in 1..=n {\n            res.push(format!(\"({}, {}), \", i, j));\n        }\n    }\n    res.join(\"\")\n}\n
        iteration.c
        /* Двойной цикл for */\nchar *nestedForLoop(int n) {\n    // n * n — это число соответствующих точек, а максимальная длина строки \"(i, j), \" равна 6+10*2, плюс дополнительное место для завершающего нулевого символа \\0\n    int size = n * n * 26 + 1;\n    char *res = malloc(size * sizeof(char));\n    // Цикл по i = 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; i++) {\n        // Цикл по j = 1, 2, ..., n-1, n\n        for (int j = 1; j <= n; j++) {\n            char tmp[26];\n            snprintf(tmp, sizeof(tmp), \"(%d, %d), \", i, j);\n            strncat(res, tmp, size - strlen(res) - 1);\n        }\n    }\n    return res;\n}\n
        iteration.kt
        /* Двойной цикл for */\nfun nestedForLoop(n: Int): String {\n    val res = StringBuilder()\n    // Цикл по i = 1, 2, ..., n-1, n\n    for (i in 1..n) {\n        // Цикл по j = 1, 2, ..., n-1, n\n        for (j in 1..n) {\n            res.append(\" ($i, $j), \")\n        }\n    }\n    return res.toString()\n}\n
        iteration.rb
        ### Двойной цикл for ###\ndef nested_for_loop(n)\n  res = \"\"\n\n  # Цикл по i = 1, 2, ..., n-1, n\n  for i in 1..n\n    # Цикл по j = 1, 2, ..., n-1, n\n    for j in 1..n\n      res += \"(#{i}, #{j}), \"\n    end\n  end\n\n  res\nend\n
        Визуализация кода

        Во весь экран >

        Ниже приведена блок-схема такого вложенного цикла.

        Рисунок 2-2   Блок-схема вложенного цикла

        В этом случае количество выполненных действий пропорционально \\(n^2\\) , или, другими словами, время выполнения алгоритма и размер входных данных \\(n\\) находятся в квадратичной зависимости.

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

        ","path":["Глава 2. Анализ сложности","2.2   Итерация и рекурсия"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#222","level":2,"title":"2.2.2   Рекурсия","text":"

        Рекурсия (recursion) - это стратегия алгоритма, при которой функция вызывает саму себя для решения задачи. Она включает два основных этапа.

        1. Вызов: программа постоянно вызывает саму себя, обычно передавая меньшие или более упрощенные параметры, пока не будет достигнуто условие завершения.
        2. Возврат: после срабатывания условия завершения программа начинает возвращаться из самой глубокой рекурсивной функции, объединяя результаты каждого уровня.

        С точки зрения реализации рекурсивный код включает три основных элемента.

        1. Условие завершения: используется для определения момента перехода от вызова к возврату.
        2. Рекурсивный вызов: соответствует вызову, функция вызывает саму себя, обычно с меньшими или упрощенными параметрами.
        3. Возврат результата: соответствует возврату, возвращает результат текущего уровня рекурсии на предыдущий уровень.

        Рассмотрим следующий код: вызов функции recur(n) позволяет вычислить сумму \\(1 + 2 + \\dots + n\\) :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py
        def recur(n: int) -> int:\n    \"\"\"Рекурсия\"\"\"\n    # Условие завершения\n    if n == 1:\n        return 1\n    # Рекурсия: рекурсивный вызов\n    res = recur(n - 1)\n    # Возврат: вернуть результат\n    return n + res\n
        recursion.cpp
        /* Рекурсия */\nint recur(int n) {\n    // Условие завершения\n    if (n == 1)\n        return 1;\n    // Рекурсия: рекурсивный вызов\n    int res = recur(n - 1);\n    // Возврат: вернуть результат\n    return n + res;\n}\n
        recursion.java
        /* Рекурсия */\nint recur(int n) {\n    // Условие завершения\n    if (n == 1)\n        return 1;\n    // Рекурсия: рекурсивный вызов\n    int res = recur(n - 1);\n    // Возврат: вернуть результат\n    return n + res;\n}\n
        recursion.cs
        /* Рекурсия */\nint Recur(int n) {\n    // Условие завершения\n    if (n == 1)\n        return 1;\n    // Рекурсия: рекурсивный вызов\n    int res = Recur(n - 1);\n    // Возврат: вернуть результат\n    return n + res;\n}\n
        recursion.go
        /* Рекурсия */\nfunc recur(n int) int {\n    // Условие завершения\n    if n == 1 {\n        return 1\n    }\n    // Рекурсия: рекурсивный вызов\n    res := recur(n - 1)\n    // Возврат: вернуть результат\n    return n + res\n}\n
        recursion.swift
        /* Рекурсия */\nfunc recur(n: Int) -> Int {\n    // Условие завершения\n    if n == 1 {\n        return 1\n    }\n    // Рекурсия: рекурсивный вызов\n    let res = recur(n: n - 1)\n    // Возврат: вернуть результат\n    return n + res\n}\n
        recursion.js
        /* Рекурсия */\nfunction recur(n) {\n    // Условие завершения\n    if (n === 1) return 1;\n    // Рекурсия: рекурсивный вызов\n    const res = recur(n - 1);\n    // Возврат: вернуть результат\n    return n + res;\n}\n
        recursion.ts
        /* Рекурсия */\nfunction recur(n: number): number {\n    // Условие завершения\n    if (n === 1) return 1;\n    // Рекурсия: рекурсивный вызов\n    const res = recur(n - 1);\n    // Возврат: вернуть результат\n    return n + res;\n}\n
        recursion.dart
        /* Рекурсия */\nint recur(int n) {\n  // Условие завершения\n  if (n == 1) return 1;\n  // Рекурсия: рекурсивный вызов\n  int res = recur(n - 1);\n  // Возврат: вернуть результат\n  return n + res;\n}\n
        recursion.rs
        /* Рекурсия */\nfn recur(n: i32) -> i32 {\n    // Условие завершения\n    if n == 1 {\n        return 1;\n    }\n    // Рекурсия: рекурсивный вызов\n    let res = recur(n - 1);\n    // Возврат: вернуть результат\n    n + res\n}\n
        recursion.c
        /* Рекурсия */\nint recur(int n) {\n    // Условие завершения\n    if (n == 1)\n        return 1;\n    // Рекурсия: рекурсивный вызов\n    int res = recur(n - 1);\n    // Возврат: вернуть результат\n    return n + res;\n}\n
        recursion.kt
        /* Рекурсия */\nfun recur(n: Int): Int {\n    // Условие завершения\n    if (n == 1)\n        return 1\n    // Рекурсивный шаг: рекурсивный вызов\n    val res = recur(n - 1)\n    // Возврат: вернуть результат\n    return n + res\n}\n
        recursion.rb
        ### Рекурсия ###\ndef recur(n)\n  # Условие завершения\n  return 1 if n == 1\n  # Рекурсия: рекурсивный вызов\n  res = recur(n - 1)\n  # Возврат: вернуть результат\n  n + res\nend\n
        Визуализация кода

        Во весь экран >

        Ниже представлен рекурсивный процесс этой функции.

        Рисунок 2-3   Рекурсивный процесс функции суммирования

        Хотя с точки зрения вычислений итерация и рекурсия могут давать одинаковый результат, они представляют собой совершенно разные парадигмы мышления и решения задач.

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

        Рассмотрим в качестве примера вышеупомянутую функцию суммирования, где решается задача \\(f(n) = 1 + 2 + \\dots + n\\) .

        • Итерация: моделирование процесса суммирования в цикле проходит от \\(1\\) до \\(n\\) , выполняя операцию суммирования на каждом шаге, чтобы получить итоговое значение \\(f(n)\\) .
        • Рекурсия: последовательное разбиение задачи на подзадачи вида \\(f(n) = n + f(n - 1)\\) до достижения базового случая \\(f(1) = 1\\) .
        ","path":["Глава 2. Анализ сложности","2.2   Итерация и рекурсия"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#1","level":3,"title":"1.   Стек вызовов","text":"

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

        • Контекстные данные функции хранятся в области памяти, называемой пространством стекового кадра, и освобождаются только после возврата функции. Поэтому рекурсия обычно требует больше памяти, чем итерация.
        • Рекурсивный вызов функции создает дополнительные накладные расходы. Поэтому рекурсия обычно менее эффективна по времени, чем цикл.

        Как показано на рисунке 2-4, до срабатывания условия завершения одновременно существует \\(n\\) невозвращенных рекурсивных функций, а глубина рекурсии равна \\(n\\) .

        Рисунок 2-4   Глубина рекурсивного вызова

        На практике глубина рекурсии, разрешенная языком программирования, обычно ограничена, и слишком глубокая рекурсия может привести к ошибке переполнения стека.

        ","path":["Глава 2. Анализ сложности","2.2   Итерация и рекурсия"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#2","level":3,"title":"2.   Хвостовая рекурсия","text":"

        Интересно, что если рекурсивный вызов происходит на последнем шаге перед возвратом функции , то компилятор или интерпретатор может оптимизировать этот вызов, сделав его по эффективности использования памяти сопоставимым с итерацией. Это называется хвостовой рекурсией (tail recursion).

        • Обычная рекурсия: когда функция возвращается на предыдущий уровень, необходимо продолжить выполнение кода, поэтому системе нужно сохранить контекст предыдущего вызова.
        • Хвостовая рекурсия: рекурсивный вызов является последней операцией перед возвратом функции, что означает, что после возврата на предыдущий уровень не требуется выполнять другие операции, поэтому системе не нужно сохранять контекст предыдущей функции.

        В качестве примера вычисления суммы \\(1 + 2 + \\dots + n\\) можно установить переменную результата res в качестве параметра функции, чтобы реализовать хвостовую рекурсию:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py
        def tail_recur(n, res):\n    \"\"\"Хвостовая рекурсия\"\"\"\n    # Условие завершения\n    if n == 0:\n        return res\n    # Хвостовой рекурсивный вызов\n    return tail_recur(n - 1, res + n)\n
        recursion.cpp
        /* Хвостовая рекурсия */\nint tailRecur(int n, int res) {\n    // Условие завершения\n    if (n == 0)\n        return res;\n    // Хвостовой рекурсивный вызов\n    return tailRecur(n - 1, res + n);\n}\n
        recursion.java
        /* Хвостовая рекурсия */\nint tailRecur(int n, int res) {\n    // Условие завершения\n    if (n == 0)\n        return res;\n    // Хвостовой рекурсивный вызов\n    return tailRecur(n - 1, res + n);\n}\n
        recursion.cs
        /* Хвостовая рекурсия */\nint TailRecur(int n, int res) {\n    // Условие завершения\n    if (n == 0)\n        return res;\n    // Хвостовой рекурсивный вызов\n    return TailRecur(n - 1, res + n);\n}\n
        recursion.go
        /* Хвостовая рекурсия */\nfunc tailRecur(n int, res int) int {\n    // Условие завершения\n    if n == 0 {\n        return res\n    }\n    // Хвостовой рекурсивный вызов\n    return tailRecur(n-1, res+n)\n}\n
        recursion.swift
        /* Хвостовая рекурсия */\nfunc tailRecur(n: Int, res: Int) -> Int {\n    // Условие завершения\n    if n == 0 {\n        return res\n    }\n    // Хвостовой рекурсивный вызов\n    return tailRecur(n: n - 1, res: res + n)\n}\n
        recursion.js
        /* Хвостовая рекурсия */\nfunction tailRecur(n, res) {\n    // Условие завершения\n    if (n === 0) return res;\n    // Хвостовой рекурсивный вызов\n    return tailRecur(n - 1, res + n);\n}\n
        recursion.ts
        /* Хвостовая рекурсия */\nfunction tailRecur(n: number, res: number): number {\n    // Условие завершения\n    if (n === 0) return res;\n    // Хвостовой рекурсивный вызов\n    return tailRecur(n - 1, res + n);\n}\n
        recursion.dart
        /* Хвостовая рекурсия */\nint tailRecur(int n, int res) {\n  // Условие завершения\n  if (n == 0) return res;\n  // Хвостовой рекурсивный вызов\n  return tailRecur(n - 1, res + n);\n}\n
        recursion.rs
        /* Хвостовая рекурсия */\nfn tail_recur(n: i32, res: i32) -> i32 {\n    // Условие завершения\n    if n == 0 {\n        return res;\n    }\n    // Хвостовой рекурсивный вызов\n    tail_recur(n - 1, res + n)\n}\n
        recursion.c
        /* Хвостовая рекурсия */\nint tailRecur(int n, int res) {\n    // Условие завершения\n    if (n == 0)\n        return res;\n    // Хвостовой рекурсивный вызов\n    return tailRecur(n - 1, res + n);\n}\n
        recursion.kt
        /* Хвостовая рекурсия */\ntailrec fun tailRecur(n: Int, res: Int): Int {\n    // Добавить ключевое слово tailrec, чтобы включить оптимизацию хвостовой рекурсии\n    // Условие завершения\n    if (n == 0)\n        return res\n    // Хвостовой рекурсивный вызов\n    return tailRecur(n - 1, res + n)\n}\n
        recursion.rb
        ### Хвостовая рекурсия ###\ndef tail_recur(n, res)\n  # Условие завершения\n  return res if n == 0\n  # Хвостовой рекурсивный вызов\n  tail_recur(n - 1, res + n)\nend\n
        Визуализация кода

        Во весь экран >

        Процесс выполнения хвостовой рекурсии показан на рисунке 2-5. Сравнивая обычную и хвостовую рекурсии, можно заметить, что точка выполнения операции суммирования у них различается.

        • Обычная рекурсия: операция суммирования выполняется в процессе возврата, после каждого возврата необходимо снова выполнить операцию суммирования.
        • Хвостовая рекурсия: операция суммирования выполняется в процессе вызова, а процесс возврата требует только последовательного возврата.

        Рисунок 2-5   Процесс хвостовой рекурсии

        Tip

        Обратите внимание: многие компиляторы и интерпретаторы не поддерживают оптимизацию хвостовой рекурсии. Например, Python по умолчанию такую оптимизацию не выполняет, поэтому даже функция в хвостово-рекурсивной форме все равно может привести к переполнению стека.

        ","path":["Глава 2. Анализ сложности","2.2   Итерация и рекурсия"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#3_1","level":3,"title":"3.   Дерево рекурсии","text":"

        При решении задач, связанных с алгоритмами типа \"разделяй и властвуй\", рекурсия часто оказывается более интуитивной и читабельной, чем итерация. Рассмотрим в качестве примера последовательность Фибоначчи.

        Question

        Дана последовательность Фибоначчи \\(0, 1, 1, 2, 3, 5, 8, 13, \\dots\\) ; найди \\(n\\)-й элемент этой последовательности.

        Обозначив \\(n\\)-й член последовательности Фибоначчи как \\(f(n)\\) , можно сформулировать два утверждения.

        • Первые два числа последовательности: \\(f(1) = 0\\) и \\(f(2) = 1\\) .
        • Каждое число последовательности является суммой двух предыдущих чисел, то есть \\(f(n) = f(n - 1) + f(n - 2)\\) .

        Используя рекурсивные вызовы в соответствии с рекуррентным соотношением и принимая первые два числа за условия остановки, можно написать рекурсивный код. Вызов fib(n) позволит получить \\(n\\)-й член последовательности Фибоначчи:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py
        def fib(n: int) -> int:\n    \"\"\"Последовательность Фибоначчи: рекурсия\"\"\"\n    # Условие завершения: f(1) = 0, f(2) = 1\n    if n == 1 or n == 2:\n        return n - 1\n    # Рекурсивный вызов f(n) = f(n-1) + f(n-2)\n    res = fib(n - 1) + fib(n - 2)\n    # Вернуть результат f(n)\n    return res\n
        recursion.cpp
        /* Последовательность Фибоначчи: рекурсия */\nint fib(int n) {\n    // Условие завершения: f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1;\n    // Рекурсивный вызов f(n) = f(n-1) + f(n-2)\n    int res = fib(n - 1) + fib(n - 2);\n    // Вернуть результат f(n)\n    return res;\n}\n
        recursion.java
        /* Последовательность Фибоначчи: рекурсия */\nint fib(int n) {\n    // Условие завершения: f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1;\n    // Рекурсивный вызов f(n) = f(n-1) + f(n-2)\n    int res = fib(n - 1) + fib(n - 2);\n    // Вернуть результат f(n)\n    return res;\n}\n
        recursion.cs
        /* Последовательность Фибоначчи: рекурсия */\nint Fib(int n) {\n    // Условие завершения: f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1;\n    // Рекурсивный вызов f(n) = f(n-1) + f(n-2)\n    int res = Fib(n - 1) + Fib(n - 2);\n    // Вернуть результат f(n)\n    return res;\n}\n
        recursion.go
        /* Последовательность Фибоначчи: рекурсия */\nfunc fib(n int) int {\n    // Условие завершения: f(1) = 0, f(2) = 1\n    if n == 1 || n == 2 {\n        return n - 1\n    }\n    // Рекурсивный вызов f(n) = f(n-1) + f(n-2)\n    res := fib(n-1) + fib(n-2)\n    // Вернуть результат f(n)\n    return res\n}\n
        recursion.swift
        /* Последовательность Фибоначчи: рекурсия */\nfunc fib(n: Int) -> Int {\n    // Условие завершения: f(1) = 0, f(2) = 1\n    if n == 1 || n == 2 {\n        return n - 1\n    }\n    // Рекурсивный вызов f(n) = f(n-1) + f(n-2)\n    let res = fib(n: n - 1) + fib(n: n - 2)\n    // Вернуть результат f(n)\n    return res\n}\n
        recursion.js
        /* Последовательность Фибоначчи: рекурсия */\nfunction fib(n) {\n    // Условие завершения: f(1) = 0, f(2) = 1\n    if (n === 1 || n === 2) return n - 1;\n    // Рекурсивный вызов f(n) = f(n-1) + f(n-2)\n    const res = fib(n - 1) + fib(n - 2);\n    // Вернуть результат f(n)\n    return res;\n}\n
        recursion.ts
        /* Последовательность Фибоначчи: рекурсия */\nfunction fib(n: number): number {\n    // Условие завершения: f(1) = 0, f(2) = 1\n    if (n === 1 || n === 2) return n - 1;\n    // Рекурсивный вызов f(n) = f(n-1) + f(n-2)\n    const res = fib(n - 1) + fib(n - 2);\n    // Вернуть результат f(n)\n    return res;\n}\n
        recursion.dart
        /* Последовательность Фибоначчи: рекурсия */\nint fib(int n) {\n  // Условие завершения: f(1) = 0, f(2) = 1\n  if (n == 1 || n == 2) return n - 1;\n  // Рекурсивный вызов f(n) = f(n-1) + f(n-2)\n  int res = fib(n - 1) + fib(n - 2);\n  // Вернуть результат f(n)\n  return res;\n}\n
        recursion.rs
        /* Последовательность Фибоначчи: рекурсия */\nfn fib(n: i32) -> i32 {\n    // Условие завершения: f(1) = 0, f(2) = 1\n    if n == 1 || n == 2 {\n        return n - 1;\n    }\n    // Рекурсивный вызов f(n) = f(n-1) + f(n-2)\n    let res = fib(n - 1) + fib(n - 2);\n    // Вернуть результат\n    res\n}\n
        recursion.c
        /* Последовательность Фибоначчи: рекурсия */\nint fib(int n) {\n    // Условие завершения: f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1;\n    // Рекурсивный вызов f(n) = f(n-1) + f(n-2)\n    int res = fib(n - 1) + fib(n - 2);\n    // Вернуть результат f(n)\n    return res;\n}\n
        recursion.kt
        /* Последовательность Фибоначчи: рекурсия */\nfun fib(n: Int): Int {\n    // Условие завершения: f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1\n    // Рекурсивный вызов f(n) = f(n-1) + f(n-2)\n    val res = fib(n - 1) + fib(n - 2)\n    // Вернуть результат f(n)\n    return res\n}\n
        recursion.rb
        ### Последовательность Фибоначчи: рекурсия ###\ndef fib(n)\n  # Условие завершения: f(1) = 0, f(2) = 1\n  return n - 1 if n == 1 || n == 2\n  # Рекурсивный вызов f(n) = f(n-1) + f(n-2)\n  res = fib(n - 1) + fib(n - 2)\n  # Вернуть результат f(n)\n  res\nend\n
        Визуализация кода

        Во весь экран >

        Проанализировав приведенный код, можно заметить, что внутри функции осуществляется рекурсивный вызов двух функций, то есть из одного вызова образуются два ветвления. Как показано на рисунке 2-6, при последующем выполнении рекурсивных вызовов в итоге образуется дерево рекурсии (recursion tree) глубиной \\(n\\) .

        Рисунок 2-6   Дерево рекурсии последовательности Фибоначчи

        По своей сути рекурсия отражает парадигму мышления \"разбиение задачи на более мелкие подзадачи\", что делает стратегию \"разделяй и властвуй\" крайне важной.

        • С точки зрения алгоритмов многие важные алгоритмические стратегии, такие как поиск, сортировка, возврат, \"разделяй и властвуй\" и динамическое программирование, прямо или косвенно используют этот подход.
        • С точки зрения структур данных рекурсия естественно подходит для решения задач, связанных со списками, деревьями и графами, поскольку они очень хорошо поддаются анализу с использованием идеи \"разделяй и властвуй\".
        ","path":["Глава 2. Анализ сложности","2.2   Итерация и рекурсия"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#223","level":2,"title":"2.2.3   Сравнение","text":"

        Подводя итог, можно сказать, что итерация и рекурсия различаются по реализации, производительности и применимости, как показано в таблице 2-1.

        Таблица 2-1   Сравнение итерации и рекурсии

        Итерация Рекурсия Способ реализации Циклическая структура Функция вызывает саму себя Временная эффективность Обычно высокая эффективность, нет затрат на вызов функции Каждый вызов функции создает затраты Использование памяти Обычно используется фиксированный объем памяти Накопление вызовов функции может использовать значительное количество пространства стека Сфера использования Подходит для простых циклических задач, код интуитивно понятен и хорошо читаем Подходит для разбиения на подзадачи, для структур деревья и графы, алгоритмов \"разделяй и властвуй\", возврата и т. д.; структура кода проста и ясна

        Tip

        Если дальнейшее содержание кажется сложным, можно вернуться к нему после чтения главы о \"стеке\".

        Какова же внутренняя связь между итерацией и рекурсией? В рассмотренном примере рекурсивной функции операция сложения выполняется на этапе возврата рекурсии. Это означает, что функция, вызванная первой, фактически завершает операцию сложения последней, что соответствует принципу стека \"первым пришел - последним вышел\".

        На самом деле такие термины рекурсии, как \"стек вызовов\" и \"пространство стекового кадра\", уже намекают на тесную связь между рекурсией и стеком.

        1. Вызов: когда вызывается функция, система выделяет для нее новый стековый кадр в \"стеке вызовов\" для хранения локальных переменных функции, параметров, адреса возврата и других данных.
        2. Возврат: когда функция завершает выполнение и возвращает результат, соответствующий стековый кадр удаляется из \"стека вызовов\", восстанавливая среду выполнения предыдущей функции.

        Таким образом, можно использовать явный стек для моделирования поведения стека вызовов, чтобы преобразовать рекурсию в итеративную форму:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py
        def for_loop_recur(n: int) -> int:\n    \"\"\"Имитация рекурсии итерацией\"\"\"\n    # Использовать явный стек для имитации системного стека вызовов\n    stack = []\n    res = 0\n    # Рекурсия: рекурсивный вызов\n    for i in range(n, 0, -1):\n        # Имитировать «рекурсию» с помощью операции помещения в стек\n        stack.append(i)\n    # Возврат: вернуть результат\n    while stack:\n        # Имитировать «возврат» с помощью операции извлечения из стека\n        res += stack.pop()\n    # res = 1+2+3+...+n\n    return res\n
        recursion.cpp
        /* Имитация рекурсии итерацией */\nint forLoopRecur(int n) {\n    // Использовать явный стек для имитации системного стека вызовов\n    stack<int> stack;\n    int res = 0;\n    // Рекурсия: рекурсивный вызов\n    for (int i = n; i > 0; i--) {\n        // Имитировать «рекурсию» с помощью операции помещения в стек\n        stack.push(i);\n    }\n    // Возврат: вернуть результат\n    while (!stack.empty()) {\n        // Имитировать «возврат» с помощью операции извлечения из стека\n        res += stack.top();\n        stack.pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
        recursion.java
        /* Имитация рекурсии итерацией */\nint forLoopRecur(int n) {\n    // Использовать явный стек для имитации системного стека вызовов\n    Stack<Integer> stack = new Stack<>();\n    int res = 0;\n    // Рекурсия: рекурсивный вызов\n    for (int i = n; i > 0; i--) {\n        // Имитировать «рекурсию» с помощью операции помещения в стек\n        stack.push(i);\n    }\n    // Возврат: вернуть результат\n    while (!stack.isEmpty()) {\n        // Имитировать «возврат» с помощью операции извлечения из стека\n        res += stack.pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
        recursion.cs
        /* Имитация рекурсии итерацией */\nint ForLoopRecur(int n) {\n    // Использовать явный стек для имитации системного стека вызовов\n    Stack<int> stack = new();\n    int res = 0;\n    // Рекурсия: рекурсивный вызов\n    for (int i = n; i > 0; i--) {\n        // Имитировать «рекурсию» с помощью операции помещения в стек\n        stack.Push(i);\n    }\n    // Возврат: вернуть результат\n    while (stack.Count > 0) {\n        // Имитировать «возврат» с помощью операции извлечения из стека\n        res += stack.Pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
        recursion.go
        /* Имитация рекурсии итерацией */\nfunc forLoopRecur(n int) int {\n    // Использовать явный стек для имитации системного стека вызовов\n    stack := list.New()\n    res := 0\n    // Рекурсия: рекурсивный вызов\n    for i := n; i > 0; i-- {\n        // Имитировать «рекурсию» с помощью операции помещения в стек\n        stack.PushBack(i)\n    }\n    // Возврат: вернуть результат\n    for stack.Len() != 0 {\n        // Имитировать «возврат» с помощью операции извлечения из стека\n        res += stack.Back().Value.(int)\n        stack.Remove(stack.Back())\n    }\n    // res = 1+2+3+...+n\n    return res\n}\n
        recursion.swift
        /* Имитация рекурсии итерацией */\nfunc forLoopRecur(n: Int) -> Int {\n    // Использовать явный стек для имитации системного стека вызовов\n    var stack: [Int] = []\n    var res = 0\n    // Рекурсия: рекурсивный вызов\n    for i in (1 ... n).reversed() {\n        // Имитировать «рекурсию» с помощью операции помещения в стек\n        stack.append(i)\n    }\n    // Возврат: вернуть результат\n    while !stack.isEmpty {\n        // Имитировать «возврат» с помощью операции извлечения из стека\n        res += stack.removeLast()\n    }\n    // res = 1+2+3+...+n\n    return res\n}\n
        recursion.js
        /* Имитация рекурсии итерацией */\nfunction forLoopRecur(n) {\n    // Использовать явный стек для имитации системного стека вызовов\n    const stack = [];\n    let res = 0;\n    // Рекурсия: рекурсивный вызов\n    for (let i = n; i > 0; i--) {\n        // Имитировать «рекурсию» с помощью операции помещения в стек\n        stack.push(i);\n    }\n    // Возврат: вернуть результат\n    while (stack.length) {\n        // Имитировать «возврат» с помощью операции извлечения из стека\n        res += stack.pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
        recursion.ts
        /* Имитация рекурсии итерацией */\nfunction forLoopRecur(n: number): number {\n    // Использовать явный стек для имитации системного стека вызовов\n    const stack: number[] = [];\n    let res: number = 0;\n    // Рекурсия: рекурсивный вызов\n    for (let i = n; i > 0; i--) {\n        // Имитировать «рекурсию» с помощью операции помещения в стек\n        stack.push(i);\n    }\n    // Возврат: вернуть результат\n    while (stack.length) {\n        // Имитировать «возврат» с помощью операции извлечения из стека\n        res += stack.pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
        recursion.dart
        /* Имитация рекурсии итерацией */\nint forLoopRecur(int n) {\n  // Использовать явный стек для имитации системного стека вызовов\n  List<int> stack = [];\n  int res = 0;\n  // Рекурсия: рекурсивный вызов\n  for (int i = n; i > 0; i--) {\n    // Имитировать «рекурсию» с помощью операции помещения в стек\n    stack.add(i);\n  }\n  // Возврат: вернуть результат\n  while (!stack.isEmpty) {\n    // Имитировать «возврат» с помощью операции извлечения из стека\n    res += stack.removeLast();\n  }\n  // res = 1+2+3+...+n\n  return res;\n}\n
        recursion.rs
        /* Имитация рекурсии итерацией */\nfn for_loop_recur(n: i32) -> i32 {\n    // Использовать явный стек для имитации системного стека вызовов\n    let mut stack = Vec::new();\n    let mut res = 0;\n    // Рекурсия: рекурсивный вызов\n    for i in (1..=n).rev() {\n        // Имитировать «рекурсию» с помощью операции помещения в стек\n        stack.push(i);\n    }\n    // Возврат: вернуть результат\n    while !stack.is_empty() {\n        // Имитировать «возврат» с помощью операции извлечения из стека\n        res += stack.pop().unwrap();\n    }\n    // res = 1+2+3+...+n\n    res\n}\n
        recursion.c
        /* Имитация рекурсии итерацией */\nint forLoopRecur(int n) {\n    int stack[1000]; // Использовать большой массив для имитации стека\n    int top = -1;    // Индекс вершины стека\n    int res = 0;\n    // Рекурсия: рекурсивный вызов\n    for (int i = n; i > 0; i--) {\n        // Имитировать «рекурсию» с помощью операции помещения в стек\n        stack[1 + top++] = i;\n    }\n    // Возврат: вернуть результат\n    while (top >= 0) {\n        // Имитировать «возврат» с помощью операции извлечения из стека\n        res += stack[top--];\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
        recursion.kt
        /* Имитация рекурсии итерацией */\nfun forLoopRecur(n: Int): Int {\n    // Использовать явный стек для имитации системного стека вызовов\n    val stack = Stack<Int>()\n    var res = 0\n    // Рекурсивный шаг: рекурсивный вызов\n    for (i in n downTo 0) {\n        // Имитировать «рекурсию» с помощью операции помещения в стек\n        stack.push(i)\n    }\n    // Возврат: вернуть результат\n    while (stack.isNotEmpty()) {\n        // Имитировать «возврат» с помощью операции извлечения из стека\n        res += stack.pop()\n    }\n    // res = 1+2+3+...+n\n    return res\n}\n
        recursion.rb
        ### Имитация рекурсии итерацией ###\ndef for_loop_recur(n)\n  # Использовать явный стек для имитации системного стека вызовов\n  stack = []\n  res = 0\n\n  # Рекурсия: рекурсивный вызов\n  for i in n.downto(0)\n    # Имитировать «рекурсию» с помощью операции помещения в стек\n    stack << i\n  end\n  # Возврат: вернуть результат\n  while !stack.empty?\n    res += stack.pop\n  end\n\n  # res = 1+2+3+...+n\n  res\nend\n
        Визуализация кода

        Во весь экран >

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

        • Преобразованный код может стать труднее для понимания и менее читаемым.
        • Для некоторых сложных задач моделирование поведения системного стека вызовов может оказаться очень трудным.

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

        ","path":["Глава 2. Анализ сложности","2.2   Итерация и рекурсия"],"tags":[]},{"location":"chapter_computational_complexity/performance_evaluation/","level":1,"title":"2.1   Оценка эффективности алгоритмов","text":"

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

        1. Найти решение задачи: алгоритм должен надежно находить правильное решение задачи в заданных пределах входных данных.
        2. Найти оптимальное решение: для одной и той же задачи может существовать несколько решений, и мы стремимся найти максимально эффективный алгоритм.

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

        • Временная эффективность: продолжительность выполнения алгоритма.
        • Пространственная эффективность: объем памяти, занимаемой алгоритмом.

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

        Методы оценки эффективности делятся на два типа: практическое тестирование и теоретическую оценку.

        ","path":["Глава 2. Анализ сложности","2.1   Оценка эффективности алгоритмов"],"tags":[]},{"location":"chapter_computational_complexity/performance_evaluation/#211","level":2,"title":"2.1.1   Практическое тестирование","text":"

        Предположим, у нас есть алгоритмы A и B, которые решают одну и ту же задачу, и необходимо сравнить их эффективность. Самый прямой метод - это запустить оба алгоритма на компьютере и зафиксировать время их выполнения и объем используемой памяти. Этот метод отражает реальную ситуацию, но имеет значительные ограничения.

        С одной стороны, сложно исключить влияние факторов тестовой среды. Аппаратная конфигурация влияет на производительность алгоритма. Например, если алгоритм обладает высокой степенью параллелизма, он будет лучше работать на многоядерных CPU; если алгоритм интенсивно использует память, его производительность будет выше на высокопроизводительной памяти. Это означает, что результаты тестирования на разных машинах могут значительно отличаться, а для получения средней эффективности пришлось бы тестировать на различных платформах, что крайне затруднительно.

        С другой стороны, проведение полного тестирования требует значительных ресурсов. С изменением объема входных данных алгоритмы демонстрируют разную эффективность. Например, при небольшом объеме данных алгоритм A может работать быстрее, чем алгоритм B, но при большом объеме данных результат может быть противоположным. Следовательно, для получения убедительных выводов необходимо тестировать различные масштабы входных данных, что требует значительных вычислительных ресурсов.

        ","path":["Глава 2. Анализ сложности","2.1   Оценка эффективности алгоритмов"],"tags":[]},{"location":"chapter_computational_complexity/performance_evaluation/#212","level":2,"title":"2.1.2   Теоретическая оценка","text":"

        Из-за значительных ограничений практического тестирования можно рассмотреть возможность оценки эффективности алгоритмов только с помощью вычислений. Такой метод называется анализом асимптотической сложности (asymptotic complexity analysis), или сокращенно анализом сложности.

        Анализ сложности позволяет отразить зависимость между ресурсами времени и пространства, необходимыми для выполнения алгоритма, и размером входных данных. Он описывает тенденцию роста времени и пространства, необходимых для выполнения алгоритма, по мере увеличения размера входных данных. Это определение может показаться сложным, но его можно разбить на три ключевых момента.

        • \"Ресурсы времени и пространства\" соответствуют временной сложности (time complexity) и пространственной сложности (space complexity).
        • \"По мере увеличения размера входных данных\" означает, что сложность отражает зависимость эффективности алгоритма от объема входных данных.
        • \"Тенденция роста времени и пространства\" указывает, что анализ сложности фокусируется не на конкретных значениях времени выполнения или объема занимаемой памяти, а на скорости их роста.

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

        • Он не требует фактического выполнения кода, что делает его более экологичным и энергосберегающим.
        • Он независим от тестовой среды, а результаты анализа применимы ко всем платформам выполнения.
        • Он может продемонстрировать эффективность алгоритма при различных объемах данных, особенно при больших объемах.

        Tip

        Если понятие сложности пока все еще кажется вам запутанным, не переживайте: мы подробно разберем его в следующих разделах.

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

        Сложность - это математическое понятие, которое новичкам может показаться абстрактным и сложным для изучения. С этой точки зрения анализ сложности не то, с чего стоит начинать изучение алгоритмов. Однако, обсуждая особенности той или иной структуры данных или алгоритма, невозможно избежать анализа их скорости выполнения и использования памяти.

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

        ","path":["Глава 2. Анализ сложности","2.1   Оценка эффективности алгоритмов"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/","level":1,"title":"2.4   Пространственная сложность","text":"

        Пространственная сложность (space complexity) служит для оценки того, как меняется объем памяти, требуемой алгоритму, по мере роста объема данных. Это понятие очень похоже на временную сложность, только вместо времени выполнения рассматривается объем используемой памяти.

        ","path":["Глава 2. Анализ сложности","2.4   Пространственная сложность"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#241","level":2,"title":"2.4.1   Пространство, связанное с алгоритмом","text":"

        Память, которую использует алгоритм во время работы, в основном делится на следующие части.

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

        Как правило, при анализе пространственной сложности в расчет включают временное пространство и выходное пространство.

        Временное пространство можно дополнительно разделить на три части.

        • Временные данные: используются для хранения различных констант, переменных, объектов и т.д., возникающих во время выполнения алгоритма.
        • Пространство кадров стека: используется для хранения контекстных данных вызываемых функций. При каждом вызове функции система создает на вершине стека новый кадр; после возврата функции пространство этого кадра освобождается.
        • Пространство инструкций: используется для хранения скомпилированных инструкций программы и в реальном подсчете обычно не учитывается.

        При анализе пространственной сложности программы обычно учитываются временные данные, пространство стека и выходные данные, как показано на рисунке 2-15.

        Рисунок 2-15   Пространство, используемое алгоритмом

        Ниже приведен соответствующий код:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
        class Node:\n    \"\"\"Класс\"\"\"\n    def __init__(self, x: int):\n        self.val: int = x              # Значение узла\n        self.next: Node | None = None  # Ссылка на следующий узел\n\ndef function() -> int:\n    \"\"\"Функция\"\"\"\n    # Выполнить некоторые операции...\n    return 0\n\ndef algorithm(n) -> int:  # Входные данные\n    A = 0                 # Временные данные (константа, обычно обозначается заглавной буквой)\n    b = 0                 # Временные данные (переменная)\n    node = Node(0)        # Временные данные (объект)\n    c = function()        # Пространство кадра стека (вызов функции)\n    return A + b + c      # Выходные данные\n
        /* Структура */\nstruct Node {\n    int val;\n    Node *next;\n    Node(int x) : val(x), next(nullptr) {}\n};\n\n/* Функция */\nint func() {\n    // Выполнить некоторые операции...\n    return 0;\n}\n\nint algorithm(int n) {        // Входные данные\n    const int a = 0;          // Временные данные (константа)\n    int b = 0;                // Временные данные (переменная)\n    Node* node = new Node(0); // Временные данные (объект)\n    int c = func();           // Пространство кадра стека (вызов функции)\n    return a + b + c;         // Выходные данные\n}\n
        /* Класс */\nclass Node {\n    int val;\n    Node next;\n    Node(int x) { val = x; }\n}\n\n/* Функция */\nint function() {\n    // Выполнить некоторые операции...\n    return 0;\n}\n\nint algorithm(int n) {        // Входные данные\n    final int a = 0;          // Временные данные (константа)\n    int b = 0;                // Временные данные (переменная)\n    Node node = new Node(0);  // Временные данные (объект)\n    int c = function();       // Пространство кадра стека (вызов функции)\n    return a + b + c;         // Выходные данные\n}\n
        /* Класс */\nclass Node(int x) {\n    int val = x;\n    Node next;\n}\n\n/* Функция */\nint Function() {\n    // Выполнить некоторые операции...\n    return 0;\n}\n\nint Algorithm(int n) {        // Входные данные\n    const int a = 0;          // Временные данные (константа)\n    int b = 0;                // Временные данные (переменная)\n    Node node = new(0);       // Временные данные (объект)\n    int c = Function();       // Пространство кадра стека (вызов функции)\n    return a + b + c;         // Выходные данные\n}\n
        /* Структура */\ntype node struct {\n    val  int\n    next *node\n}\n\n/* Создать структуру node */\nfunc newNode(val int) *node {\n    return &node{val: val}\n}\n\n/* Функция */\nfunc function() int {\n    // Выполнить некоторые операции...\n    return 0\n}\n\nfunc algorithm(n int) int { // Входные данные\n    const a = 0             // Временные данные (константа)\n    b := 0                  // Временные данные (переменная)\n    newNode(0)              // Временные данные (объект)\n    c := function()         // Пространство кадра стека (вызов функции)\n    return a + b + c        // Выходные данные\n}\n
        /* Класс */\nclass Node {\n    var val: Int\n    var next: Node?\n\n    init(x: Int) {\n        val = x\n    }\n}\n\n/* Функция */\nfunc function() -> Int {\n    // Выполнить некоторые операции...\n    return 0\n}\n\nfunc algorithm(n: Int) -> Int { // Входные данные\n    let a = 0             // Временные данные (константа)\n    var b = 0             // Временные данные (переменная)\n    let node = Node(x: 0) // Временные данные (объект)\n    let c = function()    // Пространство кадра стека (вызов функции)\n    return a + b + c      // Выходные данные\n}\n
        /* Класс */\nclass Node {\n    val;\n    next;\n    constructor(val) {\n        this.val = val === undefined ? 0 : val; // Значение узла\n        this.next = null;                       // Ссылка на следующий узел\n    }\n}\n\n/* Функция */\nfunction constFunc() {\n    // Выполнить некоторые операции\n    return 0;\n}\n\nfunction algorithm(n) {       // Входные данные\n    const a = 0;              // Временные данные (константа)\n    let b = 0;                // Временные данные (переменная)\n    const node = new Node(0); // Временные данные (объект)\n    const c = constFunc();    // Пространство кадра стека (вызов функции)\n    return a + b + c;         // Выходные данные\n}\n
        /* Класс */\nclass Node {\n    val: number;\n    next: Node | null;\n    constructor(val?: number) {\n        this.val = val === undefined ? 0 : val; // Значение узла\n        this.next = null;                       // Ссылка на следующий узел\n    }\n}\n\n/* Функция */\nfunction constFunc(): number {\n    // Выполнить некоторые операции\n    return 0;\n}\n\nfunction algorithm(n: number): number { // Входные данные\n    const a = 0;                        // Временные данные (константа)\n    let b = 0;                          // Временные данные (переменная)\n    const node = new Node(0);           // Временные данные (объект)\n    const c = constFunc();              // Пространство кадра стека (вызов функции)\n    return a + b + c;                   // Выходные данные\n}\n
        /* Класс */\nclass Node {\n  int val;\n  Node next;\n  Node(this.val, [this.next]);\n}\n\n/* Функция */\nint function() {\n  // Выполнить некоторые операции...\n  return 0;\n}\n\nint algorithm(int n) {  // Входные данные\n  const int a = 0;      // Временные данные (константа)\n  int b = 0;            // Временные данные (переменная)\n  Node node = Node(0);  // Временные данные (объект)\n  int c = function();   // Пространство кадра стека (вызов функции)\n  return a + b + c;     // Выходные данные\n}\n
        use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* Структура */\nstruct Node {\n    val: i32,\n    next: Option<Rc<RefCell<Node>>>,\n}\n\n/* Создать структуру Node */\nimpl Node {\n    fn new(val: i32) -> Self {\n        Self { val: val, next: None }\n    }\n}\n\n/* Функция */\nfn function() -> i32 {      \n    // Выполнить некоторые операции...\n    return 0;\n}\n\nfn algorithm(n: i32) -> i32 {       // Входные данные\n    const a: i32 = 0;               // Временные данные (константа)\n    let mut b = 0;                  // Временные данные (переменная)\n    let node = Node::new(0);        // Временные данные (объект)\n    let c = function();             // Пространство кадра стека (вызов функции)\n    return a + b + c;               // Выходные данные\n}\n
        /* Функция */\nint func() {\n    // Выполнить некоторые операции...\n    return 0;\n}\n\nint algorithm(int n) { // Входные данные\n    const int a = 0;   // Временные данные (константа)\n    int b = 0;         // Временные данные (переменная)\n    int c = func();    // Пространство кадра стека (вызов функции)\n    return a + b + c;  // Выходные данные\n}\n
        /* Класс */\nclass Node(var _val: Int) {\n    var next: Node? = null\n}\n\n/* Функция */\nfun function(): Int {\n    // Выполнить некоторые операции...\n    return 0\n}\n\nfun algorithm(n: Int): Int { // Входные данные\n    val a = 0                // Временные данные (константа)\n    var b = 0                // Временные данные (переменная)\n    val node = Node(0)       // Временные данные (объект)\n    val c = function()       // Пространство кадра стека (вызов функции)\n    return a + b + c         // Выходные данные\n}\n
        ### Класс ###\nclass Node\n    attr_accessor :val      # Значение узла\n    attr_accessor :next     # Ссылка на следующий узел\n\n    def initialize(x)\n        @val = x\n    end\nend\n\n### Функция ###\ndef function\n    # Выполнить некоторые операции...\n    0\nend\n\n### Алгоритм ###\ndef algorithm(n)        # Входные данные\n    a = 0               # Временные данные (константа)\n    b = 0               # Временные данные (переменная)\n    node = Node.new(0)  # Временные данные (объект)\n    c = function        # Пространство кадра стека (вызов функции)\n    a + b + c           # Выходные данные\nend\n
        ","path":["Глава 2. Анализ сложности","2.4   Пространственная сложность"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#242","level":2,"title":"2.4.2   Метод вывода","text":"

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

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

        Рассмотрим следующий код. Понятие худшей пространственной сложности здесь имеет два значения.

        1. Ориентир на худшие входные данные: когда \\(n < 10\\) , пространственная сложность равна \\(O(1)\\) ; но когда \\(n > 10\\) , инициализированный массив nums занимает \\(O(n)\\) пространства, поэтому худшая пространственная сложность равна \\(O(n)\\) .
        2. Ориентир на пиковое использование памяти во время выполнения: например, до выполнения последней строки программа занимает \\(O(1)\\) пространства; при инициализации массива nums она занимает \\(O(n)\\) пространства, поэтому худшая пространственная сложность также равна \\(O(n)\\) .
        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
        def algorithm(n: int):\n    a = 0               # O(1)\n    b = [0] * 10000     # O(1)\n    if n > 10:\n        nums = [0] * n  # O(n)\n
        void algorithm(int n) {\n    int a = 0;               // O(1)\n    vector<int> b(10000);    // O(1)\n    if (n > 10)\n        vector<int> nums(n); // O(n)\n}\n
        void algorithm(int n) {\n    int a = 0;                   // O(1)\n    int[] b = new int[10000];    // O(1)\n    if (n > 10)\n        int[] nums = new int[n]; // O(n)\n}\n
        void Algorithm(int n) {\n    int a = 0;                   // O(1)\n    int[] b = new int[10000];    // O(1)\n    if (n > 10) {\n        int[] nums = new int[n]; // O(n)\n    }\n}\n
        func algorithm(n int) {\n    a := 0                      // O(1)\n    b := make([]int, 10000)     // O(1)\n    var nums []int\n    if n > 10 {\n        nums := make([]int, n)  // O(n)\n    }\n    fmt.Println(a, b, nums)\n}\n
        func algorithm(n: Int) {\n    let a = 0 // O(1)\n    let b = Array(repeating: 0, count: 10000) // O(1)\n    if n > 10 {\n        let nums = Array(repeating: 0, count: n) // O(n)\n    }\n}\n
        function algorithm(n) {\n    const a = 0;                   // O(1)\n    const b = new Array(10000);    // O(1)\n    if (n > 10) {\n        const nums = new Array(n); // O(n)\n    }\n}\n
        function algorithm(n: number): void {\n    const a = 0;                   // O(1)\n    const b = new Array(10000);    // O(1)\n    if (n > 10) {\n        const nums = new Array(n); // O(n)\n    }\n}\n
        void algorithm(int n) {\n  int a = 0;                            // O(1)\n  List<int> b = List.filled(10000, 0);  // O(1)\n  if (n > 10) {\n    List<int> nums = List.filled(n, 0); // O(n)\n  }\n}\n
        fn algorithm(n: i32) {\n    let a = 0;                              // O(1)\n    let b = [0; 10000];                     // O(1)\n    if n > 10 {\n        let nums = vec![0; n as usize];     // O(n)\n    }\n}\n
        void algorithm(int n) {\n    int a = 0;               // O(1)\n    int b[10000];            // O(1)\n    if (n > 10)\n        int nums[n] = {0};   // O(n)\n}\n
        fun algorithm(n: Int) {\n    val a = 0                    // O(1)\n    val b = IntArray(10000)      // O(1)\n    if (n > 10) {\n        val nums = IntArray(n)   // O(n)\n    }\n}\n
        def algorithm(n)\n    a = 0                           # O(1)\n    b = Array.new(10000)            # O(1)\n    nums = Array.new(n) if n > 10   # O(n)\nend\n

        В рекурсивных функциях необходимо учитывать пространство кадров стека. Рассмотрим следующий код:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
        def function() -> int:\n    # Выполнить некоторые операции\n    return 0\n\ndef loop(n: int):\n    \"\"\"Пространственная сложность цикла равна O(1)\"\"\"\n    for _ in range(n):\n        function()\n\ndef recur(n: int):\n    \"\"\"Пространственная сложность рекурсии равна O(n)\"\"\"\n    if n == 1:\n        return\n    return recur(n - 1)\n
        int func() {\n    // Выполнить некоторые операции\n    return 0;\n}\n/* Пространственная сложность цикла равна O(1) */\nvoid loop(int n) {\n    for (int i = 0; i < n; i++) {\n        func();\n    }\n}\n/* Пространственная сложность рекурсии равна O(n) */\nvoid recur(int n) {\n    if (n == 1) return;\n    recur(n - 1);\n}\n
        int function() {\n    // Выполнить некоторые операции\n    return 0;\n}\n/* Пространственная сложность цикла равна O(1) */\nvoid loop(int n) {\n    for (int i = 0; i < n; i++) {\n        function();\n    }\n}\n/* Пространственная сложность рекурсии равна O(n) */\nvoid recur(int n) {\n    if (n == 1) return;\n    recur(n - 1);\n}\n
        int Function() {\n    // Выполнить некоторые операции\n    return 0;\n}\n/* Пространственная сложность цикла равна O(1) */\nvoid Loop(int n) {\n    for (int i = 0; i < n; i++) {\n        Function();\n    }\n}\n/* Пространственная сложность рекурсии равна O(n) */\nint Recur(int n) {\n    if (n == 1) return 1;\n    return Recur(n - 1);\n}\n
        func function() int {\n    // Выполнить некоторые операции\n    return 0\n}\n\n/* Пространственная сложность цикла равна O(1) */\nfunc loop(n int) {\n    for i := 0; i < n; i++ {\n        function()\n    }\n}\n\n/* Пространственная сложность рекурсии равна O(n) */\nfunc recur(n int) {\n    if n == 1 {\n        return\n    }\n    recur(n - 1)\n}\n
        @discardableResult\nfunc function() -> Int {\n    // Выполнить некоторые операции\n    return 0\n}\n\n/* Пространственная сложность цикла равна O(1) */\nfunc loop(n: Int) {\n    for _ in 0 ..< n {\n        function()\n    }\n}\n\n/* Пространственная сложность рекурсии равна O(n) */\nfunc recur(n: Int) {\n    if n == 1 {\n        return\n    }\n    recur(n: n - 1)\n}\n
        function constFunc() {\n    // Выполнить некоторые операции\n    return 0;\n}\n/* Пространственная сложность цикла равна O(1) */\nfunction loop(n) {\n    for (let i = 0; i < n; i++) {\n        constFunc();\n    }\n}\n/* Пространственная сложность рекурсии равна O(n) */\nfunction recur(n) {\n    if (n === 1) return;\n    return recur(n - 1);\n}\n
        function constFunc(): number {\n    // Выполнить некоторые операции\n    return 0;\n}\n/* Пространственная сложность цикла равна O(1) */\nfunction loop(n: number): void {\n    for (let i = 0; i < n; i++) {\n        constFunc();\n    }\n}\n/* Пространственная сложность рекурсии равна O(n) */\nfunction recur(n: number): void {\n    if (n === 1) return;\n    return recur(n - 1);\n}\n
        int function() {\n  // Выполнить некоторые операции\n  return 0;\n}\n/* Пространственная сложность цикла равна O(1) */\nvoid loop(int n) {\n  for (int i = 0; i < n; i++) {\n    function();\n  }\n}\n/* Пространственная сложность рекурсии равна O(n) */\nvoid recur(int n) {\n  if (n == 1) return;\n  recur(n - 1);\n}\n
        fn function() -> i32 {\n    // Выполнить некоторые операции\n    return 0;\n}\n/* Пространственная сложность цикла равна O(1) */\nfn loop(n: i32) {\n    for i in 0..n {\n        function();\n    }\n}\n/* Пространственная сложность рекурсии равна O(n) */\nfn recur(n: i32) {\n    if n == 1 {\n        return;\n    }\n    recur(n - 1);\n}\n
        int func() {\n    // Выполнить некоторые операции\n    return 0;\n}\n/* Пространственная сложность цикла равна O(1) */\nvoid loop(int n) {\n    for (int i = 0; i < n; i++) {\n        func();\n    }\n}\n/* Пространственная сложность рекурсии равна O(n) */\nvoid recur(int n) {\n    if (n == 1) return;\n    recur(n - 1);\n}\n
        fun function(): Int {\n    // Выполнить некоторые операции\n    return 0\n}\n/* Пространственная сложность цикла равна O(1) */\nfun loop(n: Int) {\n    for (i in 0..<n) {\n        function()\n    }\n}\n/* Пространственная сложность рекурсии равна O(n) */\nfun recur(n: Int) {\n    if (n == 1) return\n    return recur(n - 1)\n}\n
        def function\n    # Выполнить некоторые операции\n    0\nend\n\n### Пространственная сложность цикла равна O(1) ###\ndef loop(n)\n    (0...n).each { function }\nend\n\n### Пространственная сложность рекурсии равна O(n) ###\ndef recur(n)\n    return if n == 1\n    recur(n - 1)\nend\n

        Функции loop() и recur() имеют временную сложность \\(O(n)\\) , но их пространственная сложность различается.

        • Функция loop() вызывает function() в цикле \\(n\\) раз; на каждой итерации function() возвращается и освобождает пространство своего кадра стека, поэтому пространственная сложность по-прежнему равна \\(O(1)\\) .
        • Рекурсивная функция recur() во время выполнения одновременно содержит \\(n\\) еще не завершившихся экземпляров recur() , поэтому занимает \\(O(n)\\) пространства кадров стека.
        ","path":["Глава 2. Анализ сложности","2.4   Пространственная сложность"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#243","level":2,"title":"2.4.3   Распространенные типы","text":"

        Пусть размер входных данных равен \\(n\\) . На рисунке 2-16 показаны распространенные типы пространственной сложности в порядке от меньшей к большей.

        \\[ \\begin{aligned} & O(1) < O(\\log n) < O(n) < O(n^2) < O(2^n) \\newline & \\text{Постоянная} < \\text{Логарифмическая} < \\text{Линейная} < \\text{Квадратичная} < \\text{Экспоненциальная} \\end{aligned} \\]

        Рисунок 2-16   Распространенные типы пространственной сложности

        ","path":["Глава 2. Анализ сложности","2.4   Пространственная сложность"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#1-o1","level":3,"title":"1.   Постоянная сложность \\(O(1)\\)","text":"

        Постоянная сложность обычно встречается у констант, переменных и объектов, количество которых не зависит от размера входных данных \\(n\\) .

        Следует заметить, что память, занятая инициализацией переменных или вызовом функций внутри цикла, освобождается при переходе к следующей итерации, поэтому она не накапливается, и пространственная сложность по-прежнему остается \\(O(1)\\) :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
        def function() -> int:\n    \"\"\"Функция\"\"\"\n    # Выполнить некоторые операции\n    return 0\n\ndef constant(n: int):\n    \"\"\"Постоянная сложность\"\"\"\n    # Константы, переменные и объекты занимают O(1) памяти\n    a = 0\n    nums = [0] * 10000\n    node = ListNode(0)\n    # Переменные в цикле занимают O(1) памяти\n    for _ in range(n):\n        c = 0\n    # Функции в цикле занимают O(1) памяти\n    for _ in range(n):\n        function()\n
        space_complexity.cpp
        /* Функция */\nint func() {\n    // Выполнить некоторые операции\n    return 0;\n}\n\n/* Постоянная сложность */\nvoid constant(int n) {\n    // Константы, переменные и объекты занимают O(1) памяти\n    const int a = 0;\n    int b = 0;\n    vector<int> nums(10000);\n    ListNode node(0);\n    // Переменные в цикле занимают O(1) памяти\n    for (int i = 0; i < n; i++) {\n        int c = 0;\n    }\n    // Функции в цикле занимают O(1) памяти\n    for (int i = 0; i < n; i++) {\n        func();\n    }\n}\n
        space_complexity.java
        /* Функция */\nint function() {\n    // Выполнить некоторые операции\n    return 0;\n}\n\n/* Постоянная сложность */\nvoid constant(int n) {\n    // Константы, переменные и объекты занимают O(1) памяти\n    final int a = 0;\n    int b = 0;\n    int[] nums = new int[10000];\n    ListNode node = new ListNode(0);\n    // Переменные в цикле занимают O(1) памяти\n    for (int i = 0; i < n; i++) {\n        int c = 0;\n    }\n    // Функции в цикле занимают O(1) памяти\n    for (int i = 0; i < n; i++) {\n        function();\n    }\n}\n
        space_complexity.cs
        /* Функция */\nint Function() {\n    // Выполнить некоторые операции\n    return 0;\n}\n\n/* Постоянная сложность */\nvoid Constant(int n) {\n    // Константы, переменные и объекты занимают O(1) памяти\n    int a = 0;\n    int b = 0;\n    int[] nums = new int[10000];\n    ListNode node = new(0);\n    // Переменные в цикле занимают O(1) памяти\n    for (int i = 0; i < n; i++) {\n        int c = 0;\n    }\n    // Функции в цикле занимают O(1) памяти\n    for (int i = 0; i < n; i++) {\n        Function();\n    }\n}\n
        space_complexity.go
        /* Функция */\nfunc function() int {\n    // Выполнить некоторые операции...\n    return 0\n}\n\n/* Постоянная сложность */\nfunc spaceConstant(n int) {\n    // Константы, переменные и объекты занимают O(1) памяти\n    const a = 0\n    b := 0\n    nums := make([]int, 10000)\n    node := newNode(0)\n    // Переменные в цикле занимают O(1) памяти\n    var c int\n    for i := 0; i < n; i++ {\n        c = 0\n    }\n    // Функции в цикле занимают O(1) памяти\n    for i := 0; i < n; i++ {\n        function()\n    }\n    b += 0\n    c += 0\n    nums[0] = 0\n    node.val = 0\n}\n
        space_complexity.swift
        /* Функция */\n@discardableResult\nfunc function() -> Int {\n    // Выполнить некоторые операции\n    return 0\n}\n\n/* Постоянная сложность */\nfunc constant(n: Int) {\n    // Константы, переменные и объекты занимают O(1) памяти\n    let a = 0\n    var b = 0\n    let nums = Array(repeating: 0, count: 10000)\n    let node = ListNode(x: 0)\n    // Переменные в цикле занимают O(1) памяти\n    for _ in 0 ..< n {\n        let c = 0\n    }\n    // Функции в цикле занимают O(1) памяти\n    for _ in 0 ..< n {\n        function()\n    }\n}\n
        space_complexity.js
        /* Функция */\nfunction constFunc() {\n    // Выполнить некоторые операции\n    return 0;\n}\n\n/* Постоянная сложность */\nfunction constant(n) {\n    // Константы, переменные и объекты занимают O(1) памяти\n    const a = 0;\n    const b = 0;\n    const nums = new Array(10000);\n    const node = new ListNode(0);\n    // Переменные в цикле занимают O(1) памяти\n    for (let i = 0; i < n; i++) {\n        const c = 0;\n    }\n    // Функции в цикле занимают O(1) памяти\n    for (let i = 0; i < n; i++) {\n        constFunc();\n    }\n}\n
        space_complexity.ts
        /* Функция */\nfunction constFunc(): number {\n    // Выполнить некоторые операции\n    return 0;\n}\n\n/* Постоянная сложность */\nfunction constant(n: number): void {\n    // Константы, переменные и объекты занимают O(1) памяти\n    const a = 0;\n    const b = 0;\n    const nums = new Array(10000);\n    const node = new ListNode(0);\n    // Переменные в цикле занимают O(1) памяти\n    for (let i = 0; i < n; i++) {\n        const c = 0;\n    }\n    // Функции в цикле занимают O(1) памяти\n    for (let i = 0; i < n; i++) {\n        constFunc();\n    }\n}\n
        space_complexity.dart
        /* Функция */\nint function() {\n  // Выполнить некоторые операции\n  return 0;\n}\n\n/* Постоянная сложность */\nvoid constant(int n) {\n  // Константы, переменные и объекты занимают O(1) памяти\n  final int a = 0;\n  int b = 0;\n  List<int> nums = List.filled(10000, 0);\n  ListNode node = ListNode(0);\n  // Переменные в цикле занимают O(1) памяти\n  for (var i = 0; i < n; i++) {\n    int c = 0;\n  }\n  // Функции в цикле занимают O(1) памяти\n  for (var i = 0; i < n; i++) {\n    function();\n  }\n}\n
        space_complexity.rs
        /* Функция */\nfn function() -> i32 {\n    // Выполнить некоторые операции\n    return 0;\n}\n\n/* Постоянная сложность */\n#[allow(unused)]\nfn constant(n: i32) {\n    // Константы, переменные и объекты занимают O(1) памяти\n    const A: i32 = 0;\n    let b = 0;\n    let nums = vec![0; 10000];\n    let node = ListNode::new(0);\n    // Переменные в цикле занимают O(1) памяти\n    for i in 0..n {\n        let c = 0;\n    }\n    // Функции в цикле занимают O(1) памяти\n    for i in 0..n {\n        function();\n    }\n}\n
        space_complexity.c
        /* Функция */\nint func() {\n    // Выполнить некоторые операции\n    return 0;\n}\n\n/* Постоянная сложность */\nvoid constant(int n) {\n    // Константы, переменные и объекты занимают O(1) памяти\n    const int a = 0;\n    int b = 0;\n    int nums[1000];\n    ListNode *node = newListNode(0);\n    free(node);\n    // Переменные в цикле занимают O(1) памяти\n    for (int i = 0; i < n; i++) {\n        int c = 0;\n    }\n    // Функции в цикле занимают O(1) памяти\n    for (int i = 0; i < n; i++) {\n        func();\n    }\n}\n
        space_complexity.kt
        /* Функция */\nfun function(): Int {\n    // Выполнить некоторые операции\n    return 0\n}\n\n/* Постоянная сложность */\nfun constant(n: Int) {\n    // Константы, переменные и объекты занимают O(1) памяти\n    val a = 0\n    var b = 0\n    val nums = Array(10000) { 0 }\n    val node = ListNode(0)\n    // Переменные в цикле занимают O(1) памяти\n    for (i in 0..<n) {\n        val c = 0\n    }\n    // Функции в цикле занимают O(1) памяти\n    for (i in 0..<n) {\n        function()\n    }\n}\n
        space_complexity.rb
        ### Функция ###\ndef function\n  # Выполнить некоторые операции\n  0\nend\n\n### Постоянная сложность ###\ndef constant(n)\n  # Константы, переменные и объекты занимают O(1) памяти\n  a = 0\n  nums = [0] * 10000\n  node = ListNode.new\n\n  # Переменные в цикле занимают O(1) памяти\n  (0...n).each { c = 0 }\n  # Функции в цикле занимают O(1) памяти\n  (0...n).each { function }\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 2. Анализ сложности","2.4   Пространственная сложность"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#2-on","level":3,"title":"2.   Линейная сложность \\(O(n)\\)","text":"

        Линейная сложность часто встречается у массивов, списков, стеков, очередей и других структур, число элементов в которых пропорционально \\(n\\) :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
        def linear(n: int):\n    \"\"\"Линейная сложность\"\"\"\n    # Список длины n занимает O(n) памяти\n    nums = [0] * n\n    # Хеш-таблица длины n занимает O(n) памяти\n    hmap = dict[int, str]()\n    for i in range(n):\n        hmap[i] = str(i)\n
        space_complexity.cpp
        /* Линейная сложность */\nvoid linear(int n) {\n    // Массив длины n занимает O(n) памяти\n    vector<int> nums(n);\n    // Список длины n занимает O(n) памяти\n    vector<ListNode> nodes;\n    for (int i = 0; i < n; i++) {\n        nodes.push_back(ListNode(i));\n    }\n    // Хеш-таблица длины n занимает O(n) памяти\n    unordered_map<int, string> map;\n    for (int i = 0; i < n; i++) {\n        map[i] = to_string(i);\n    }\n}\n
        space_complexity.java
        /* Линейная сложность */\nvoid linear(int n) {\n    // Массив длины n занимает O(n) памяти\n    int[] nums = new int[n];\n    // Список длины n занимает O(n) памяти\n    List<ListNode> nodes = new ArrayList<>();\n    for (int i = 0; i < n; i++) {\n        nodes.add(new ListNode(i));\n    }\n    // Хеш-таблица длины n занимает O(n) памяти\n    Map<Integer, String> map = new HashMap<>();\n    for (int i = 0; i < n; i++) {\n        map.put(i, String.valueOf(i));\n    }\n}\n
        space_complexity.cs
        /* Линейная сложность */\nvoid Linear(int n) {\n    // Массив длины n занимает O(n) памяти\n    int[] nums = new int[n];\n    // Список длины n занимает O(n) памяти\n    List<ListNode> nodes = [];\n    for (int i = 0; i < n; i++) {\n        nodes.Add(new ListNode(i));\n    }\n    // Хеш-таблица длины n занимает O(n) памяти\n    Dictionary<int, string> map = [];\n    for (int i = 0; i < n; i++) {\n        map.Add(i, i.ToString());\n    }\n}\n
        space_complexity.go
        /* Линейная сложность */\nfunc spaceLinear(n int) {\n    // Массив длины n занимает O(n) памяти\n    _ = make([]int, n)\n    // Список длины n занимает O(n) памяти\n    var nodes []*node\n    for i := 0; i < n; i++ {\n        nodes = append(nodes, newNode(i))\n    }\n    // Хеш-таблица длины n занимает O(n) памяти\n    m := make(map[int]string, n)\n    for i := 0; i < n; i++ {\n        m[i] = strconv.Itoa(i)\n    }\n}\n
        space_complexity.swift
        /* Линейная сложность */\nfunc linear(n: Int) {\n    // Массив длины n занимает O(n) памяти\n    let nums = Array(repeating: 0, count: n)\n    // Список длины n занимает O(n) памяти\n    let nodes = (0 ..< n).map { ListNode(x: $0) }\n    // Хеш-таблица длины n занимает O(n) памяти\n    let map = Dictionary(uniqueKeysWithValues: (0 ..< n).map { ($0, \"\\($0)\") })\n}\n
        space_complexity.js
        /* Линейная сложность */\nfunction linear(n) {\n    // Массив длины n занимает O(n) памяти\n    const nums = new Array(n);\n    // Список длины n занимает O(n) памяти\n    const nodes = [];\n    for (let i = 0; i < n; i++) {\n        nodes.push(new ListNode(i));\n    }\n    // Хеш-таблица длины n занимает O(n) памяти\n    const map = new Map();\n    for (let i = 0; i < n; i++) {\n        map.set(i, i.toString());\n    }\n}\n
        space_complexity.ts
        /* Линейная сложность */\nfunction linear(n: number): void {\n    // Массив длины n занимает O(n) памяти\n    const nums = new Array(n);\n    // Список длины n занимает O(n) памяти\n    const nodes: ListNode[] = [];\n    for (let i = 0; i < n; i++) {\n        nodes.push(new ListNode(i));\n    }\n    // Хеш-таблица длины n занимает O(n) памяти\n    const map = new Map();\n    for (let i = 0; i < n; i++) {\n        map.set(i, i.toString());\n    }\n}\n
        space_complexity.dart
        /* Линейная сложность */\nvoid linear(int n) {\n  // Массив длины n занимает O(n) памяти\n  List<int> nums = List.filled(n, 0);\n  // Список длины n занимает O(n) памяти\n  List<ListNode> nodes = [];\n  for (var i = 0; i < n; i++) {\n    nodes.add(ListNode(i));\n  }\n  // Хеш-таблица длины n занимает O(n) памяти\n  Map<int, String> map = HashMap();\n  for (var i = 0; i < n; i++) {\n    map.putIfAbsent(i, () => i.toString());\n  }\n}\n
        space_complexity.rs
        /* Линейная сложность */\n#[allow(unused)]\nfn linear(n: i32) {\n    // Массив длины n занимает O(n) памяти\n    let mut nums = vec![0; n as usize];\n    // Список длины n занимает O(n) памяти\n    let mut nodes = Vec::new();\n    for i in 0..n {\n        nodes.push(ListNode::new(i))\n    }\n    // Хеш-таблица длины n занимает O(n) памяти\n    let mut map = HashMap::new();\n    for i in 0..n {\n        map.insert(i, i.to_string());\n    }\n}\n
        space_complexity.c
        /* Хеш-таблица */\ntypedef struct {\n    int key;\n    int val;\n    UT_hash_handle hh; // Реализовано на основе uthash.h\n} HashTable;\n\n/* Линейная сложность */\nvoid linear(int n) {\n    // Массив длины n занимает O(n) памяти\n    int *nums = malloc(sizeof(int) * n);\n    free(nums);\n\n    // Список длины n занимает O(n) памяти\n    ListNode **nodes = malloc(sizeof(ListNode *) * n);\n    for (int i = 0; i < n; i++) {\n        nodes[i] = newListNode(i);\n    }\n    // Освобождение памяти\n    for (int i = 0; i < n; i++) {\n        free(nodes[i]);\n    }\n    free(nodes);\n\n    // Хеш-таблица длины n занимает O(n) памяти\n    HashTable *h = NULL;\n    for (int i = 0; i < n; i++) {\n        HashTable *tmp = malloc(sizeof(HashTable));\n        tmp->key = i;\n        tmp->val = i;\n        HASH_ADD_INT(h, key, tmp);\n    }\n\n    // Освобождение памяти\n    HashTable *curr, *tmp;\n    HASH_ITER(hh, h, curr, tmp) {\n        HASH_DEL(h, curr);\n        free(curr);\n    }\n}\n
        space_complexity.kt
        /* Линейная сложность */\nfun linear(n: Int) {\n    // Массив длины n занимает O(n) памяти\n    val nums = Array(n) { 0 }\n    // Список длины n занимает O(n) памяти\n    val nodes = mutableListOf<ListNode>()\n    for (i in 0..<n) {\n        nodes.add(ListNode(i))\n    }\n    // Хеш-таблица длины n занимает O(n) памяти\n    val map = mutableMapOf<Int, String>()\n    for (i in 0..<n) {\n        map[i] = i.toString()\n    }\n}\n
        space_complexity.rb
        ### Линейная сложность ###\ndef linear(n)\n  # Список длины n занимает O(n) памяти\n  nums = Array.new(n, 0)\n\n  # Хеш-таблица длины n занимает O(n) памяти\n  hmap = {}\n  for i in 0...n\n    hmap[i] = i.to_s\n  end\nend\n
        Визуализация кода

        Во весь экран >

        Как показано на рисунке 2-17, глубина рекурсии этой функции равна \\(n\\) , то есть одновременно существует \\(n\\) еще не завершившихся функций linear_recur() , которые используют \\(O(n)\\) пространства кадров стека:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
        def linear_recur(n: int):\n    \"\"\"Линейная сложность (рекурсивная реализация)\"\"\"\n    print(\"Рекурсия n =\", n)\n    if n == 1:\n        return\n    linear_recur(n - 1)\n
        space_complexity.cpp
        /* Линейная сложность (рекурсивная реализация) */\nvoid linearRecur(int n) {\n    cout << \"Рекурсия n = \" << n << endl;\n    if (n == 1)\n        return;\n    linearRecur(n - 1);\n}\n
        space_complexity.java
        /* Линейная сложность (рекурсивная реализация) */\nvoid linearRecur(int n) {\n    System.out.println(\"Рекурсия n = \" + n);\n    if (n == 1)\n        return;\n    linearRecur(n - 1);\n}\n
        space_complexity.cs
        /* Линейная сложность (рекурсивная реализация) */\nvoid LinearRecur(int n) {\n    Console.WriteLine(\"Рекурсия n = \" + n);\n    if (n == 1) return;\n    LinearRecur(n - 1);\n}\n
        space_complexity.go
        /* Линейная сложность (рекурсивная реализация) */\nfunc spaceLinearRecur(n int) {\n    fmt.Println(\"Рекурсия n =\", n)\n    if n == 1 {\n        return\n    }\n    spaceLinearRecur(n - 1)\n}\n
        space_complexity.swift
        /* Линейная сложность (рекурсивная реализация) */\nfunc linearRecur(n: Int) {\n    print(\"Рекурсия n = \\(n)\")\n    if n == 1 {\n        return\n    }\n    linearRecur(n: n - 1)\n}\n
        space_complexity.js
        /* Линейная сложность (рекурсивная реализация) */\nfunction linearRecur(n) {\n    console.log(`Рекурсия n = ${n}`);\n    if (n === 1) return;\n    linearRecur(n - 1);\n}\n
        space_complexity.ts
        /* Линейная сложность (рекурсивная реализация) */\nfunction linearRecur(n: number): void {\n    console.log(`Рекурсия n = ${n}`);\n    if (n === 1) return;\n    linearRecur(n - 1);\n}\n
        space_complexity.dart
        /* Линейная сложность (рекурсивная реализация) */\nvoid linearRecur(int n) {\n  print('Рекурсия n = $n');\n  if (n == 1) return;\n  linearRecur(n - 1);\n}\n
        space_complexity.rs
        /* Линейная сложность (рекурсивная реализация) */\nfn linear_recur(n: i32) {\n    println!(\"Рекурсия n = {}\", n);\n    if n == 1 {\n        return;\n    };\n    linear_recur(n - 1);\n}\n
        space_complexity.c
        /* Линейная сложность (рекурсивная реализация) */\nvoid linearRecur(int n) {\n    printf(\"Рекурсия n = %d\\r\\n\", n);\n    if (n == 1)\n        return;\n    linearRecur(n - 1);\n}\n
        space_complexity.kt
        /* Линейная сложность (рекурсивная реализация) */\nfun linearRecur(n: Int) {\n    println(\"Рекурсия n = $n\")\n    if (n == 1)\n        return\n    linearRecur(n - 1)\n}\n
        space_complexity.rb
        ### Линейная сложность ###\ndef linear(n)\n  # Список длины n занимает O(n) памяти\n  nums = Array.new(n, 0)\n\n  # Хеш-таблица длины n занимает O(n) памяти\n  hmap = {}\n  for i in 0...n\n    hmap[i] = i.to_s\n  end\nend\n\n# ## Линейная сложность (рекурсивная реализация) ###\ndef linear_recur(n)\n  puts \"Рекурсия n = #{n}\"\n  return if n == 1\n  linear_recur(n - 1)\nend\n
        Визуализация кода

        Во весь экран >

        Рисунок 2-17   Линейная пространственная сложность, порождаемая рекурсивной функцией

        ","path":["Глава 2. Анализ сложности","2.4   Пространственная сложность"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#3-on2","level":3,"title":"3.   Квадратичная сложность \\(O(n^2)\\)","text":"

        Квадратичная сложность часто встречается у матриц и графов, где число элементов связано с \\(n\\) квадратичной зависимостью:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
        def quadratic(n: int):\n    \"\"\"Квадратичная сложность\"\"\"\n    # Двумерный список занимает O(n^2) памяти\n    num_matrix = [[0] * n for _ in range(n)]\n
        space_complexity.cpp
        /* Квадратичная сложность */\nvoid quadratic(int n) {\n    // Двумерный список занимает O(n^2) памяти\n    vector<vector<int>> numMatrix;\n    for (int i = 0; i < n; i++) {\n        vector<int> tmp;\n        for (int j = 0; j < n; j++) {\n            tmp.push_back(0);\n        }\n        numMatrix.push_back(tmp);\n    }\n}\n
        space_complexity.java
        /* Квадратичная сложность */\nvoid quadratic(int n) {\n    // Матрица занимает O(n^2) памяти\n    int[][] numMatrix = new int[n][n];\n    // Двумерный список занимает O(n^2) памяти\n    List<List<Integer>> numList = new ArrayList<>();\n    for (int i = 0; i < n; i++) {\n        List<Integer> tmp = new ArrayList<>();\n        for (int j = 0; j < n; j++) {\n            tmp.add(0);\n        }\n        numList.add(tmp);\n    }\n}\n
        space_complexity.cs
        /* Квадратичная сложность */\nvoid Quadratic(int n) {\n    // Матрица занимает O(n^2) памяти\n    int[,] numMatrix = new int[n, n];\n    // Двумерный список занимает O(n^2) памяти\n    List<List<int>> numList = [];\n    for (int i = 0; i < n; i++) {\n        List<int> tmp = [];\n        for (int j = 0; j < n; j++) {\n            tmp.Add(0);\n        }\n        numList.Add(tmp);\n    }\n}\n
        space_complexity.go
        /* Квадратичная сложность */\nfunc spaceQuadratic(n int) {\n    // Матрица занимает O(n^2) памяти\n    numMatrix := make([][]int, n)\n    for i := 0; i < n; i++ {\n        numMatrix[i] = make([]int, n)\n    }\n}\n
        space_complexity.swift
        /* Квадратичная сложность */\nfunc quadratic(n: Int) {\n    // Двумерный список занимает O(n^2) памяти\n    let numList = Array(repeating: Array(repeating: 0, count: n), count: n)\n}\n
        space_complexity.js
        /* Квадратичная сложность */\nfunction quadratic(n) {\n    // Матрица занимает O(n^2) памяти\n    const numMatrix = Array(n)\n        .fill(null)\n        .map(() => Array(n).fill(null));\n    // Двумерный список занимает O(n^2) памяти\n    const numList = [];\n    for (let i = 0; i < n; i++) {\n        const tmp = [];\n        for (let j = 0; j < n; j++) {\n            tmp.push(0);\n        }\n        numList.push(tmp);\n    }\n}\n
        space_complexity.ts
        /* Квадратичная сложность */\nfunction quadratic(n: number): void {\n    // Матрица занимает O(n^2) памяти\n    const numMatrix = Array(n)\n        .fill(null)\n        .map(() => Array(n).fill(null));\n    // Двумерный список занимает O(n^2) памяти\n    const numList = [];\n    for (let i = 0; i < n; i++) {\n        const tmp = [];\n        for (let j = 0; j < n; j++) {\n            tmp.push(0);\n        }\n        numList.push(tmp);\n    }\n}\n
        space_complexity.dart
        /* Квадратичная сложность */\nvoid quadratic(int n) {\n  // Матрица занимает O(n^2) памяти\n  List<List<int>> numMatrix = List.generate(n, (_) => List.filled(n, 0));\n  // Двумерный список занимает O(n^2) памяти\n  List<List<int>> numList = [];\n  for (var i = 0; i < n; i++) {\n    List<int> tmp = [];\n    for (int j = 0; j < n; j++) {\n      tmp.add(0);\n    }\n    numList.add(tmp);\n  }\n}\n
        space_complexity.rs
        /* Квадратичная сложность */\n#[allow(unused)]\nfn quadratic(n: i32) {\n    // Матрица занимает O(n^2) памяти\n    let num_matrix = vec![vec![0; n as usize]; n as usize];\n    // Двумерный список занимает O(n^2) памяти\n    let mut num_list = Vec::new();\n    for i in 0..n {\n        let mut tmp = Vec::new();\n        for j in 0..n {\n            tmp.push(0);\n        }\n        num_list.push(tmp);\n    }\n}\n
        space_complexity.c
        /* Квадратичная сложность */\nvoid quadratic(int n) {\n    // Двумерный список занимает O(n^2) памяти\n    int **numMatrix = malloc(sizeof(int *) * n);\n    for (int i = 0; i < n; i++) {\n        int *tmp = malloc(sizeof(int) * n);\n        for (int j = 0; j < n; j++) {\n            tmp[j] = 0;\n        }\n        numMatrix[i] = tmp;\n    }\n\n    // Освобождение памяти\n    for (int i = 0; i < n; i++) {\n        free(numMatrix[i]);\n    }\n    free(numMatrix);\n}\n
        space_complexity.kt
        /* Квадратичная сложность */\nfun quadratic(n: Int) {\n    // Матрица занимает O(n^2) памяти\n    val numMatrix = arrayOfNulls<Array<Int>?>(n)\n    // Двумерный список занимает O(n^2) памяти\n    val numList = mutableListOf<MutableList<Int>>()\n    for (i in 0..<n) {\n        val tmp = mutableListOf<Int>()\n        for (j in 0..<n) {\n            tmp.add(0)\n        }\n        numList.add(tmp)\n    }\n}\n
        space_complexity.rb
        ### Квадратичная сложность ###\ndef quadratic(n)\n  # Двумерный список занимает O(n^2) памяти\n  Array.new(n) { Array.new(n, 0) }\nend\n
        Визуализация кода

        Во весь экран >

        Как показано на рисунке 2-18, глубина рекурсии этой функции равна \\(n\\) , и в каждой рекурсивной функции инициализируется массив длины \\(n\\) , \\(n-1\\) , \\(\\dots\\) , \\(2\\) , \\(1\\) ; его средняя длина равна \\(n / 2\\) , поэтому в сумме используется \\(O(n^2)\\) пространства:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
        def quadratic_recur(n: int) -> int:\n    \"\"\"Квадратичная сложность (рекурсивная реализация)\"\"\"\n    if n <= 0:\n        return 0\n    # Длина массива nums равна n, n-1, ..., 2, 1\n    nums = [0] * n\n    return quadratic_recur(n - 1)\n
        space_complexity.cpp
        /* Квадратичная сложность (рекурсивная реализация) */\nint quadraticRecur(int n) {\n    if (n <= 0)\n        return 0;\n    vector<int> nums(n);\n    cout << \"Рекурсия n = \" << n << \" , длина nums = \" << nums.size() << endl;\n    return quadraticRecur(n - 1);\n}\n
        space_complexity.java
        /* Квадратичная сложность (рекурсивная реализация) */\nint quadraticRecur(int n) {\n    if (n <= 0)\n        return 0;\n    // Длина массива nums равна n, n-1, ..., 2, 1\n    int[] nums = new int[n];\n    System.out.println(\"В рекурсии n = \" + n + \", длина nums = \" + nums.length);\n    return quadraticRecur(n - 1);\n}\n
        space_complexity.cs
        /* Квадратичная сложность (рекурсивная реализация) */\nint QuadraticRecur(int n) {\n    if (n <= 0) return 0;\n    int[] nums = new int[n];\n    Console.WriteLine(\"В рекурсии n = \" + n + \", длина nums = \" + nums.Length);\n    return QuadraticRecur(n - 1);\n}\n
        space_complexity.go
        /* Квадратичная сложность (рекурсивная реализация) */\nfunc spaceQuadraticRecur(n int) int {\n    if n <= 0 {\n        return 0\n    }\n    nums := make([]int, n)\n    fmt.Printf(\"В рекурсии n = %d, длина nums = %d\\n\", n, len(nums))\n    return spaceQuadraticRecur(n - 1)\n}\n
        space_complexity.swift
        /* Квадратичная сложность (рекурсивная реализация) */\n@discardableResult\nfunc quadraticRecur(n: Int) -> Int {\n    if n <= 0 {\n        return 0\n    }\n    // Длина массива nums равна n, n-1, ..., 2, 1\n    let nums = Array(repeating: 0, count: n)\n    print(\"В рекурсии n = \\(n), длина nums = \\(nums.count)\")\n    return quadraticRecur(n: n - 1)\n}\n
        space_complexity.js
        /* Квадратичная сложность (рекурсивная реализация) */\nfunction quadraticRecur(n) {\n    if (n <= 0) return 0;\n    const nums = new Array(n);\n    console.log(`В рекурсии n = ${n} длина nums = ${nums.length}`);\n    return quadraticRecur(n - 1);\n}\n
        space_complexity.ts
        /* Квадратичная сложность (рекурсивная реализация) */\nfunction quadraticRecur(n: number): number {\n    if (n <= 0) return 0;\n    const nums = new Array(n);\n    console.log(`В рекурсии n = ${n} длина nums = ${nums.length}`);\n    return quadraticRecur(n - 1);\n}\n
        space_complexity.dart
        /* Квадратичная сложность (рекурсивная реализация) */\nint quadraticRecur(int n) {\n  if (n <= 0) return 0;\n  List<int> nums = List.filled(n, 0);\n  print('В рекурсии n = $n длина nums = ${nums.length}');\n  return quadraticRecur(n - 1);\n}\n
        space_complexity.rs
        /* Квадратичная сложность (рекурсивная реализация) */\nfn quadratic_recur(n: i32) -> i32 {\n    if n <= 0 {\n        return 0;\n    };\n    // Длина массива nums равна n, n-1, ..., 2, 1\n    let nums = vec![0; n as usize];\n    println!(\"В рекурсии n = {} , длина nums = {}\", n, nums.len());\n    return quadratic_recur(n - 1);\n}\n
        space_complexity.c
        /* Квадратичная сложность (рекурсивная реализация) */\nint quadraticRecur(int n) {\n    if (n <= 0)\n        return 0;\n    int *nums = malloc(sizeof(int) * n);\n    printf(\"Рекурсия n = %d, длина nums = %d\\r\\n\", n, n);\n    int res = quadraticRecur(n - 1);\n    free(nums);\n    return res;\n}\n
        space_complexity.kt
        /* Квадратичная сложность (рекурсивная реализация) */\ntailrec fun quadraticRecur(n: Int): Int {\n    if (n <= 0)\n        return 0\n    // Длина массива nums равна n, n-1, ..., 2, 1\n    val nums = Array(n) { 0 }\n    println(\"В рекурсии n = $n длина nums = ${nums.size}\")\n    return quadraticRecur(n - 1)\n}\n
        space_complexity.rb
        ### Квадратичная сложность ###\ndef quadratic(n)\n  # Двумерный список занимает O(n^2) памяти\n  Array.new(n) { Array.new(n, 0) }\nend\n\n# ## Квадратичная сложность (рекурсивная реализация) ###\ndef quadratic_recur(n)\n  return 0 unless n > 0\n\n  # Длина массива nums равна n, n-1, ..., 2, 1\n  nums = Array.new(n, 0)\n  quadratic_recur(n - 1)\nend\n
        Визуализация кода

        Во весь экран >

        Рисунок 2-18   Квадратичная пространственная сложность, порождаемая рекурсивной функцией

        ","path":["Глава 2. Анализ сложности","2.4   Пространственная сложность"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#4-o2n","level":3,"title":"4.   Экспоненциальная сложность \\(O(2^n)\\)","text":"

        Экспоненциальная сложность часто встречается у бинарных деревьев. Полное бинарное дерево с \\(n\\) уровнями содержит \\(2^n - 1\\) узлов и занимает \\(O(2^n)\\) пространства:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
        def build_tree(n: int) -> TreeNode | None:\n    \"\"\"Экспоненциальная сложность (построение полного двоичного дерева)\"\"\"\n    if n == 0:\n        return None\n    root = TreeNode(0)\n    root.left = build_tree(n - 1)\n    root.right = build_tree(n - 1)\n    return root\n
        space_complexity.cpp
        /* Экспоненциальная сложность (построение полного двоичного дерева) */\nTreeNode *buildTree(int n) {\n    if (n == 0)\n        return nullptr;\n    TreeNode *root = new TreeNode(0);\n    root->left = buildTree(n - 1);\n    root->right = buildTree(n - 1);\n    return root;\n}\n
        space_complexity.java
        /* Экспоненциальная сложность (построение полного двоичного дерева) */\nTreeNode buildTree(int n) {\n    if (n == 0)\n        return null;\n    TreeNode root = new TreeNode(0);\n    root.left = buildTree(n - 1);\n    root.right = buildTree(n - 1);\n    return root;\n}\n
        space_complexity.cs
        /* Экспоненциальная сложность (построение полного двоичного дерева) */\nTreeNode? BuildTree(int n) {\n    if (n == 0) return null;\n    TreeNode root = new(0) {\n        left = BuildTree(n - 1),\n        right = BuildTree(n - 1)\n    };\n    return root;\n}\n
        space_complexity.go
        /* Экспоненциальная сложность (построение полного двоичного дерева) */\nfunc buildTree(n int) *TreeNode {\n    if n == 0 {\n        return nil\n    }\n    root := NewTreeNode(0)\n    root.Left = buildTree(n - 1)\n    root.Right = buildTree(n - 1)\n    return root\n}\n
        space_complexity.swift
        /* Экспоненциальная сложность (построение полного двоичного дерева) */\nfunc buildTree(n: Int) -> TreeNode? {\n    if n == 0 {\n        return nil\n    }\n    let root = TreeNode(x: 0)\n    root.left = buildTree(n: n - 1)\n    root.right = buildTree(n: n - 1)\n    return root\n}\n
        space_complexity.js
        /* Экспоненциальная сложность (построение полного двоичного дерева) */\nfunction buildTree(n) {\n    if (n === 0) return null;\n    const root = new TreeNode(0);\n    root.left = buildTree(n - 1);\n    root.right = buildTree(n - 1);\n    return root;\n}\n
        space_complexity.ts
        /* Экспоненциальная сложность (построение полного двоичного дерева) */\nfunction buildTree(n: number): TreeNode | null {\n    if (n === 0) return null;\n    const root = new TreeNode(0);\n    root.left = buildTree(n - 1);\n    root.right = buildTree(n - 1);\n    return root;\n}\n
        space_complexity.dart
        /* Экспоненциальная сложность (построение полного двоичного дерева) */\nTreeNode? buildTree(int n) {\n  if (n == 0) return null;\n  TreeNode root = TreeNode(0);\n  root.left = buildTree(n - 1);\n  root.right = buildTree(n - 1);\n  return root;\n}\n
        space_complexity.rs
        /* Экспоненциальная сложность (построение полного двоичного дерева) */\nfn build_tree(n: i32) -> Option<Rc<RefCell<TreeNode>>> {\n    if n == 0 {\n        return None;\n    };\n    let root = TreeNode::new(0);\n    root.borrow_mut().left = build_tree(n - 1);\n    root.borrow_mut().right = build_tree(n - 1);\n    return Some(root);\n}\n
        space_complexity.c
        /* Экспоненциальная сложность (построение полного двоичного дерева) */\nTreeNode *buildTree(int n) {\n    if (n == 0)\n        return NULL;\n    TreeNode *root = newTreeNode(0);\n    root->left = buildTree(n - 1);\n    root->right = buildTree(n - 1);\n    return root;\n}\n
        space_complexity.kt
        /* Экспоненциальная сложность (построение полного двоичного дерева) */\nfun buildTree(n: Int): TreeNode? {\n    if (n == 0)\n        return null\n    val root = TreeNode(0)\n    root.left = buildTree(n - 1)\n    root.right = buildTree(n - 1)\n    return root\n}\n
        space_complexity.rb
        ### Квадратичная сложность ###\ndef quadratic(n)\n  # Двумерный список занимает O(n^2) памяти\n  Array.new(n) { Array.new(n, 0) }\nend\n\n# ## Квадратичная сложность (рекурсивная реализация) ###\ndef quadratic_recur(n)\n  return 0 unless n > 0\n\n  # Длина массива nums равна n, n-1, ..., 2, 1\n  nums = Array.new(n, 0)\n  quadratic_recur(n - 1)\nend\n\n# ## Экспоненциальная сложность (построение полного двоичного дерева) ###\ndef build_tree(n)\n  return if n == 0\n\n  TreeNode.new.tap do |root|\n    root.left = build_tree(n - 1)\n    root.right = build_tree(n - 1)\n  end\nend\n
        Визуализация кода

        Во весь экран >

        Рисунок 2-19   Экспоненциальная пространственная сложность, порождаемая полным бинарным деревом

        ","path":["Глава 2. Анализ сложности","2.4   Пространственная сложность"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#5-olog-n","level":3,"title":"5.   Логарифмическая сложность \\(O(\\log n)\\)","text":"

        Логарифмическая сложность часто встречается в алгоритмах \"разделяй и властвуй\". Например, при сортировке слиянием входной массив длины \\(n\\) на каждом шаге рекурсии делится пополам, образуя рекурсивное дерево высоты \\(\\log n\\) и используя \\(O(\\log n)\\) пространства кадров стека.

        Еще один пример - преобразование числа в строку. Если задано положительное целое число \\(n\\) , то количество его цифр равно \\(\\lfloor \\log_{10} n \\rfloor + 1\\) , то есть длина соответствующей строки тоже равна \\(\\lfloor \\log_{10} n \\rfloor + 1\\) , следовательно, пространственная сложность составляет \\(O(\\log_{10} n + 1) = O(\\log n)\\) .

        ","path":["Глава 2. Анализ сложности","2.4   Пространственная сложность"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#244","level":2,"title":"2.4.4   Компромисс между временем и пространством","text":"

        В идеальных условиях хотелось бы, чтобы и временная, и пространственная сложность алгоритма были оптимальными. Однако на практике одновременно оптимизировать и время, и память обычно очень трудно.

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

        Выбор между этими двумя идеями зависит от того, что важнее в конкретной задаче. В большинстве случаев время ценнее памяти, поэтому стратегия обмена пространства на время используется чаще. Но при очень больших объемах данных контроль пространственной сложности тоже становится крайне важным.

        ","path":["Глава 2. Анализ сложности","2.4   Пространственная сложность"],"tags":[]},{"location":"chapter_computational_complexity/summary/","level":1,"title":"2.5   Резюме","text":"","path":["Глава 2. Анализ сложности","2.5   Резюме"],"tags":[]},{"location":"chapter_computational_complexity/summary/#1","level":3,"title":"1.   Ключевые выводы","text":"

        Оценка эффективности алгоритмов

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

        Временная сложность

        • Временная сложность используется для оценки тенденции изменения времени выполнения алгоритма с увеличением объема данных, что позволяет оценивать его эффективность. Однако в некоторых случаях она может работать не так хорошо, например когда объем входных данных мал или временная сложность одинакова, что не позволяет точно сравнить эффективность алгоритмов.
        • Худшая временная сложность обозначается символом Big \\(O\\) и соответствует асимптотической верхней границе, отражая уровень роста количества операций \\(T(n)\\) при стремлении \\(n\\) к бесконечности.
        • Определение временной сложности включает два этапа: сначала подсчитывается количество операций, затем определяется асимптотическая верхняя граница.
        • Наиболее распространенные временные сложности в порядке возрастания: \\(O(1)\\), \\(O(\\log n)\\), \\(O(n)\\), \\(O(n \\log n)\\), \\(O(n^2)\\), \\(O(2^n)\\) и \\(O(n!)\\).
        • Временная сложность некоторых алгоритмов не является фиксированной и зависит от распределения входных данных. Временная сложность делится на худшую, лучшую и среднюю. Лучшая временная сложность почти не используется, так как для достижения лучшего случая входные данные должны соответствовать строгим критериям.
        • Средняя временная сложность отражает эффективность алгоритма при случайных входных данных и наиболее близка к реальной производительности алгоритма. Для расчета средней временной сложности необходимо учитывать распределение входных данных и математическое ожидание.

        Пространственная сложность

        • Пространственная сложность аналогична временной сложности и используется для оценки тенденции изменения объема памяти, занимаемой алгоритмом, с увеличением объема данных.
        • Память, используемую в процессе выполнения алгоритма, можно разделить на входное пространство, временное пространство и выходное пространство. Обычно при расчете пространственной сложности входное пространство не учитывается. Временное пространство делится на временные данные, пространство стека и пространство инструкций, причем пространство стека обычно влияет на сложность только в рекурсивных функциях.
        • Обычно рассматривается только худшая пространственная сложность, то есть пространственная сложность алгоритма при худших входных данных и в худший момент выполнения.
        • Наиболее распространенные пространственные сложности в порядке возрастания: \\(O(1)\\), \\(O(\\log n)\\), \\(O(n)\\), \\(O(n^2)\\) и \\(O(2^n)\\).
        ","path":["Глава 2. Анализ сложности","2.5   Резюме"],"tags":[]},{"location":"chapter_computational_complexity/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

        Q: Является ли пространственная сложность хвостовой рекурсии равной \\(O(1)\\)?

        Теоретически пространственную сложность хвостово-рекурсивных функций можно оптимизировать до \\(O(1)\\) . Однако большинство языков программирования (например Java, Python, C++, Go, C# и другие) не поддерживают автоматическую оптимизацию хвостовой рекурсии, поэтому на практике пространственная сложность обычно считается равной \\(O(n)\\) .

        Q: В чем разница между терминами function и method?

        Функция (function) может выполняться независимо, и все ее параметры передаются явно. Метод (method) связан с объектом, неявно получает объект, который его вызывает, и может работать с данными, содержащимися в экземпляре класса.

        Ниже это проиллюстрировано на примере нескольких распространенных языков программирования.

        • C - процедурный язык программирования без объектно-ориентированной модели, поэтому в нем есть только функции. Однако мы можем имитировать объектно-ориентированное программирование через структуры (struct), и функции, связанные со структурами, эквивалентны методам в других языках.
        • Java и C# - объектно-ориентированные языки программирования, в которых блоки кода (методы) обычно являются частью класса. Статические методы по поведению похожи на функции, потому что они привязаны к классу и не могут обращаться к конкретным переменным экземпляра.
        • C++ и Python поддерживают как процедурное программирование (функции), так и объектно-ориентированное программирование (методы).

        Q: Отражает ли диаграмма \"распространенных типов пространственной сложности\" абсолютный размер занятой памяти?

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

        Если взять \\(n = 8\\) , можно заметить, что значения на кривых не совпадают напрямую с соответствующими функциями. Это связано с тем, что каждая кривая содержит константный член, который сжимает диапазон значений до визуально удобного масштаба.

        На практике, поскольку мы обычно не знаем, какова \"константная\" сложность каждого метода, только по сложности мы, как правило, не можем выбрать оптимальное решение для случая \\(n = 8\\) . Но для \\(n = 8^5\\) выбор уже очевиден: в этой области доминирует именно тенденция роста.

        Q: Бывают ли случаи, когда в реальных сценариях алгоритм специально проектируют так, чтобы жертвовать временем ради пространства или пространством ради времени?

        На практике в большинстве случаев выбирают обмен пространства на время. Например, для индексов в базах данных обычно строят B+ деревья или хеш-индексы, расходуя значительный объем памяти ради эффективных запросов уровня \\(O(\\log n)\\) или даже \\(O(1)\\).

        В сценариях, где память особенно дорога, наоборот, могут жертвовать временем ради пространства. Например, в embedded-разработке память устройства очень ограничена, поэтому инженеры могут отказаться от хеш-таблиц и выбрать последовательный поиск по массиву, экономя память ценой более медленного поиска.

        ","path":["Глава 2. Анализ сложности","2.5   Резюме"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/","level":1,"title":"2.3   Временная сложность","text":"

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

        1. Определить платформу выполнения, включая конфигурацию оборудования, язык программирования, системную среду и т.д., поскольку все эти факторы влияют на эффективность выполнения кода.
        2. Оценить время выполнения различных вычислительных операций, например операция сложения + требует 1 нс , операция умножения * требует 10 нс , операция вывода print() требует 5 нс и т.д.
        3. Подсчитать все вычислительные операции в коде и суммировать время выполнения всех операций, чтобы получить общее время работы.

        Например, в следующем коде размер входных данных равен \\(n\\) :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
        # На некоторой платформе выполнения\ndef algorithm(n: int):\n    a = 2      # 1 нс\n    a = a + 1  # 1 нс\n    a = a * 2  # 10 нс\n    # Цикл выполняется n раз\n    for _ in range(n):  # 1 нс\n        print(0)        # 5 нс\n
        // На некоторой платформе выполнения\nvoid algorithm(int n) {\n    int a = 2;  // 1 нс\n    a = a + 1;  // 1 нс\n    a = a * 2;  // 10 нс\n    // Цикл выполняется n раз\n    for (int i = 0; i < n; i++) {  // 1 нс\n        cout << 0 << endl;         // 5 нс\n    }\n}\n
        // На некоторой платформе выполнения\nvoid algorithm(int n) {\n    int a = 2;  // 1 нс\n    a = a + 1;  // 1 нс\n    a = a * 2;  // 10 нс\n    // Цикл выполняется n раз\n    for (int i = 0; i < n; i++) {  // 1 нс\n        System.out.println(0);     // 5 нс\n    }\n}\n
        // На некоторой платформе выполнения\nvoid Algorithm(int n) {\n    int a = 2;  // 1 нс\n    a = a + 1;  // 1 нс\n    a = a * 2;  // 10 нс\n    // Цикл выполняется n раз\n    for (int i = 0; i < n; i++) {  // 1 нс\n        Console.WriteLine(0);      // 5 нс\n    }\n}\n
        // На некоторой платформе выполнения\nfunc algorithm(n int) {\n    a := 2     // 1 нс\n    a = a + 1  // 1 нс\n    a = a * 2  // 10 нс\n    // Цикл выполняется n раз\n    for i := 0; i < n; i++ {  // 1 нс\n        fmt.Println(a)        // 5 нс\n    }\n}\n
        // На некоторой платформе выполнения\nfunc algorithm(n: Int) {\n    var a = 2 // 1 нс\n    a = a + 1 // 1 нс\n    a = a * 2 // 10 нс\n    // Цикл выполняется n раз\n    for _ in 0 ..< n { // 1 нс\n        print(0) // 5 нс\n    }\n}\n
        // На некоторой платформе выполнения\nfunction algorithm(n) {\n    var a = 2; // 1 нс\n    a = a + 1; // 1 нс\n    a = a * 2; // 10 нс\n    // Цикл выполняется n раз\n    for(let i = 0; i < n; i++) { // 1 нс\n        console.log(0); // 5 нс\n    }\n}\n
        // На некоторой платформе выполнения\nfunction algorithm(n: number): void {\n    var a: number = 2; // 1 нс\n    a = a + 1; // 1 нс\n    a = a * 2; // 10 нс\n    // Цикл выполняется n раз\n    for(let i = 0; i < n; i++) { // 1 нс\n        console.log(0); // 5 нс\n    }\n}\n
        // На некоторой платформе выполнения\nvoid algorithm(int n) {\n  int a = 2; // 1 нс\n  a = a + 1; // 1 нс\n  a = a * 2; // 10 нс\n  // Цикл выполняется n раз\n  for (int i = 0; i < n; i++) { // 1 нс\n    print(0); // 5 нс\n  }\n}\n
        // На некоторой платформе выполнения\nfn algorithm(n: i32) {\n    let mut a = 2;      // 1 нс\n    a = a + 1;          // 1 нс\n    a = a * 2;          // 10 нс\n    // Цикл выполняется n раз\n    for _ in 0..n {     // 1 нс\n        println!(\"{}\", 0);  // 5 нс\n    }\n}\n
        // На некоторой платформе выполнения\nvoid algorithm(int n) {\n    int a = 2;  // 1 нс\n    a = a + 1;  // 1 нс\n    a = a * 2;  // 10 нс\n    // Цикл выполняется n раз\n    for (int i = 0; i < n; i++) {   // 1 нс\n        printf(\"%d\", 0);            // 5 нс\n    }\n}\n
        // На некоторой платформе выполнения\nfun algorithm(n: Int) {\n    var a = 2 // 1 нс\n    a = a + 1 // 1 нс\n    a = a * 2 // 10 нс\n    // Цикл выполняется n раз\n    for (i in 0..<n) {  // 1 нс\n        println(0)      // 5 нс\n    }\n}\n
        # На некоторой платформе выполнения\ndef algorithm(n)\n    a = 2       # 1 нс\n    a = a + 1   # 1 нс\n    a = a * 2   # 10 нс\n    # Цикл выполняется n раз\n    (0...n).each do # 1 нс\n        puts 0      # 5 нс\n    end\nend\n

        Согласно приведенному выше методу, время работы алгоритма равно \\((6n + 12)\\) нс :

        \\[ 1 + 1 + 10 + (1 + 5) \\times n = 6n + 12 \\]

        Но на практике подсчитывать реальное время выполнения алгоритма и неразумно, и нереалистично. Во-первых, мы не хотим привязывать оценку времени к конкретной платформе, потому что алгоритм должен запускаться на самых разных платформах. Во-вторых, нам трудно определить время выполнения каждого типа операций, а это делает точную оценку крайне затруднительной.

        ","path":["Глава 2. Анализ сложности","2.3   Временная сложность"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#231","level":2,"title":"2.3.1   Подсчет тенденции роста времени","text":"

        Анализ временной сложности оценивает не само время выполнения алгоритма, а тенденцию роста этого времени по мере увеличения объема данных.

        Понятие \"тенденции роста времени\" выглядит довольно абстрактным, поэтому разберем его на примере. Предположим, размер входных данных равен \\(n\\) , и даны три алгоритма A , B и C :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
        # Временная сложность алгоритма A: постоянная\ndef algorithm_A(n: int):\n    print(0)\n# Временная сложность алгоритма B: линейная\ndef algorithm_B(n: int):\n    for _ in range(n):\n        print(0)\n# Временная сложность алгоритма C: постоянная\ndef algorithm_C(n: int):\n    for _ in range(1000000):\n        print(0)\n
        // Временная сложность алгоритма A: постоянная\nvoid algorithm_A(int n) {\n    cout << 0 << endl;\n}\n// Временная сложность алгоритма B: линейная\nvoid algorithm_B(int n) {\n    for (int i = 0; i < n; i++) {\n        cout << 0 << endl;\n    }\n}\n// Временная сложность алгоритма C: постоянная\nvoid algorithm_C(int n) {\n    for (int i = 0; i < 1000000; i++) {\n        cout << 0 << endl;\n    }\n}\n
        // Временная сложность алгоритма A: постоянная\nvoid algorithm_A(int n) {\n    System.out.println(0);\n}\n// Временная сложность алгоритма B: линейная\nvoid algorithm_B(int n) {\n    for (int i = 0; i < n; i++) {\n        System.out.println(0);\n    }\n}\n// Временная сложность алгоритма C: постоянная\nvoid algorithm_C(int n) {\n    for (int i = 0; i < 1000000; i++) {\n        System.out.println(0);\n    }\n}\n
        // Временная сложность алгоритма A: постоянная\nvoid AlgorithmA(int n) {\n    Console.WriteLine(0);\n}\n// Временная сложность алгоритма B: линейная\nvoid AlgorithmB(int n) {\n    for (int i = 0; i < n; i++) {\n        Console.WriteLine(0);\n    }\n}\n// Временная сложность алгоритма C: постоянная\nvoid AlgorithmC(int n) {\n    for (int i = 0; i < 1000000; i++) {\n        Console.WriteLine(0);\n    }\n}\n
        // Временная сложность алгоритма A: постоянная\nfunc algorithm_A(n int) {\n    fmt.Println(0)\n}\n// Временная сложность алгоритма B: линейная\nfunc algorithm_B(n int) {\n    for i := 0; i < n; i++ {\n        fmt.Println(0)\n    }\n}\n// Временная сложность алгоритма C: постоянная\nfunc algorithm_C(n int) {\n    for i := 0; i < 1000000; i++ {\n        fmt.Println(0)\n    }\n}\n
        // Временная сложность алгоритма A: постоянная\nfunc algorithmA(n: Int) {\n    print(0)\n}\n\n// Временная сложность алгоритма B: линейная\nfunc algorithmB(n: Int) {\n    for _ in 0 ..< n {\n        print(0)\n    }\n}\n\n// Временная сложность алгоритма C: постоянная\nfunc algorithmC(n: Int) {\n    for _ in 0 ..< 1_000_000 {\n        print(0)\n    }\n}\n
        // Временная сложность алгоритма A: постоянная\nfunction algorithm_A(n) {\n    console.log(0);\n}\n// Временная сложность алгоритма B: линейная\nfunction algorithm_B(n) {\n    for (let i = 0; i < n; i++) {\n        console.log(0);\n    }\n}\n// Временная сложность алгоритма C: постоянная\nfunction algorithm_C(n) {\n    for (let i = 0; i < 1000000; i++) {\n        console.log(0);\n    }\n}\n
        // Временная сложность алгоритма A: постоянная\nfunction algorithm_A(n: number): void {\n    console.log(0);\n}\n// Временная сложность алгоритма B: линейная\nfunction algorithm_B(n: number): void {\n    for (let i = 0; i < n; i++) {\n        console.log(0);\n    }\n}\n// Временная сложность алгоритма C: постоянная\nfunction algorithm_C(n: number): void {\n    for (let i = 0; i < 1000000; i++) {\n        console.log(0);\n    }\n}\n
        // Временная сложность алгоритма A: постоянная\nvoid algorithmA(int n) {\n  print(0);\n}\n// Временная сложность алгоритма B: линейная\nvoid algorithmB(int n) {\n  for (int i = 0; i < n; i++) {\n    print(0);\n  }\n}\n// Временная сложность алгоритма C: постоянная\nvoid algorithmC(int n) {\n  for (int i = 0; i < 1000000; i++) {\n    print(0);\n  }\n}\n
        // Временная сложность алгоритма A: постоянная\nfn algorithm_A(n: i32) {\n    println!(\"{}\", 0);\n}\n// Временная сложность алгоритма B: линейная\nfn algorithm_B(n: i32) {\n    for _ in 0..n {\n        println!(\"{}\", 0);\n    }\n}\n// Временная сложность алгоритма C: постоянная\nfn algorithm_C(n: i32) {\n    for _ in 0..1000000 {\n        println!(\"{}\", 0);\n    }\n}\n
        // Временная сложность алгоритма A: постоянная\nvoid algorithm_A(int n) {\n    printf(\"%d\", 0);\n}\n// Временная сложность алгоритма B: линейная\nvoid algorithm_B(int n) {\n    for (int i = 0; i < n; i++) {\n        printf(\"%d\", 0);\n    }\n}\n// Временная сложность алгоритма C: постоянная\nvoid algorithm_C(int n) {\n    for (int i = 0; i < 1000000; i++) {\n        printf(\"%d\", 0);\n    }\n}\n
        // Временная сложность алгоритма A: постоянная\nfun algoritm_A(n: Int) {\n    println(0)\n}\n// Временная сложность алгоритма B: линейная\nfun algorithm_B(n: Int) {\n    for (i in 0..<n){\n        println(0)\n    }\n}\n// Временная сложность алгоритма C: постоянная\nfun algorithm_C(n: Int) {\n    for (i in 0..<1000000) {\n        println(0)\n    }\n}\n
        # Временная сложность алгоритма A: постоянная\ndef algorithm_A(n)\n    puts 0\nend\n\n# Временная сложность алгоритма B: линейная\ndef algorithm_B(n)\n    (0...n).each { puts 0 }\nend\n\n# Временная сложность алгоритма C: постоянная\ndef algorithm_C(n)\n    (0...1_000_000).each { puts 0 }\nend\n

        Ниже показаны временные сложности трех приведенных выше функций.

        • У алгоритма A есть только одна операция вывода, и время его работы не растет с увеличением \\(n\\) . Такую временную сложность называют постоянной.
        • В алгоритме B операция вывода выполняется в цикле \\(n\\) раз, поэтому время работы растет линейно по мере увеличения \\(n\\) . Такая временная сложность называется линейной.
        • В алгоритме C операция вывода выполняется \\(1000000\\) раз; хотя время работы велико, оно не зависит от размера входных данных \\(n\\) . Поэтому временная сложность C такая же, как у A , и тоже является постоянной.

        Рисунок 2-7   Тенденции роста времени для алгоритмов A, B и C

        Какие особенности имеет анализ временной сложности по сравнению с непосредственным измерением времени работы алгоритма?

        • Временная сложность позволяет эффективно оценивать эффективность алгоритма. Например, время работы алгоритма B растет линейно: при \\(n > 1\\) он медленнее алгоритма A , а при \\(n > 1000000\\) медленнее алгоритма C . Если размер входных данных достаточно велик, алгоритм с постоянной сложностью обязательно лучше алгоритма с линейной сложностью. В этом и состоит смысл тенденции роста времени.
        • Метод вывода временной сложности проще. Платформа выполнения и тип вычислительных операций не влияют на тенденцию роста времени работы алгоритма. Поэтому в анализе временной сложности можно считать время выполнения всех вычислительных операций одинаковым единичным временем и тем самым упростить подсчет времени выполнения до подсчета количества операций.
        • У временной сложности есть и определенные ограничения. Например, хотя временная сложность алгоритмов A и C одинакова, их реальное время выполнения сильно различается. Точно так же, хотя временная сложность B выше, чем у C , при малых \\(n\\) алгоритм B очевидно лучше C . Несмотря на эти ограничения, анализ сложности все равно остается самым эффективным и самым распространенным способом оценки алгоритмов.
        ","path":["Глава 2. Анализ сложности","2.3   Временная сложность"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#232","level":2,"title":"2.3.2   Асимптотическая верхняя граница функции","text":"

        Для функции с входным размером \\(n\\) :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
        def algorithm(n: int):\n    a = 1      # +1\n    a = a + 1  # +1\n    a = a * 2  # +1\n    # Цикл выполняется n раз\n    for i in range(n):  # +1\n        print(0)        # +1\n
        void algorithm(int n) {\n    int a = 1;  // +1\n    a = a + 1;  // +1\n    a = a * 2;  // +1\n    // Цикл выполняется n раз\n    for (int i = 0; i < n; i++) { // +1 (каждый раз выполняется i ++)\n        cout << 0 << endl;    // +1\n    }\n}\n
        void algorithm(int n) {\n    int a = 1;  // +1\n    a = a + 1;  // +1\n    a = a * 2;  // +1\n    // Цикл выполняется n раз\n    for (int i = 0; i < n; i++) { // +1 (каждый раз выполняется i ++)\n        System.out.println(0);    // +1\n    }\n}\n
        void Algorithm(int n) {\n    int a = 1;  // +1\n    a = a + 1;  // +1\n    a = a * 2;  // +1\n    // Цикл выполняется n раз\n    for (int i = 0; i < n; i++) {   // +1 (каждый раз выполняется i ++)\n        Console.WriteLine(0);   // +1\n    }\n}\n
        func algorithm(n int) {\n    a := 1      // +1\n    a = a + 1   // +1\n    a = a * 2   // +1\n    // Цикл выполняется n раз\n    for i := 0; i < n; i++ {   // +1\n        fmt.Println(a)         // +1\n    }\n}\n
        func algorithm(n: Int) {\n    var a = 1 // +1\n    a = a + 1 // +1\n    a = a * 2 // +1\n    // Цикл выполняется n раз\n    for _ in 0 ..< n { // +1\n        print(0) // +1\n    }\n}\n
        function algorithm(n) {\n    var a = 1; // +1\n    a += 1; // +1\n    a *= 2; // +1\n    // Цикл выполняется n раз\n    for(let i = 0; i < n; i++){ // +1 (каждый раз выполняется i ++)\n        console.log(0); // +1\n    }\n}\n
        function algorithm(n: number): void{\n    var a: number = 1; // +1\n    a += 1; // +1\n    a *= 2; // +1\n    // Цикл выполняется n раз\n    for(let i = 0; i < n; i++){ // +1 (каждый раз выполняется i ++)\n        console.log(0); // +1\n    }\n}\n
        void algorithm(int n) {\n  int a = 1; // +1\n  a = a + 1; // +1\n  a = a * 2; // +1\n  // Цикл выполняется n раз\n  for (int i = 0; i < n; i++) { // +1 (каждый раз выполняется i ++)\n    print(0); // +1\n  }\n}\n
        fn algorithm(n: i32) {\n    let mut a = 1;   // +1\n    a = a + 1;      // +1\n    a = a * 2;      // +1\n\n    // Цикл выполняется n раз\n    for _ in 0..n { // +1 (каждый раз выполняется i ++)\n        println!(\"{}\", 0); // +1\n    }\n}\n
        void algorithm(int n) {\n    int a = 1;  // +1\n    a = a + 1;  // +1\n    a = a * 2;  // +1\n    // Цикл выполняется n раз\n    for (int i = 0; i < n; i++) {   // +1 (каждый раз выполняется i ++)\n        printf(\"%d\", 0);            // +1\n    }\n}\n
        fun algorithm(n: Int) {\n    var a = 1 // +1\n    a = a + 1 // +1\n    a = a * 2 // +1\n    // Цикл выполняется n раз\n    for (i in 0..<n) { // +1 (каждый раз выполняется i ++)\n        println(0) // +1\n    }\n}\n
        def algorithm(n)\n    a = 1       # +1\n    a = a + 1   # +1\n    a = a * 2   # +1\n    # Цикл выполняется n раз\n    (0...n).each do # +1\n        puts 0      # +1\n    end\nend\n

        Пусть количество операций алгоритма является функцией от размера входных данных \\(n\\) и обозначается как \\(T(n)\\) ; тогда для приведенной выше функции число операций равно:

        \\[ T(n) = 3 + 2n \\]

        \\(T(n)\\) - линейная функция, а это означает, что тенденция роста времени работы линейна, следовательно, временная сложность здесь тоже линейна.

        Линейную временную сложность записывают как \\(O(n)\\) ; этот математический символ называется нотацией Big \\(O\\) (big-\\(O\\) notation) и обозначает асимптотическую верхнюю границу (asymptotic upper bound) функции \\(T(n)\\) .

        Иными словами, анализ временной сложности сводится к определению асимптотической верхней границы числа операций \\(T(n)\\), и у этого понятия есть строгое математическое определение.

        Асимптотическая верхняя граница функции

        Если существуют положительное действительное число \\(c\\) и действительное число \\(n_0\\) , такие что для всех \\(n > n_0\\) выполняется \\(T(n) \\leq c \\cdot f(n)\\) , то можно считать, что \\(f(n)\\) задает асимптотическую верхнюю границу для \\(T(n)\\) ; это записывается как \\(T(n) = O(f(n))\\) .

        Как показано на рисунке 2-8, вычислить асимптотическую верхнюю границу - значит найти такую функцию \\(f(n)\\) , что при стремлении \\(n\\) к бесконечности функции \\(T(n)\\) и \\(f(n)\\) имеют один и тот же порядок роста и отличаются только постоянным коэффициентом \\(c\\).

        Рисунок 2-8   Асимптотическая верхняя граница функции

        ","path":["Глава 2. Анализ сложности","2.3   Временная сложность"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#233","level":2,"title":"2.3.3   Метод вывода","text":"

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

        Согласно определению, после того как мы определили \\(f(n)\\) , можно получить временную сложность \\(O(f(n))\\) . Но как определить саму асимптотическую верхнюю границу \\(f(n)\\) ? В целом процесс состоит из двух шагов: сначала подсчитать количество операций, затем определить асимптотическую верхнюю границу.

        ","path":["Глава 2. Анализ сложности","2.3   Временная сложность"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#1-1","level":3,"title":"1.   Шаг 1: подсчет количества операций","text":"

        Для кода это можно делать построчно сверху вниз. Однако, поскольку в выражении \\(c \\cdot f(n)\\) постоянный коэффициент \\(c\\) может быть сколь угодно большим, различные коэффициенты и постоянные члены в числе операций \\(T(n)\\) можно игнорировать. Исходя из этого принципа, можно сформулировать следующие упрощающие приемы подсчета.

        1. Игнорировать константы в \\(T(n)\\). Они не зависят от \\(n\\) , а значит не влияют на временную сложность.
        2. Опускать все коэффициенты. Например, циклы на \\(2n\\) раз или \\(5n + 1\\) раз можно упростить до \\(n\\) раз, потому что коэффициент перед \\(n\\) не влияет на временную сложность.
        3. При вложенных циклах использовать умножение. Общее число операций равно произведению числа операций внешнего и внутреннего циклов; при этом для каждого уровня цикла по-прежнему можно применять приемы из пунктов 1. и 2. .

        Для заданной функции мы можем использовать перечисленные выше приемы и подсчитать число операций:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
        def algorithm(n: int):\n    a = 1      # +0 (прием 1)\n    a = a + n  # +0 (прием 1)\n    # +n (прием 2)\n    for i in range(5 * n + 1):\n        print(0)\n    # +n*n (прием 3)\n    for i in range(2 * n):\n        for j in range(n + 1):\n            print(0)\n
        void algorithm(int n) {\n    int a = 1;  // +0 (прием 1)\n    a = a + n;  // +0 (прием 1)\n    // +n (прием 2)\n    for (int i = 0; i < 5 * n + 1; i++) {\n        cout << 0 << endl;\n    }\n    // +n*n (прием 3)\n    for (int i = 0; i < 2 * n; i++) {\n        for (int j = 0; j < n + 1; j++) {\n            cout << 0 << endl;\n        }\n    }\n}\n
        void algorithm(int n) {\n    int a = 1;  // +0 (прием 1)\n    a = a + n;  // +0 (прием 1)\n    // +n (прием 2)\n    for (int i = 0; i < 5 * n + 1; i++) {\n        System.out.println(0);\n    }\n    // +n*n (прием 3)\n    for (int i = 0; i < 2 * n; i++) {\n        for (int j = 0; j < n + 1; j++) {\n            System.out.println(0);\n        }\n    }\n}\n
        void Algorithm(int n) {\n    int a = 1;  // +0 (прием 1)\n    a = a + n;  // +0 (прием 1)\n    // +n (прием 2)\n    for (int i = 0; i < 5 * n + 1; i++) {\n        Console.WriteLine(0);\n    }\n    // +n*n (прием 3)\n    for (int i = 0; i < 2 * n; i++) {\n        for (int j = 0; j < n + 1; j++) {\n            Console.WriteLine(0);\n        }\n    }\n}\n
        func algorithm(n int) {\n    a := 1     // +0 (прием 1)\n    a = a + n  // +0 (прием 1)\n    // +n (прием 2)\n    for i := 0; i < 5 * n + 1; i++ {\n        fmt.Println(0)\n    }\n    // +n*n (прием 3)\n    for i := 0; i < 2 * n; i++ {\n        for j := 0; j < n + 1; j++ {\n            fmt.Println(0)\n        }\n    }\n}\n
        func algorithm(n: Int) {\n    var a = 1 // +0 (прием 1)\n    a = a + n // +0 (прием 1)\n    // +n (прием 2)\n    for _ in 0 ..< (5 * n + 1) {\n        print(0)\n    }\n    // +n*n (прием 3)\n    for _ in 0 ..< (2 * n) {\n        for _ in 0 ..< (n + 1) {\n            print(0)\n        }\n    }\n}\n
        function algorithm(n) {\n    let a = 1;  // +0 (прием 1)\n    a = a + n;  // +0 (прием 1)\n    // +n (прием 2)\n    for (let i = 0; i < 5 * n + 1; i++) {\n        console.log(0);\n    }\n    // +n*n (прием 3)\n    for (let i = 0; i < 2 * n; i++) {\n        for (let j = 0; j < n + 1; j++) {\n            console.log(0);\n        }\n    }\n}\n
        function algorithm(n: number): void {\n    let a = 1;  // +0 (прием 1)\n    a = a + n;  // +0 (прием 1)\n    // +n (прием 2)\n    for (let i = 0; i < 5 * n + 1; i++) {\n        console.log(0);\n    }\n    // +n*n (прием 3)\n    for (let i = 0; i < 2 * n; i++) {\n        for (let j = 0; j < n + 1; j++) {\n            console.log(0);\n        }\n    }\n}\n
        void algorithm(int n) {\n  int a = 1; // +0 (прием 1)\n  a = a + n; // +0 (прием 1)\n  // +n (прием 2)\n  for (int i = 0; i < 5 * n + 1; i++) {\n    print(0);\n  }\n  // +n*n (прием 3)\n  for (int i = 0; i < 2 * n; i++) {\n    for (int j = 0; j < n + 1; j++) {\n      print(0);\n    }\n  }\n}\n
        fn algorithm(n: i32) {\n    let mut a = 1;     // +0 (прием 1)\n    a = a + n;        // +0 (прием 1)\n\n    // +n (прием 2)\n    for i in 0..(5 * n + 1) {\n        println!(\"{}\", 0);\n    }\n\n    // +n*n (прием 3)\n    for i in 0..(2 * n) {\n        for j in 0..(n + 1) {\n            println!(\"{}\", 0);\n        }\n    }\n}\n
        void algorithm(int n) {\n    int a = 1;  // +0 (прием 1)\n    a = a + n;  // +0 (прием 1)\n    // +n (прием 2)\n    for (int i = 0; i < 5 * n + 1; i++) {\n        printf(\"%d\", 0);\n    }\n    // +n*n (прием 3)\n    for (int i = 0; i < 2 * n; i++) {\n        for (int j = 0; j < n + 1; j++) {\n            printf(\"%d\", 0);\n        }\n    }\n}\n
        fun algorithm(n: Int) {\n    var a = 1   // +0 (прием 1)\n    a = a + n   // +0 (прием 1)\n    // +n (прием 2)\n    for (i in 0..<5 * n + 1) {\n        println(0)\n    }\n    // +n*n (прием 3)\n    for (i in 0..<2 * n) {\n        for (j in 0..<n + 1) {\n            println(0)\n        }\n    }\n}\n
        def algorithm(n)\n    a = 1       # +0 (прием 1)\n    a = a + n   # +0 (прием 1)\n    # +n (прием 2)\n    (0...(5 * n + 1)).each do { puts 0 }\n    # +n*n (прием 3)\n    (0...(2 * n)).each do\n        (0...(n + 1)).each do { puts 0 }\n    end\nend\n

        Следующая формула показывает результаты подсчета до и после использования перечисленных выше приемов; в обоих случаях выводимая временная сложность равна \\(O(n^2)\\) .

        \\[ \\begin{aligned} T(n) & = 2n(n + 1) + (5n + 1) + 2 & \\text{полный подсчет (-.-|||)} \\newline & = 2n^2 + 7n + 3 \\newline T(n) & = n^2 + n & \\text{ленивый подсчет (o.O)} \\end{aligned} \\]","path":["Глава 2. Анализ сложности","2.3   Временная сложность"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#2-2","level":3,"title":"2.   Шаг 2: определение асимптотической верхней границы","text":"

        **Временная сложность определяется старшим по степени членом в \\(T(n)\\) **. Это связано с тем, что при стремлении \\(n\\) к бесконечности именно старший член начинает доминировать, а влиянием остальных членов можно пренебречь.

        В таблице 2-2 приведены несколько примеров. Некоторые значения специально сделаны преувеличенными, чтобы подчеркнуть вывод: коэффициент не способен изменить порядок. Когда \\(n\\) стремится к бесконечности, эти константы становятся несущественными.

        Таблица 2-2   Временная сложность, соответствующая разному количеству операций

        Число операций \\(T(n)\\) Временная сложность \\(O(f(n))\\) \\(100000\\) \\(O(1)\\) \\(3n + 2\\) \\(O(n)\\) \\(2n^2 + 3n + 2\\) \\(O(n^2)\\) \\(n^3 + 10000n^2\\) \\(O(n^3)\\) \\(2^n + 10000n^{10000}\\) \\(O(2^n)\\)","path":["Глава 2. Анализ сложности","2.3   Временная сложность"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#234","level":2,"title":"2.3.4   Распространенные типы","text":"

        Пусть размер входных данных равен \\(n\\) ; распространенные типы временной сложности показаны на рисунке 2-9 в порядке от меньшей к большей.

        \\[ \\begin{aligned} & O(1) < O(\\log n) < O(n) < O(n \\log n) < O(n^2) < O(2^n) < O(n!) \\newline & \\text{Постоянная} < \\text{Логарифмическая} < \\text{Линейная} < \\text{Линейно-логарифмическая} < \\text{Квадратичная} < \\text{Экспоненциальная} < \\text{Факториальная} \\end{aligned} \\]

        Рисунок 2-9   Распространенные типы временной сложности

        ","path":["Глава 2. Анализ сложности","2.3   Временная сложность"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#1-o1","level":3,"title":"1.   Постоянная сложность \\(O(1)\\)","text":"

        Число операций при постоянной сложности не зависит от размера входных данных \\(n\\) , то есть не изменяется вместе с изменением \\(n\\) .

        В следующей функции, хотя число операций size может быть большим, оно не зависит от размера входных данных \\(n\\) , поэтому временная сложность по-прежнему равна \\(O(1)\\) :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
        def constant(n: int) -> int:\n    \"\"\"Постоянная сложность\"\"\"\n    count = 0\n    size = 100000\n    for _ in range(size):\n        count += 1\n    return count\n
        time_complexity.cpp
        /* Постоянная сложность */\nint constant(int n) {\n    int count = 0;\n    int size = 100000;\n    for (int i = 0; i < size; i++)\n        count++;\n    return count;\n}\n
        time_complexity.java
        /* Постоянная сложность */\nint constant(int n) {\n    int count = 0;\n    int size = 100000;\n    for (int i = 0; i < size; i++)\n        count++;\n    return count;\n}\n
        time_complexity.cs
        /* Постоянная сложность */\nint Constant(int n) {\n    int count = 0;\n    int size = 100000;\n    for (int i = 0; i < size; i++)\n        count++;\n    return count;\n}\n
        time_complexity.go
        /* Постоянная сложность */\nfunc constant(n int) int {\n    count := 0\n    size := 100000\n    for i := 0; i < size; i++ {\n        count++\n    }\n    return count\n}\n
        time_complexity.swift
        /* Постоянная сложность */\nfunc constant(n: Int) -> Int {\n    var count = 0\n    let size = 100_000\n    for _ in 0 ..< size {\n        count += 1\n    }\n    return count\n}\n
        time_complexity.js
        /* Постоянная сложность */\nfunction constant(n) {\n    let count = 0;\n    const size = 100000;\n    for (let i = 0; i < size; i++) count++;\n    return count;\n}\n
        time_complexity.ts
        /* Постоянная сложность */\nfunction constant(n: number): number {\n    let count = 0;\n    const size = 100000;\n    for (let i = 0; i < size; i++) count++;\n    return count;\n}\n
        time_complexity.dart
        /* Постоянная сложность */\nint constant(int n) {\n  int count = 0;\n  int size = 100000;\n  for (var i = 0; i < size; i++) {\n    count++;\n  }\n  return count;\n}\n
        time_complexity.rs
        /* Постоянная сложность */\nfn constant(n: i32) -> i32 {\n    _ = n;\n    let mut count = 0;\n    let size = 100_000;\n    for _ in 0..size {\n        count += 1;\n    }\n    count\n}\n
        time_complexity.c
        /* Постоянная сложность */\nint constant(int n) {\n    int count = 0;\n    int size = 100000;\n    int i = 0;\n    for (int i = 0; i < size; i++) {\n        count++;\n    }\n    return count;\n}\n
        time_complexity.kt
        /* Постоянная сложность */\nfun constant(n: Int): Int {\n    var count = 0\n    val size = 100000\n    for (i in 0..<size)\n        count++\n    return count\n}\n
        time_complexity.rb
        ### Постоянная сложность ###\ndef constant(n)\n  count = 0\n  size = 100000\n\n  (0...size).each { count += 1 }\n\n  count\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 2. Анализ сложности","2.3   Временная сложность"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#2-on","level":3,"title":"2.   Линейная сложность \\(O(n)\\)","text":"

        Линейная сложность характеризуется тем, что число операций растет линейно относительно размера входных данных \\(n\\) . Линейная сложность обычно встречается в одноуровневых циклах:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
        def linear(n: int) -> int:\n    \"\"\"Линейная сложность\"\"\"\n    count = 0\n    for _ in range(n):\n        count += 1\n    return count\n
        time_complexity.cpp
        /* Линейная сложность */\nint linear(int n) {\n    int count = 0;\n    for (int i = 0; i < n; i++)\n        count++;\n    return count;\n}\n
        time_complexity.java
        /* Линейная сложность */\nint linear(int n) {\n    int count = 0;\n    for (int i = 0; i < n; i++)\n        count++;\n    return count;\n}\n
        time_complexity.cs
        /* Линейная сложность */\nint Linear(int n) {\n    int count = 0;\n    for (int i = 0; i < n; i++)\n        count++;\n    return count;\n}\n
        time_complexity.go
        /* Линейная сложность */\nfunc linear(n int) int {\n    count := 0\n    for i := 0; i < n; i++ {\n        count++\n    }\n    return count\n}\n
        time_complexity.swift
        /* Линейная сложность */\nfunc linear(n: Int) -> Int {\n    var count = 0\n    for _ in 0 ..< n {\n        count += 1\n    }\n    return count\n}\n
        time_complexity.js
        /* Линейная сложность */\nfunction linear(n) {\n    let count = 0;\n    for (let i = 0; i < n; i++) count++;\n    return count;\n}\n
        time_complexity.ts
        /* Линейная сложность */\nfunction linear(n: number): number {\n    let count = 0;\n    for (let i = 0; i < n; i++) count++;\n    return count;\n}\n
        time_complexity.dart
        /* Линейная сложность */\nint linear(int n) {\n  int count = 0;\n  for (var i = 0; i < n; i++) {\n    count++;\n  }\n  return count;\n}\n
        time_complexity.rs
        /* Линейная сложность */\nfn linear(n: i32) -> i32 {\n    let mut count = 0;\n    for _ in 0..n {\n        count += 1;\n    }\n    count\n}\n
        time_complexity.c
        /* Линейная сложность */\nint linear(int n) {\n    int count = 0;\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
        time_complexity.kt
        /* Линейная сложность */\nfun linear(n: Int): Int {\n    var count = 0\n    for (i in 0..<n)\n        count++\n    return count\n}\n
        time_complexity.rb
        ### Линейная сложность ###\ndef linear(n)\n  count = 0\n  (0...n).each { count += 1 }\n  count\nend\n
        Визуализация кода

        Во весь экран >

        Операции обхода массива и обхода связного списка имеют временную сложность \\(O(n)\\) , где \\(n\\) - длина массива или списка:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
        def array_traversal(nums: list[int]) -> int:\n    \"\"\"Линейная сложность (обход массива)\"\"\"\n    count = 0\n    # Число итераций пропорционально длине массива\n    for num in nums:\n        count += 1\n    return count\n
        time_complexity.cpp
        /* Линейная сложность (обход массива) */\nint arrayTraversal(vector<int> &nums) {\n    int count = 0;\n    // Число итераций пропорционально длине массива\n    for (int num : nums) {\n        count++;\n    }\n    return count;\n}\n
        time_complexity.java
        /* Линейная сложность (обход массива) */\nint arrayTraversal(int[] nums) {\n    int count = 0;\n    // Число итераций пропорционально длине массива\n    for (int num : nums) {\n        count++;\n    }\n    return count;\n}\n
        time_complexity.cs
        /* Линейная сложность (обход массива) */\nint ArrayTraversal(int[] nums) {\n    int count = 0;\n    // Число итераций пропорционально длине массива\n    foreach (int num in nums) {\n        count++;\n    }\n    return count;\n}\n
        time_complexity.go
        /* Линейная сложность (обход массива) */\nfunc arrayTraversal(nums []int) int {\n    count := 0\n    // Число итераций пропорционально длине массива\n    for range nums {\n        count++\n    }\n    return count\n}\n
        time_complexity.swift
        /* Линейная сложность (обход массива) */\nfunc arrayTraversal(nums: [Int]) -> Int {\n    var count = 0\n    // Число итераций пропорционально длине массива\n    for _ in nums {\n        count += 1\n    }\n    return count\n}\n
        time_complexity.js
        /* Линейная сложность (обход массива) */\nfunction arrayTraversal(nums) {\n    let count = 0;\n    // Число итераций пропорционально длине массива\n    for (let i = 0; i < nums.length; i++) {\n        count++;\n    }\n    return count;\n}\n
        time_complexity.ts
        /* Линейная сложность (обход массива) */\nfunction arrayTraversal(nums: number[]): number {\n    let count = 0;\n    // Число итераций пропорционально длине массива\n    for (let i = 0; i < nums.length; i++) {\n        count++;\n    }\n    return count;\n}\n
        time_complexity.dart
        /* Линейная сложность (обход массива) */\nint arrayTraversal(List<int> nums) {\n  int count = 0;\n  // Число итераций пропорционально длине массива\n  for (var _num in nums) {\n    count++;\n  }\n  return count;\n}\n
        time_complexity.rs
        /* Линейная сложность (обход массива) */\nfn array_traversal(nums: &[i32]) -> i32 {\n    let mut count = 0;\n    // Число итераций пропорционально длине массива\n    for _ in nums {\n        count += 1;\n    }\n    count\n}\n
        time_complexity.c
        /* Линейная сложность (обход массива) */\nint arrayTraversal(int *nums, int n) {\n    int count = 0;\n    // Число итераций пропорционально длине массива\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
        time_complexity.kt
        /* Линейная сложность (обход массива) */\nfun arrayTraversal(nums: IntArray): Int {\n    var count = 0\n    // Число итераций пропорционально длине массива\n    for (num in nums) {\n        count++\n    }\n    return count\n}\n
        time_complexity.rb
        ### Линейная сложность ###\ndef linear(n)\n  count = 0\n  (0...n).each { count += 1 }\n  count\nend\n\n# ## Линейная сложность (обход массива) ###\ndef array_traversal(nums)\n  count = 0\n\n  # Число итераций пропорционально длине массива\n  for num in nums\n    count += 1\n  end\n\n  count\nend\n
        Визуализация кода

        Во весь экран >

        Стоит отметить, что размер входных данных \\(n\\) нужно определять конкретно в зависимости от типа входа. Например, в первом примере переменная \\(n\\) сама является размером входных данных; во втором примере размером данных служит длина массива.

        ","path":["Глава 2. Анализ сложности","2.3   Временная сложность"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#3-on2","level":3,"title":"3.   Квадратичная сложность \\(O(n^2)\\)","text":"

        Квадратичная сложность характеризуется тем, что число операций растет квадратично относительно размера входных данных \\(n\\) . Квадратичная сложность обычно встречается во вложенных циклах: временная сложность внешнего и внутреннего циклов равна \\(O(n)\\) , поэтому общая временная сложность составляет \\(O(n^2)\\) :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
        def quadratic(n: int) -> int:\n    \"\"\"Квадратичная сложность\"\"\"\n    count = 0\n    # Число итераций квадратично зависит от размера данных n\n    for i in range(n):\n        for j in range(n):\n            count += 1\n    return count\n
        time_complexity.cpp
        /* Квадратичная сложность */\nint quadratic(int n) {\n    int count = 0;\n    // Число итераций квадратично зависит от размера данных n\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
        time_complexity.java
        /* Квадратичная сложность */\nint quadratic(int n) {\n    int count = 0;\n    // Число итераций квадратично зависит от размера данных n\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
        time_complexity.cs
        /* Квадратичная сложность */\nint Quadratic(int n) {\n    int count = 0;\n    // Число итераций квадратично зависит от размера данных n\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
        time_complexity.go
        /* Квадратичная сложность */\nfunc quadratic(n int) int {\n    count := 0\n    // Число итераций квадратично зависит от размера данных n\n    for i := 0; i < n; i++ {\n        for j := 0; j < n; j++ {\n            count++\n        }\n    }\n    return count\n}\n
        time_complexity.swift
        /* Квадратичная сложность */\nfunc quadratic(n: Int) -> Int {\n    var count = 0\n    // Число итераций квадратично зависит от размера данных n\n    for _ in 0 ..< n {\n        for _ in 0 ..< n {\n            count += 1\n        }\n    }\n    return count\n}\n
        time_complexity.js
        /* Квадратичная сложность */\nfunction quadratic(n) {\n    let count = 0;\n    // Число итераций квадратично зависит от размера данных n\n    for (let i = 0; i < n; i++) {\n        for (let j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
        time_complexity.ts
        /* Квадратичная сложность */\nfunction quadratic(n: number): number {\n    let count = 0;\n    // Число итераций квадратично зависит от размера данных n\n    for (let i = 0; i < n; i++) {\n        for (let j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
        time_complexity.dart
        /* Квадратичная сложность */\nint quadratic(int n) {\n  int count = 0;\n  // Число итераций квадратично зависит от размера данных n\n  for (int i = 0; i < n; i++) {\n    for (int j = 0; j < n; j++) {\n      count++;\n    }\n  }\n  return count;\n}\n
        time_complexity.rs
        /* Квадратичная сложность */\nfn quadratic(n: i32) -> i32 {\n    let mut count = 0;\n    // Число итераций квадратично зависит от размера данных n\n    for _ in 0..n {\n        for _ in 0..n {\n            count += 1;\n        }\n    }\n    count\n}\n
        time_complexity.c
        /* Квадратичная сложность */\nint quadratic(int n) {\n    int count = 0;\n    // Число итераций квадратично зависит от размера данных n\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
        time_complexity.kt
        /* Квадратичная сложность */\nfun quadratic(n: Int): Int {\n    var count = 0\n    // Число итераций квадратично зависит от размера данных n\n    for (i in 0..<n) {\n        for (j in 0..<n) {\n            count++\n        }\n    }\n    return count\n}\n
        time_complexity.rb
        ### Квадратичная сложность ###\ndef quadratic(n)\n  count = 0\n\n  # Число итераций квадратично зависит от размера данных n\n  for i in 0...n\n    for j in 0...n\n      count += 1\n    end\n  end\n\n  count\nend\n
        Визуализация кода

        Во весь экран >

        На рисунке 2-10 сравниваются три временные сложности: постоянная, линейная и квадратичная.

        Рисунок 2-10   Постоянная, линейная и квадратичная временная сложность

        Возьмем в качестве примера пузырьковую сортировку: внешний цикл выполняется \\(n - 1\\) раз, внутренний цикл выполняется \\(n-1\\) , \\(n-2\\) , \\(\\dots\\) , \\(2\\) , \\(1\\) раз, в среднем это \\(n / 2\\) раз, поэтому временная сложность равна \\(O((n - 1)n / 2) = O(n^2)\\) :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
        def bubble_sort(nums: list[int]) -> int:\n    \"\"\"Квадратичная сложность (пузырьковая сортировка)\"\"\"\n    count = 0  # Счетчик\n    # Внешний цикл: неотсортированный диапазон [0, i]\n    for i in range(len(nums) - 1, 0, -1):\n        # Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for j in range(i):\n            if nums[j] > nums[j + 1]:\n                # Поменять местами nums[j] и nums[j + 1]\n                tmp: int = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = tmp\n                count += 3  # Обмен элементов включает 3 элементарные операции\n    return count\n
        time_complexity.cpp
        /* Квадратичная сложность (пузырьковая сортировка) */\nint bubbleSort(vector<int> &nums) {\n    int count = 0; // Счетчик\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (int i = nums.size() - 1; i > 0; i--) {\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Поменять местами nums[j] и nums[j + 1]\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // Обмен элементов включает 3 элементарные операции\n            }\n        }\n    }\n    return count;\n}\n
        time_complexity.java
        /* Квадратичная сложность (пузырьковая сортировка) */\nint bubbleSort(int[] nums) {\n    int count = 0; // Счетчик\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (int i = nums.length - 1; i > 0; i--) {\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Поменять местами nums[j] и nums[j + 1]\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // Обмен элементов включает 3 элементарные операции\n            }\n        }\n    }\n    return count;\n}\n
        time_complexity.cs
        /* Квадратичная сложность (пузырьковая сортировка) */\nint BubbleSort(int[] nums) {\n    int count = 0;  // Счетчик\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (int i = nums.Length - 1; i > 0; i--) {\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Поменять местами nums[j] и nums[j + 1]\n                (nums[j + 1], nums[j]) = (nums[j], nums[j + 1]);\n                count += 3;  // Обмен элементов включает 3 элементарные операции\n            }\n        }\n    }\n    return count;\n}\n
        time_complexity.go
        /* Квадратичная сложность (пузырьковая сортировка) */\nfunc bubbleSort(nums []int) int {\n    count := 0 // Счетчик\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for i := len(nums) - 1; i > 0; i-- {\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for j := 0; j < i; j++ {\n            if nums[j] > nums[j+1] {\n                // Поменять местами nums[j] и nums[j + 1]\n                tmp := nums[j]\n                nums[j] = nums[j+1]\n                nums[j+1] = tmp\n                count += 3 // Обмен элементов включает 3 элементарные операции\n            }\n        }\n    }\n    return count\n}\n
        time_complexity.swift
        /* Квадратичная сложность (пузырьковая сортировка) */\nfunc bubbleSort(nums: inout [Int]) -> Int {\n    var count = 0 // Счетчик\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for i in nums.indices.dropFirst().reversed() {\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for j in 0 ..< i {\n            if nums[j] > nums[j + 1] {\n                // Поменять местами nums[j] и nums[j + 1]\n                let tmp = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = tmp\n                count += 3 // Обмен элементов включает 3 элементарные операции\n            }\n        }\n    }\n    return count\n}\n
        time_complexity.js
        /* Квадратичная сложность (пузырьковая сортировка) */\nfunction bubbleSort(nums) {\n    let count = 0; // Счетчик\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Поменять местами nums[j] и nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // Обмен элементов включает 3 элементарные операции\n            }\n        }\n    }\n    return count;\n}\n
        time_complexity.ts
        /* Квадратичная сложность (пузырьковая сортировка) */\nfunction bubbleSort(nums: number[]): number {\n    let count = 0; // Счетчик\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Поменять местами nums[j] и nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // Обмен элементов включает 3 элементарные операции\n            }\n        }\n    }\n    return count;\n}\n
        time_complexity.dart
        /* Квадратичная сложность (пузырьковая сортировка) */\nint bubbleSort(List<int> nums) {\n  int count = 0; // Счетчик\n  // Внешний цикл: неотсортированный диапазон [0, i]\n  for (var i = nums.length - 1; i > 0; i--) {\n    // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n    for (var j = 0; j < i; j++) {\n      if (nums[j] > nums[j + 1]) {\n        // Поменять местами nums[j] и nums[j + 1]\n        int tmp = nums[j];\n        nums[j] = nums[j + 1];\n        nums[j + 1] = tmp;\n        count += 3; // Обмен элементов включает 3 элементарные операции\n      }\n    }\n  }\n  return count;\n}\n
        time_complexity.rs
        /* Квадратичная сложность (пузырьковая сортировка) */\nfn bubble_sort(nums: &mut [i32]) -> i32 {\n    let mut count = 0; // Счетчик\n\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for i in (1..nums.len()).rev() {\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for j in 0..i {\n            if nums[j] > nums[j + 1] {\n                // Поменять местами nums[j] и nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // Обмен элементов включает 3 элементарные операции\n            }\n        }\n    }\n    count\n}\n
        time_complexity.c
        /* Квадратичная сложность (пузырьковая сортировка) */\nint bubbleSort(int *nums, int n) {\n    int count = 0; // Счетчик\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (int i = n - 1; i > 0; i--) {\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Поменять местами nums[j] и nums[j + 1]\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // Обмен элементов включает 3 элементарные операции\n            }\n        }\n    }\n    return count;\n}\n
        time_complexity.kt
        /* Квадратичная сложность (пузырьковая сортировка) */\nfun bubbleSort(nums: IntArray): Int {\n    var count = 0 // Счетчик\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (i in nums.size - 1 downTo 1) {\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (j in 0..<i) {\n            if (nums[j] > nums[j + 1]) {\n                // Поменять местами nums[j] и nums[j + 1]\n                val temp = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = temp\n                count += 3 // Обмен элементов включает 3 элементарные операции\n            }\n        }\n    }\n    return count\n}\n
        time_complexity.rb
        ### Квадратичная сложность ###\ndef quadratic(n)\n  count = 0\n\n  # Число итераций квадратично зависит от размера данных n\n  for i in 0...n\n    for j in 0...n\n      count += 1\n    end\n  end\n\n  count\nend\n\n# ## Квадратичная сложность (пузырьковая сортировка) ###\ndef bubble_sort(nums)\n  count = 0  # Счетчик\n\n  # Внешний цикл: неотсортированный диапазон [0, i]\n  for i in (nums.length - 1).downto(0)\n    # Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # Поменять местами nums[j] и nums[j + 1]\n        tmp = nums[j]\n        nums[j] = nums[j + 1]\n        nums[j + 1] = tmp\n        count += 3 # Обмен элементов включает 3 элементарные операции\n      end\n    end\n  end\n\n  count\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 2. Анализ сложности","2.3   Временная сложность"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#4-o2n","level":3,"title":"4.   Экспоненциальная сложность \\(O(2^n)\\)","text":"

        Типичный пример экспоненциального роста в биологии - деление клеток: в начальном состоянии есть одна клетка, после одного деления их становится 2, после двух делений - 4 и так далее; после \\(n\\) раундов деления клеток становится \\(2^n\\) .

        На рисунке 2-11 и в следующем коде моделируется процесс деления клеток; временная сложность равна \\(O(2^n)\\) . Здесь входное значение \\(n\\) обозначает число раундов деления, а возвращаемое значение count обозначает общее число делений.

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
        def exponential(n: int) -> int:\n    \"\"\"Экспоненциальная сложность (итеративная реализация)\"\"\"\n    count = 0\n    base = 1\n    # На каждом шаге клетка делится надвое, образуя последовательность 1, 2, 4, 8, ..., 2^(n-1)\n    for _ in range(n):\n        for _ in range(base):\n            count += 1\n        base *= 2\n    # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count\n
        time_complexity.cpp
        /* Экспоненциальная сложность (итеративная реализация) */\nint exponential(int n) {\n    int count = 0, base = 1;\n    // На каждом шаге клетка делится надвое, образуя последовательность 1, 2, 4, 8, ..., 2^(n-1)\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < base; j++) {\n            count++;\n        }\n        base *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
        time_complexity.java
        /* Экспоненциальная сложность (итеративная реализация) */\nint exponential(int n) {\n    int count = 0, base = 1;\n    // На каждом шаге клетка делится надвое, образуя последовательность 1, 2, 4, 8, ..., 2^(n-1)\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < base; j++) {\n            count++;\n        }\n        base *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
        time_complexity.cs
        /* Экспоненциальная сложность (итеративная реализация) */\nint Exponential(int n) {\n    int count = 0, bas = 1;\n    // На каждом шаге клетка делится надвое, образуя последовательность 1, 2, 4, 8, ..., 2^(n-1)\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < bas; j++) {\n            count++;\n        }\n        bas *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
        time_complexity.go
        /* Экспоненциальная сложность (итеративная реализация) */\nfunc exponential(n int) int {\n    count, base := 0, 1\n    // На каждом шаге клетка делится надвое, образуя последовательность 1, 2, 4, 8, ..., 2^(n-1)\n    for i := 0; i < n; i++ {\n        for j := 0; j < base; j++ {\n            count++\n        }\n        base *= 2\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count\n}\n
        time_complexity.swift
        /* Экспоненциальная сложность (итеративная реализация) */\nfunc exponential(n: Int) -> Int {\n    var count = 0\n    var base = 1\n    // На каждом шаге клетка делится надвое, образуя последовательность 1, 2, 4, 8, ..., 2^(n-1)\n    for _ in 0 ..< n {\n        for _ in 0 ..< base {\n            count += 1\n        }\n        base *= 2\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count\n}\n
        time_complexity.js
        /* Экспоненциальная сложность (итеративная реализация) */\nfunction exponential(n) {\n    let count = 0,\n        base = 1;\n    // На каждом шаге клетка делится надвое, образуя последовательность 1, 2, 4, 8, ..., 2^(n-1)\n    for (let i = 0; i < n; i++) {\n        for (let j = 0; j < base; j++) {\n            count++;\n        }\n        base *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
        time_complexity.ts
        /* Экспоненциальная сложность (итеративная реализация) */\nfunction exponential(n: number): number {\n    let count = 0,\n        base = 1;\n    // На каждом шаге клетка делится надвое, образуя последовательность 1, 2, 4, 8, ..., 2^(n-1)\n    for (let i = 0; i < n; i++) {\n        for (let j = 0; j < base; j++) {\n            count++;\n        }\n        base *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
        time_complexity.dart
        /* Экспоненциальная сложность (итеративная реализация) */\nint exponential(int n) {\n  int count = 0, base = 1;\n  // На каждом шаге клетка делится надвое, образуя последовательность 1, 2, 4, 8, ..., 2^(n-1)\n  for (var i = 0; i < n; i++) {\n    for (var j = 0; j < base; j++) {\n      count++;\n    }\n    base *= 2;\n  }\n  // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n  return count;\n}\n
        time_complexity.rs
        /* Экспоненциальная сложность (итеративная реализация) */\nfn exponential(n: i32) -> i32 {\n    let mut count = 0;\n    let mut base = 1;\n    // На каждом шаге клетка делится надвое, образуя последовательность 1, 2, 4, 8, ..., 2^(n-1)\n    for _ in 0..n {\n        for _ in 0..base {\n            count += 1\n        }\n        base *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    count\n}\n
        time_complexity.c
        /* Экспоненциальная сложность (итеративная реализация) */\nint exponential(int n) {\n    int count = 0;\n    int bas = 1;\n    // На каждом шаге клетка делится надвое, образуя последовательность 1, 2, 4, 8, ..., 2^(n-1)\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < bas; j++) {\n            count++;\n        }\n        bas *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
        time_complexity.kt
        /* Экспоненциальная сложность (итеративная реализация) */\nfun exponential(n: Int): Int {\n    var count = 0\n    var base = 1\n    // На каждом шаге клетка делится надвое, образуя последовательность 1, 2, 4, 8, ..., 2^(n-1)\n    for (i in 0..<n) {\n        for (j in 0..<base) {\n            count++\n        }\n        base *= 2\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count\n}\n
        time_complexity.rb
        ### Квадратичная сложность ###\ndef quadratic(n)\n  count = 0\n\n  # Число итераций квадратично зависит от размера данных n\n  for i in 0...n\n    for j in 0...n\n      count += 1\n    end\n  end\n\n  count\nend\n\n# ## Квадратичная сложность (пузырьковая сортировка) ###\ndef bubble_sort(nums)\n  count = 0  # Счетчик\n\n  # Внешний цикл: неотсортированный диапазон [0, i]\n  for i in (nums.length - 1).downto(0)\n    # Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # Поменять местами nums[j] и nums[j + 1]\n        tmp = nums[j]\n        nums[j] = nums[j + 1]\n        nums[j + 1] = tmp\n        count += 3 # Обмен элементов включает 3 элементарные операции\n      end\n    end\n  end\n\n  count\nend\n\n# ## Экспоненциальная сложность (итеративная реализация) ###\ndef exponential(n)\n  count, base = 0, 1\n\n  # На каждом шаге клетка делится надвое, образуя последовательность 1, 2, 4, 8, ..., 2^(n-1)\n  (0...n).each do\n    (0...base).each { count += 1 }\n    base *= 2\n  end\n\n  # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n  count\nend\n
        Визуализация кода

        Во весь экран >

        Рисунок 2-11   Экспоненциальная временная сложность

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

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
        def exp_recur(n: int) -> int:\n    \"\"\"Экспоненциальная сложность (рекурсивная реализация)\"\"\"\n    if n == 1:\n        return 1\n    return exp_recur(n - 1) + exp_recur(n - 1) + 1\n
        time_complexity.cpp
        /* Экспоненциальная сложность (рекурсивная реализация) */\nint expRecur(int n) {\n    if (n == 1)\n        return 1;\n    return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
        time_complexity.java
        /* Экспоненциальная сложность (рекурсивная реализация) */\nint expRecur(int n) {\n    if (n == 1)\n        return 1;\n    return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
        time_complexity.cs
        /* Экспоненциальная сложность (рекурсивная реализация) */\nint ExpRecur(int n) {\n    if (n == 1) return 1;\n    return ExpRecur(n - 1) + ExpRecur(n - 1) + 1;\n}\n
        time_complexity.go
        /* Экспоненциальная сложность (рекурсивная реализация) */\nfunc expRecur(n int) int {\n    if n == 1 {\n        return 1\n    }\n    return expRecur(n-1) + expRecur(n-1) + 1\n}\n
        time_complexity.swift
        /* Экспоненциальная сложность (рекурсивная реализация) */\nfunc expRecur(n: Int) -> Int {\n    if n == 1 {\n        return 1\n    }\n    return expRecur(n: n - 1) + expRecur(n: n - 1) + 1\n}\n
        time_complexity.js
        /* Экспоненциальная сложность (рекурсивная реализация) */\nfunction expRecur(n) {\n    if (n === 1) return 1;\n    return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
        time_complexity.ts
        /* Экспоненциальная сложность (рекурсивная реализация) */\nfunction expRecur(n: number): number {\n    if (n === 1) return 1;\n    return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
        time_complexity.dart
        /* Экспоненциальная сложность (рекурсивная реализация) */\nint expRecur(int n) {\n  if (n == 1) return 1;\n  return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
        time_complexity.rs
        /* Экспоненциальная сложность (рекурсивная реализация) */\nfn exp_recur(n: i32) -> i32 {\n    if n == 1 {\n        return 1;\n    }\n    exp_recur(n - 1) + exp_recur(n - 1) + 1\n}\n
        time_complexity.c
        /* Экспоненциальная сложность (рекурсивная реализация) */\nint expRecur(int n) {\n    if (n == 1)\n        return 1;\n    return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
        time_complexity.kt
        /* Экспоненциальная сложность (рекурсивная реализация) */\nfun expRecur(n: Int): Int {\n    if (n == 1) {\n        return 1\n    }\n    return expRecur(n - 1) + expRecur(n - 1) + 1\n}\n
        time_complexity.rb
        ### Квадратичная сложность ###\ndef quadratic(n)\n  count = 0\n\n  # Число итераций квадратично зависит от размера данных n\n  for i in 0...n\n    for j in 0...n\n      count += 1\n    end\n  end\n\n  count\nend\n\n# ## Квадратичная сложность (пузырьковая сортировка) ###\ndef bubble_sort(nums)\n  count = 0  # Счетчик\n\n  # Внешний цикл: неотсортированный диапазон [0, i]\n  for i in (nums.length - 1).downto(0)\n    # Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # Поменять местами nums[j] и nums[j + 1]\n        tmp = nums[j]\n        nums[j] = nums[j + 1]\n        nums[j + 1] = tmp\n        count += 3 # Обмен элементов включает 3 элементарные операции\n      end\n    end\n  end\n\n  count\nend\n\n# ## Экспоненциальная сложность (итеративная реализация) ###\ndef exponential(n)\n  count, base = 0, 1\n\n  # На каждом шаге клетка делится надвое, образуя последовательность 1, 2, 4, 8, ..., 2^(n-1)\n  (0...n).each do\n    (0...base).each { count += 1 }\n    base *= 2\n  end\n\n  # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n  count\nend\n\n# ## Экспоненциальная сложность (рекурсивная реализация) ###\ndef exp_recur(n)\n  return 1 if n == 1\n  exp_recur(n - 1) + exp_recur(n - 1) + 1\nend\n
        Визуализация кода

        Во весь экран >

        Экспоненциальный рост происходит очень быстро и часто встречается в переборных методах, грубой силе, поиске с возвратом и тому подобных подходах. Для задач большого масштаба экспоненциальная сложность неприемлема, и обычно приходится применять динамическое программирование, жадные алгоритмы и другие стратегии.

        ","path":["Глава 2. Анализ сложности","2.3   Временная сложность"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#5-olog-n","level":3,"title":"5.   Логарифмическая сложность \\(O(\\log n)\\)","text":"

        В противоположность экспоненциальной, логарифмическая сложность описывает ситуацию, когда в каждом раунде размер задачи уменьшается вдвое. Пусть размер входных данных равен \\(n\\) ; так как на каждом шаге размер уменьшается вдвое, число итераций равно \\(\\log_2 n\\) , то есть является обратной функцией к \\(2^n\\) .

        На рисунке 2-12 и в следующем коде моделируется процесс, в котором в каждом раунде размер задачи уменьшается вдвое; временная сложность равна \\(O(\\log_2 n)\\) и кратко записывается как \\(O(\\log n)\\) :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
        def logarithmic(n: int) -> int:\n    \"\"\"Логарифмическая сложность (итеративная реализация)\"\"\"\n    count = 0\n    while n > 1:\n        n = n / 2\n        count += 1\n    return count\n
        time_complexity.cpp
        /* Логарифмическая сложность (итеративная реализация) */\nint logarithmic(int n) {\n    int count = 0;\n    while (n > 1) {\n        n = n / 2;\n        count++;\n    }\n    return count;\n}\n
        time_complexity.java
        /* Логарифмическая сложность (итеративная реализация) */\nint logarithmic(int n) {\n    int count = 0;\n    while (n > 1) {\n        n = n / 2;\n        count++;\n    }\n    return count;\n}\n
        time_complexity.cs
        /* Логарифмическая сложность (итеративная реализация) */\nint Logarithmic(int n) {\n    int count = 0;\n    while (n > 1) {\n        n /= 2;\n        count++;\n    }\n    return count;\n}\n
        time_complexity.go
        /* Логарифмическая сложность (итеративная реализация) */\nfunc logarithmic(n int) int {\n    count := 0\n    for n > 1 {\n        n = n / 2\n        count++\n    }\n    return count\n}\n
        time_complexity.swift
        /* Логарифмическая сложность (итеративная реализация) */\nfunc logarithmic(n: Int) -> Int {\n    var count = 0\n    var n = n\n    while n > 1 {\n        n = n / 2\n        count += 1\n    }\n    return count\n}\n
        time_complexity.js
        /* Логарифмическая сложность (итеративная реализация) */\nfunction logarithmic(n) {\n    let count = 0;\n    while (n > 1) {\n        n = n / 2;\n        count++;\n    }\n    return count;\n}\n
        time_complexity.ts
        /* Логарифмическая сложность (итеративная реализация) */\nfunction logarithmic(n: number): number {\n    let count = 0;\n    while (n > 1) {\n        n = n / 2;\n        count++;\n    }\n    return count;\n}\n
        time_complexity.dart
        /* Логарифмическая сложность (итеративная реализация) */\nint logarithmic(int n) {\n  int count = 0;\n  while (n > 1) {\n    n = n ~/ 2;\n    count++;\n  }\n  return count;\n}\n
        time_complexity.rs
        /* Логарифмическая сложность (итеративная реализация) */\nfn logarithmic(mut n: i32) -> i32 {\n    let mut count = 0;\n    while n > 1 {\n        n = n / 2;\n        count += 1;\n    }\n    count\n}\n
        time_complexity.c
        /* Логарифмическая сложность (итеративная реализация) */\nint logarithmic(int n) {\n    int count = 0;\n    while (n > 1) {\n        n = n / 2;\n        count++;\n    }\n    return count;\n}\n
        time_complexity.kt
        /* Логарифмическая сложность (итеративная реализация) */\nfun logarithmic(n: Int): Int {\n    var n1 = n\n    var count = 0\n    while (n1 > 1) {\n        n1 /= 2\n        count++\n    }\n    return count\n}\n
        time_complexity.rb
        ### Квадратичная сложность ###\ndef quadratic(n)\n  count = 0\n\n  # Число итераций квадратично зависит от размера данных n\n  for i in 0...n\n    for j in 0...n\n      count += 1\n    end\n  end\n\n  count\nend\n\n# ## Квадратичная сложность (пузырьковая сортировка) ###\ndef bubble_sort(nums)\n  count = 0  # Счетчик\n\n  # Внешний цикл: неотсортированный диапазон [0, i]\n  for i in (nums.length - 1).downto(0)\n    # Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # Поменять местами nums[j] и nums[j + 1]\n        tmp = nums[j]\n        nums[j] = nums[j + 1]\n        nums[j + 1] = tmp\n        count += 3 # Обмен элементов включает 3 элементарные операции\n      end\n    end\n  end\n\n  count\nend\n\n# ## Экспоненциальная сложность (итеративная реализация) ###\ndef exponential(n)\n  count, base = 0, 1\n\n  # На каждом шаге клетка делится надвое, образуя последовательность 1, 2, 4, 8, ..., 2^(n-1)\n  (0...n).each do\n    (0...base).each { count += 1 }\n    base *= 2\n  end\n\n  # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n  count\nend\n\n# ## Экспоненциальная сложность (рекурсивная реализация) ###\ndef exp_recur(n)\n  return 1 if n == 1\n  exp_recur(n - 1) + exp_recur(n - 1) + 1\nend\n\n# ## Логарифмическая сложность (итеративная реализация) ###\ndef logarithmic(n)\n  count = 0\n\n  while n > 1\n    n /= 2\n    count += 1\n  end\n\n  count\nend\n
        Визуализация кода

        Во весь экран >

        Рисунок 2-12   Логарифмическая временная сложность

        Подобно экспоненциальной сложности, логарифмическая также часто встречается в рекурсивных функциях. Следующий код формирует рекурсивное дерево высотой \\(\\log_2 n\\) :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
        def log_recur(n: int) -> int:\n    \"\"\"Логарифмическая сложность (рекурсивная реализация)\"\"\"\n    if n <= 1:\n        return 0\n    return log_recur(n / 2) + 1\n
        time_complexity.cpp
        /* Логарифмическая сложность (рекурсивная реализация) */\nint logRecur(int n) {\n    if (n <= 1)\n        return 0;\n    return logRecur(n / 2) + 1;\n}\n
        time_complexity.java
        /* Логарифмическая сложность (рекурсивная реализация) */\nint logRecur(int n) {\n    if (n <= 1)\n        return 0;\n    return logRecur(n / 2) + 1;\n}\n
        time_complexity.cs
        /* Логарифмическая сложность (рекурсивная реализация) */\nint LogRecur(int n) {\n    if (n <= 1) return 0;\n    return LogRecur(n / 2) + 1;\n}\n
        time_complexity.go
        /* Логарифмическая сложность (рекурсивная реализация) */\nfunc logRecur(n int) int {\n    if n <= 1 {\n        return 0\n    }\n    return logRecur(n/2) + 1\n}\n
        time_complexity.swift
        /* Логарифмическая сложность (рекурсивная реализация) */\nfunc logRecur(n: Int) -> Int {\n    if n <= 1 {\n        return 0\n    }\n    return logRecur(n: n / 2) + 1\n}\n
        time_complexity.js
        /* Логарифмическая сложность (рекурсивная реализация) */\nfunction logRecur(n) {\n    if (n <= 1) return 0;\n    return logRecur(n / 2) + 1;\n}\n
        time_complexity.ts
        /* Логарифмическая сложность (рекурсивная реализация) */\nfunction logRecur(n: number): number {\n    if (n <= 1) return 0;\n    return logRecur(n / 2) + 1;\n}\n
        time_complexity.dart
        /* Логарифмическая сложность (рекурсивная реализация) */\nint logRecur(int n) {\n  if (n <= 1) return 0;\n  return logRecur(n ~/ 2) + 1;\n}\n
        time_complexity.rs
        /* Логарифмическая сложность (рекурсивная реализация) */\nfn log_recur(n: i32) -> i32 {\n    if n <= 1 {\n        return 0;\n    }\n    log_recur(n / 2) + 1\n}\n
        time_complexity.c
        /* Логарифмическая сложность (рекурсивная реализация) */\nint logRecur(int n) {\n    if (n <= 1)\n        return 0;\n    return logRecur(n / 2) + 1;\n}\n
        time_complexity.kt
        /* Логарифмическая сложность (рекурсивная реализация) */\nfun logRecur(n: Int): Int {\n    if (n <= 1)\n        return 0\n    return logRecur(n / 2) + 1\n}\n
        time_complexity.rb
        ### Квадратичная сложность ###\ndef quadratic(n)\n  count = 0\n\n  # Число итераций квадратично зависит от размера данных n\n  for i in 0...n\n    for j in 0...n\n      count += 1\n    end\n  end\n\n  count\nend\n\n# ## Квадратичная сложность (пузырьковая сортировка) ###\ndef bubble_sort(nums)\n  count = 0  # Счетчик\n\n  # Внешний цикл: неотсортированный диапазон [0, i]\n  for i in (nums.length - 1).downto(0)\n    # Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # Поменять местами nums[j] и nums[j + 1]\n        tmp = nums[j]\n        nums[j] = nums[j + 1]\n        nums[j + 1] = tmp\n        count += 3 # Обмен элементов включает 3 элементарные операции\n      end\n    end\n  end\n\n  count\nend\n\n# ## Экспоненциальная сложность (итеративная реализация) ###\ndef exponential(n)\n  count, base = 0, 1\n\n  # На каждом шаге клетка делится надвое, образуя последовательность 1, 2, 4, 8, ..., 2^(n-1)\n  (0...n).each do\n    (0...base).each { count += 1 }\n    base *= 2\n  end\n\n  # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n  count\nend\n\n# ## Экспоненциальная сложность (рекурсивная реализация) ###\ndef exp_recur(n)\n  return 1 if n == 1\n  exp_recur(n - 1) + exp_recur(n - 1) + 1\nend\n\n# ## Логарифмическая сложность (итеративная реализация) ###\ndef logarithmic(n)\n  count = 0\n\n  while n > 1\n    n /= 2\n    count += 1\n  end\n\n  count\nend\n\n# ## Логарифмическая сложность (рекурсивная реализация) ###\ndef log_recur(n)\n  return 0 unless n > 1\n  log_recur(n / 2) + 1\nend\n
        Визуализация кода

        Во весь экран >

        Логарифмическая сложность часто встречается в алгоритмах, основанных на стратегии \"разделяй и властвуй\", и отражает идеи разбиения на части и упрощения сложной задачи. Она растет медленно и считается одной из самых желательных временных сложностей после константной.

        Каково основание у \\(O(\\log n)\\) ?

        Точнее говоря, \"разделение на \\(m\\) частей\" соответствует временной сложности \\(O(\\log_m n)\\) . А по формуле перехода к другому основанию логарифма мы получаем равные по сложности выражения с разными основаниями:

        \\[ O(\\log_m n) = O(\\log_k n / \\log_k m) = O(\\log_k n) \\]

        Иными словами, основание \\(m\\) можно менять без влияния на сложность. Поэтому мы обычно опускаем основание \\(m\\) и напрямую записываем логарифмическую сложность как \\(O(\\log n)\\) .

        ","path":["Глава 2. Анализ сложности","2.3   Временная сложность"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#6-on-log-n","level":3,"title":"6.   Линейно-логарифмическая сложность \\(O(n \\log n)\\)","text":"

        Линейно-логарифмическая сложность часто встречается в рекурсивных разбиениях, где временная сложность одного измерения равна \\(O(\\log n)\\) , а другого - \\(O(n)\\) . Соответствующий код выглядит следующим образом:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
        def linear_log_recur(n: int) -> int:\n    \"\"\"Линейно-логарифмическая сложность\"\"\"\n    if n <= 1:\n        return 1\n    # Разделение надвое: размер подзадачи уменьшается вдвое\n    count = linear_log_recur(n // 2) + linear_log_recur(n // 2)\n    # Текущая подзадача содержит n операций\n    for _ in range(n):\n        count += 1\n    return count\n
        time_complexity.cpp
        /* Линейно-логарифмическая сложность */\nint linearLogRecur(int n) {\n    if (n <= 1)\n        return 1;\n    int count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
        time_complexity.java
        /* Линейно-логарифмическая сложность */\nint linearLogRecur(int n) {\n    if (n <= 1)\n        return 1;\n    int count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
        time_complexity.cs
        /* Линейно-логарифмическая сложность */\nint LinearLogRecur(int n) {\n    if (n <= 1) return 1;\n    int count = LinearLogRecur(n / 2) + LinearLogRecur(n / 2);\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
        time_complexity.go
        /* Линейно-логарифмическая сложность */\nfunc linearLogRecur(n int) int {\n    if n <= 1 {\n        return 1\n    }\n    count := linearLogRecur(n/2) + linearLogRecur(n/2)\n    for i := 0; i < n; i++ {\n        count++\n    }\n    return count\n}\n
        time_complexity.swift
        /* Линейно-логарифмическая сложность */\nfunc linearLogRecur(n: Int) -> Int {\n    if n <= 1 {\n        return 1\n    }\n    var count = linearLogRecur(n: n / 2) + linearLogRecur(n: n / 2)\n    for _ in stride(from: 0, to: n, by: 1) {\n        count += 1\n    }\n    return count\n}\n
        time_complexity.js
        /* Линейно-логарифмическая сложность */\nfunction linearLogRecur(n) {\n    if (n <= 1) return 1;\n    let count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n    for (let i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
        time_complexity.ts
        /* Линейно-логарифмическая сложность */\nfunction linearLogRecur(n: number): number {\n    if (n <= 1) return 1;\n    let count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n    for (let i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
        time_complexity.dart
        /* Линейно-логарифмическая сложность */\nint linearLogRecur(int n) {\n  if (n <= 1) return 1;\n  int count = linearLogRecur(n ~/ 2) + linearLogRecur(n ~/ 2);\n  for (var i = 0; i < n; i++) {\n    count++;\n  }\n  return count;\n}\n
        time_complexity.rs
        /* Линейно-логарифмическая сложность */\nfn linear_log_recur(n: i32) -> i32 {\n    if n <= 1 {\n        return 1;\n    }\n    let mut count = linear_log_recur(n / 2) + linear_log_recur(n / 2);\n    for _ in 0..n {\n        count += 1;\n    }\n    return count;\n}\n
        time_complexity.c
        /* Линейно-логарифмическая сложность */\nint linearLogRecur(int n) {\n    if (n <= 1)\n        return 1;\n    int count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
        time_complexity.kt
        /* Линейно-логарифмическая сложность */\nfun linearLogRecur(n: Int): Int {\n    if (n <= 1)\n        return 1\n    var count = linearLogRecur(n / 2) + linearLogRecur(n / 2)\n    for (i in 0..<n) {\n        count++\n    }\n    return count\n}\n
        time_complexity.rb
        ### Линейно-логарифмическая сложность ###\ndef linear_log_recur(n)\n  return 1 unless n > 1\n\n  count = linear_log_recur(n / 2) + linear_log_recur(n / 2)\n  (0...n).each { count += 1 }\n\n  count\nend\n
        Визуализация кода

        Во весь экран >

        На рисунке 2-13 показано, как возникает линейно-логарифмическая сложность. Общее число операций на каждом уровне бинарного дерева равно \\(n\\) , а дерево имеет \\(\\log_2 n + 1\\) уровней, поэтому временная сложность равна \\(O(n \\log n)\\) .

        Рисунок 2-13   Линейно-логарифмическая временная сложность

        Временная сложность основных алгоритмов сортировки обычно равна \\(O(n \\log n)\\) , например у быстрой сортировки, сортировки слиянием, пирамидальной сортировки и т.д.

        ","path":["Глава 2. Анализ сложности","2.3   Временная сложность"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#7-on","level":3,"title":"7.   Факториальная сложность \\(O(n!)\\)","text":"

        Факториальная сложность соответствует математической задаче полной перестановки. Если даны \\(n\\) попарно различных элементов, то число всех возможных перестановок равно:

        \\[ n! = n \\times (n - 1) \\times (n - 2) \\times \\dots \\times 2 \\times 1 \\]

        Факториал обычно реализуют через рекурсию. Как показано на рисунке 2-14 и в следующем коде, на первом уровне происходит ветвление на \\(n\\) подзадач, на втором - на \\(n - 1\\) и так далее, пока на \\(n\\)-м уровне ветвление не прекращается:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
        def factorial_recur(n: int) -> int:\n    \"\"\"Факториальная сложность (рекурсивная реализация)\"\"\"\n    if n == 0:\n        return 1\n    count = 0\n    # Из одного получается n\n    for _ in range(n):\n        count += factorial_recur(n - 1)\n    return count\n
        time_complexity.cpp
        /* Факториальная сложность (рекурсивная реализация) */\nint factorialRecur(int n) {\n    if (n == 0)\n        return 1;\n    int count = 0;\n    // Из одного получается n\n    for (int i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
        time_complexity.java
        /* Факториальная сложность (рекурсивная реализация) */\nint factorialRecur(int n) {\n    if (n == 0)\n        return 1;\n    int count = 0;\n    // Из одного получается n\n    for (int i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
        time_complexity.cs
        /* Факториальная сложность (рекурсивная реализация) */\nint FactorialRecur(int n) {\n    if (n == 0) return 1;\n    int count = 0;\n    // Из одного получается n\n    for (int i = 0; i < n; i++) {\n        count += FactorialRecur(n - 1);\n    }\n    return count;\n}\n
        time_complexity.go
        /* Факториальная сложность (рекурсивная реализация) */\nfunc factorialRecur(n int) int {\n    if n == 0 {\n        return 1\n    }\n    count := 0\n    // Из одного получается n\n    for i := 0; i < n; i++ {\n        count += factorialRecur(n - 1)\n    }\n    return count\n}\n
        time_complexity.swift
        /* Факториальная сложность (рекурсивная реализация) */\nfunc factorialRecur(n: Int) -> Int {\n    if n == 0 {\n        return 1\n    }\n    var count = 0\n    // Из одного получается n\n    for _ in 0 ..< n {\n        count += factorialRecur(n: n - 1)\n    }\n    return count\n}\n
        time_complexity.js
        /* Факториальная сложность (рекурсивная реализация) */\nfunction factorialRecur(n) {\n    if (n === 0) return 1;\n    let count = 0;\n    // Из одного получается n\n    for (let i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
        time_complexity.ts
        /* Факториальная сложность (рекурсивная реализация) */\nfunction factorialRecur(n: number): number {\n    if (n === 0) return 1;\n    let count = 0;\n    // Из одного получается n\n    for (let i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
        time_complexity.dart
        /* Факториальная сложность (рекурсивная реализация) */\nint factorialRecur(int n) {\n  if (n == 0) return 1;\n  int count = 0;\n  // Из одного получается n\n  for (var i = 0; i < n; i++) {\n    count += factorialRecur(n - 1);\n  }\n  return count;\n}\n
        time_complexity.rs
        /* Факториальная сложность (рекурсивная реализация) */\nfn factorial_recur(n: i32) -> i32 {\n    if n == 0 {\n        return 1;\n    }\n    let mut count = 0;\n    // Из одного получается n\n    for _ in 0..n {\n        count += factorial_recur(n - 1);\n    }\n    count\n}\n
        time_complexity.c
        /* Факториальная сложность (рекурсивная реализация) */\nint factorialRecur(int n) {\n    if (n == 0)\n        return 1;\n    int count = 0;\n    for (int i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
        time_complexity.kt
        /* Факториальная сложность (рекурсивная реализация) */\nfun factorialRecur(n: Int): Int {\n    if (n == 0)\n        return 1\n    var count = 0\n    // Из одного получается n\n    for (i in 0..<n) {\n        count += factorialRecur(n - 1)\n    }\n    return count\n}\n
        time_complexity.rb
        ### Линейно-логарифмическая сложность ###\ndef linear_log_recur(n)\n  return 1 unless n > 1\n\n  count = linear_log_recur(n / 2) + linear_log_recur(n / 2)\n  (0...n).each { count += 1 }\n\n  count\nend\n\n# ## Факториальная сложность (рекурсивная реализация) ###\ndef factorial_recur(n)\n  return 1 if n == 0\n\n  count = 0\n  # Из одного получается n\n  (0...n).each { count += factorial_recur(n - 1) }\n\n  count\nend\n
        Визуализация кода

        Во весь экран >

        Рисунок 2-14   Факториальная временная сложность

        Следует отметить, что поскольку при \\(n \\geq 4\\) всегда выполняется \\(n! > 2^n\\) , факториальная сложность растет еще быстрее, чем экспоненциальная, и при больших \\(n\\) становится неприемлемой.

        ","path":["Глава 2. Анализ сложности","2.3   Временная сложность"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#235","level":2,"title":"2.3.5   Худшая, лучшая и средняя временная сложность","text":"

        Временная эффективность алгоритма часто не фиксирована, а зависит от распределения входных данных. Предположим, на вход подается массив nums длины \\(n\\) , состоящий из чисел от \\(1\\) до \\(n\\) , каждое из которых встречается ровно один раз; при этом порядок элементов случайно перемешан. Задача состоит в том, чтобы вернуть индекс элемента \\(1\\) . Тогда можно сделать следующие выводы.

        • Когда nums = [?, ?, ..., 1] , то есть когда последний элемент равен \\(1\\) , нужно полностью пройти по массиву, что дает худшую временную сложность \\(O(n)\\) .
        • Когда nums = [1, ?, ?, ...] , то есть когда первый элемент равен \\(1\\) , независимо от длины массива продолжать обход не нужно, что дает лучшую временную сложность \\(\\Omega(1)\\) .

        Худшая временная сложность соответствует асимптотической верхней границе функции и обозначается нотацией Big \\(O\\) . Соответственно, лучшая временная сложность соответствует асимптотической нижней границе функции и обозначается символом \\(\\Omega\\) :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby worst_best_time_complexity.py
        def random_numbers(n: int) -> list[int]:\n    \"\"\"Сгенерировать массив с элементами 1, 2, ..., n в случайном порядке\"\"\"\n    # Создать массив nums =: 1, 2, 3, ..., n\n    nums = [i for i in range(1, n + 1)]\n    # Случайно перемешать элементы массива\n    random.shuffle(nums)\n    return nums\n\ndef find_one(nums: list[int]) -> int:\n    \"\"\"Найти индекс числа 1 в массиве nums\"\"\"\n    for i in range(len(nums)):\n        # Когда элемент 1 находится в начале массива, достигается лучшая временная сложность O(1)\n        # Когда элемент 1 находится в конце массива, достигается худшая временная сложность O(n)\n        if nums[i] == 1:\n            return i\n    return -1\n
        worst_best_time_complexity.cpp
        /* Создать массив с элементами { 1, 2, ..., n } в случайном порядке */\nvector<int> randomNumbers(int n) {\n    vector<int> nums(n);\n    // Создать массив nums = { 1, 2, 3, ..., n }\n    for (int i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // Использовать системное время для генерации случайного seed\n    unsigned seed = chrono::system_clock::now().time_since_epoch().count();\n    // Случайно перемешать элементы массива\n    shuffle(nums.begin(), nums.end(), default_random_engine(seed));\n    return nums;\n}\n\n/* Найти индекс числа 1 в массиве nums */\nint findOne(vector<int> &nums) {\n    for (int i = 0; i < nums.size(); i++) {\n        // Когда элемент 1 находится в начале массива, достигается лучшая временная сложность O(1)\n        // Когда элемент 1 находится в конце массива, достигается худшая временная сложность O(n)\n        if (nums[i] == 1)\n            return i;\n    }\n    return -1;\n}\n
        worst_best_time_complexity.java
        /* Создать массив с элементами { 1, 2, ..., n } в случайном порядке */\nint[] randomNumbers(int n) {\n    Integer[] nums = new Integer[n];\n    // Создать массив nums = { 1, 2, 3, ..., n }\n    for (int i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // Случайно перемешать элементы массива\n    Collections.shuffle(Arrays.asList(nums));\n    // Integer[] -> int[]\n    int[] res = new int[n];\n    for (int i = 0; i < n; i++) {\n        res[i] = nums[i];\n    }\n    return res;\n}\n\n/* Найти индекс числа 1 в массиве nums */\nint findOne(int[] nums) {\n    for (int i = 0; i < nums.length; i++) {\n        // Когда элемент 1 находится в начале массива, достигается лучшая временная сложность O(1)\n        // Когда элемент 1 находится в конце массива, достигается худшая временная сложность O(n)\n        if (nums[i] == 1)\n            return i;\n    }\n    return -1;\n}\n
        worst_best_time_complexity.cs
        /* Создать массив с элементами { 1, 2, ..., n } в случайном порядке */\nint[] RandomNumbers(int n) {\n    int[] nums = new int[n];\n    // Создать массив nums = { 1, 2, 3, ..., n }\n    for (int i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n\n    // Случайно перемешать элементы массива\n    for (int i = 0; i < nums.Length; i++) {\n        int index = new Random().Next(i, nums.Length);\n        (nums[i], nums[index]) = (nums[index], nums[i]);\n    }\n    return nums;\n}\n\n/* Найти индекс числа 1 в массиве nums */\nint FindOne(int[] nums) {\n    for (int i = 0; i < nums.Length; i++) {\n        // Когда элемент 1 находится в начале массива, достигается лучшая временная сложность O(1)\n        // Когда элемент 1 находится в конце массива, достигается худшая временная сложность O(n)\n        if (nums[i] == 1)\n            return i;\n    }\n    return -1;\n}\n
        worst_best_time_complexity.go
        /* Создать массив с элементами { 1, 2, ..., n } в случайном порядке */\nfunc randomNumbers(n int) []int {\n    nums := make([]int, n)\n    // Создать массив nums = { 1, 2, 3, ..., n }\n    for i := 0; i < n; i++ {\n        nums[i] = i + 1\n    }\n    // Случайно перемешать элементы массива\n    rand.Shuffle(len(nums), func(i, j int) {\n        nums[i], nums[j] = nums[j], nums[i]\n    })\n    return nums\n}\n\n/* Найти индекс числа 1 в массиве nums */\nfunc findOne(nums []int) int {\n    for i := 0; i < len(nums); i++ {\n        // Когда элемент 1 находится в начале массива, достигается лучшая временная сложность O(1)\n        // Когда элемент 1 находится в конце массива, достигается худшая временная сложность O(n)\n        if nums[i] == 1 {\n            return i\n        }\n    }\n    return -1\n}\n
        worst_best_time_complexity.swift
        /* Создать массив с элементами { 1, 2, ..., n } в случайном порядке */\nfunc randomNumbers(n: Int) -> [Int] {\n    // Создать массив nums = { 1, 2, 3, ..., n }\n    var nums = Array(1 ... n)\n    // Случайно перемешать элементы массива\n    nums.shuffle()\n    return nums\n}\n\n/* Найти индекс числа 1 в массиве nums */\nfunc findOne(nums: [Int]) -> Int {\n    for i in nums.indices {\n        // Когда элемент 1 находится в начале массива, достигается лучшая временная сложность O(1)\n        // Когда элемент 1 находится в конце массива, достигается худшая временная сложность O(n)\n        if nums[i] == 1 {\n            return i\n        }\n    }\n    return -1\n}\n
        worst_best_time_complexity.js
        /* Создать массив с элементами { 1, 2, ..., n } в случайном порядке */\nfunction randomNumbers(n) {\n    const nums = Array(n);\n    // Создать массив nums = { 1, 2, 3, ..., n }\n    for (let i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // Случайно перемешать элементы массива\n    for (let i = 0; i < n; i++) {\n        const r = Math.floor(Math.random() * (i + 1));\n        const temp = nums[i];\n        nums[i] = nums[r];\n        nums[r] = temp;\n    }\n    return nums;\n}\n\n/* Найти индекс числа 1 в массиве nums */\nfunction findOne(nums) {\n    for (let i = 0; i < nums.length; i++) {\n        // Когда элемент 1 находится в начале массива, достигается лучшая временная сложность O(1)\n        // Когда элемент 1 находится в конце массива, достигается худшая временная сложность O(n)\n        if (nums[i] === 1) {\n            return i;\n        }\n    }\n    return -1;\n}\n
        worst_best_time_complexity.ts
        /* Создать массив с элементами { 1, 2, ..., n } в случайном порядке */\nfunction randomNumbers(n: number): number[] {\n    const nums = Array(n);\n    // Создать массив nums = { 1, 2, 3, ..., n }\n    for (let i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // Случайно перемешать элементы массива\n    for (let i = 0; i < n; i++) {\n        const r = Math.floor(Math.random() * (i + 1));\n        const temp = nums[i];\n        nums[i] = nums[r];\n        nums[r] = temp;\n    }\n    return nums;\n}\n\n/* Найти индекс числа 1 в массиве nums */\nfunction findOne(nums: number[]): number {\n    for (let i = 0; i < nums.length; i++) {\n        // Когда элемент 1 находится в начале массива, достигается лучшая временная сложность O(1)\n        // Когда элемент 1 находится в конце массива, достигается худшая временная сложность O(n)\n        if (nums[i] === 1) {\n            return i;\n        }\n    }\n    return -1;\n}\n
        worst_best_time_complexity.dart
        /* Создать массив с элементами { 1, 2, ..., n } в случайном порядке */\nList<int> randomNumbers(int n) {\n  final nums = List.filled(n, 0);\n  // Создать массив nums = { 1, 2, 3, ..., n }\n  for (var i = 0; i < n; i++) {\n    nums[i] = i + 1;\n  }\n  // Случайно перемешать элементы массива\n  nums.shuffle();\n\n  return nums;\n}\n\n/* Найти индекс числа 1 в массиве nums */\nint findOne(List<int> nums) {\n  for (var i = 0; i < nums.length; i++) {\n    // Когда элемент 1 находится в начале массива, достигается лучшая временная сложность O(1)\n    // Когда элемент 1 находится в конце массива, достигается худшая временная сложность O(n)\n    if (nums[i] == 1) return i;\n  }\n\n  return -1;\n}\n
        worst_best_time_complexity.rs
        /* Создать массив с элементами { 1, 2, ..., n } в случайном порядке */\nfn random_numbers(n: i32) -> Vec<i32> {\n    // Создать массив nums = { 1, 2, 3, ..., n }\n    let mut nums = (1..=n).collect::<Vec<i32>>();\n    // Случайно перемешать элементы массива\n    nums.shuffle(&mut thread_rng());\n    nums\n}\n\n/* Найти индекс числа 1 в массиве nums */\nfn find_one(nums: &[i32]) -> Option<usize> {\n    for i in 0..nums.len() {\n        // Когда элемент 1 находится в начале массива, достигается лучшая временная сложность O(1)\n        // Когда элемент 1 находится в конце массива, достигается худшая временная сложность O(n)\n        if nums[i] == 1 {\n            return Some(i);\n        }\n    }\n    None\n}\n
        worst_best_time_complexity.c
        /* Создать массив с элементами { 1, 2, ..., n } в случайном порядке */\nint *randomNumbers(int n) {\n    // Выделить память в куче (создать одномерный массив переменной длины: число элементов равно n, тип элементов — int)\n    int *nums = (int *)malloc(n * sizeof(int));\n    // Создать массив nums = { 1, 2, 3, ..., n }\n    for (int i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // Случайно перемешать элементы массива\n    for (int i = n - 1; i > 0; i--) {\n        int j = rand() % (i + 1);\n        int temp = nums[i];\n        nums[i] = nums[j];\n        nums[j] = temp;\n    }\n    return nums;\n}\n\n/* Найти индекс числа 1 в массиве nums */\nint findOne(int *nums, int n) {\n    for (int i = 0; i < n; i++) {\n        // Когда элемент 1 находится в начале массива, достигается лучшая временная сложность O(1)\n        // Когда элемент 1 находится в конце массива, достигается худшая временная сложность O(n)\n        if (nums[i] == 1)\n            return i;\n    }\n    return -1;\n}\n
        worst_best_time_complexity.kt
        /* Создать массив с элементами { 1, 2, ..., n } в случайном порядке */\nfun randomNumbers(n: Int): Array<Int?> {\n    val nums = IntArray(n)\n    // Создать массив nums = { 1, 2, 3, ..., n }\n    for (i in 0..<n) {\n        nums[i] = i + 1\n    }\n    // Случайно перемешать элементы массива\n    nums.shuffle()\n    val res = arrayOfNulls<Int>(n)\n    for (i in 0..<n) {\n        res[i] = nums[i]\n    }\n    return res\n}\n\n/* Найти индекс числа 1 в массиве nums */\nfun findOne(nums: Array<Int?>): Int {\n    for (i in nums.indices) {\n        // Когда элемент 1 находится в начале массива, достигается лучшая временная сложность O(1)\n        // Когда элемент 1 находится в конце массива, достигается худшая временная сложность O(n)\n        if (nums[i] == 1)\n            return i\n    }\n    return -1\n}\n
        worst_best_time_complexity.rb
        ### Создать массив с элементами: 1, 2, ..., n в случайном порядке ###\ndef random_numbers(n)\n  # Создать массив nums =: 1, 2, 3, ..., n\n  nums = Array.new(n) { |i| i + 1 }\n  # Случайно перемешать элементы массива\n  nums.shuffle!\nend\n\n### Найти индекс числа 1 в массиве nums ###\ndef find_one(nums)\n  for i in 0...nums.length\n    # Когда элемент 1 находится в начале массива, достигается лучшая временная сложность O(1)\n    # Когда элемент 1 находится в конце массива, достигается худшая временная сложность O(n)\n    return i if nums[i] == 1\n  end\n\n  -1\nend\n
        Визуализация кода

        Во весь экран >

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

        Из приведенного выше примера видно, что худшая и лучшая временные сложности возникают только при особых распределениях данных; вероятность таких случаев может быть низкой, и они не всегда реально отражают эффективность алгоритма. Напротив, средняя временная сложность способна показать эффективность алгоритма на случайных входных данных и обозначается символом \\(\\Theta\\) .

        Для некоторых алгоритмов можно относительно просто вывести средний случай при случайном распределении данных. Например, в приведенном выше примере входной массив перемешан, а вероятность появления элемента \\(1\\) на любом индексе одинакова; следовательно, среднее число итераций алгоритма равно половине длины массива, то есть \\(n / 2\\) , а средняя временная сложность равна \\(\\Theta(n / 2) = \\Theta(n)\\) .

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

        Почему символ \\(\\Theta\\) встречается так редко?

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

        ","path":["Глава 2. Анализ сложности","2.3   Временная сложность"],"tags":[]},{"location":"chapter_data_structure/","level":1,"title":"Глава 3.   Структуры данных","text":"

        Abstract

        Структуры данных подобны прочному и многообразному каркасу.

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

        ","path":["Глава 3. Структуры данных","Глава 3.   Структуры данных"],"tags":[]},{"location":"chapter_data_structure/#_1","level":2,"title":"Содержание главы","text":"
        • 3.1   Классификация структур данных
        • 3.2   Базовые типы данных
        • 3.3   Кодирование чисел *
        • 3.4   Кодирование символов *
        • 3.5   Резюме
        ","path":["Глава 3. Структуры данных","Глава 3.   Структуры данных"],"tags":[]},{"location":"chapter_data_structure/basic_data_types/","level":1,"title":"3.2   Базовые типы данных","text":"

        Когда речь заходит о данных в компьютере, мы в первую очередь вспоминаем текст, изображения, видео, звук, 3D-модели и многие другие формы представления информации. Хотя способы организации этих данных различаются, все они строятся из базовых типов данных.

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

        • Целочисленные типы byte , short , int , long .
        • Типы с плавающей точкой float , double , используемые для представления дробных чисел.
        • Символьный тип char , используемый для представления букв, знаков препинания и даже эмодзи в разных языках.
        • Логический тип bool , используемый для представления суждений \"да\" и \"нет\".

        Базовые типы данных хранятся в компьютере в двоичной форме. Один двоичный разряд равен \\(1\\) биту. В большинстве современных операционных систем \\(1\\) байт (byte) состоит из \\(8\\) битов (bit).

        Диапазон значений базовых типов данных зависит от объема занимаемого ими пространства. Ниже в качестве примера используется Java.

        • Целочисленный тип byte занимает \\(1\\) байт = \\(8\\) бит и может представлять \\(2^{8}\\) чисел.
        • Целочисленный тип int занимает \\(4\\) байта = \\(32\\) бита и может представлять \\(2^{32}\\) чисел.

        В таблице 3-1 перечислены объем памяти, диапазон значений и значения по умолчанию для различных базовых типов данных в Java. Эту таблицу не нужно заучивать наизусть; достаточно иметь общее представление и при необходимости обращаться к ней.

        Таблица 3-1   Объем памяти и диапазоны значений базовых типов данных

        Тип Обозначение Объем памяти Минимальное значение Максимальное значение Значение по умолчанию Целые byte 1 байт \\(-2^7\\) (\\(-128\\)) \\(2^7 - 1\\) (\\(127\\)) \\(0\\) short 2 байта \\(-2^{15}\\) \\(2^{15} - 1\\) \\(0\\) int 4 байта \\(-2^{31}\\) \\(2^{31} - 1\\) \\(0\\) long 8 байт \\(-2^{63}\\) \\(2^{63} - 1\\) \\(0\\) Вещественные float 4 байта \\(1.175 \\times 10^{-38}\\) \\(3.403 \\times 10^{38}\\) \\(0.0\\text{f}\\) double 8 байт \\(2.225 \\times 10^{-308}\\) \\(1.798 \\times 10^{308}\\) \\(0.0\\) Символы char 2 байта \\(0\\) \\(2^{16} - 1\\) \\(0\\) Логические bool 1 байт \\(\\text{false}\\) \\(\\text{true}\\) \\(\\text{false}\\)

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

        • В Python целочисленный тип int может иметь произвольный размер, ограниченный только доступной памятью; тип float является 64-битным числом двойной точности; типа char нет, а отдельный символ на деле является строкой str длины 1.
        • В C и C++ размер базовых типов данных явно не зафиксирован и зависит от реализации и платформы. таблица 3-1 соответствует модели данных LP64 data model, используемой в 64-битных Unix-системах, включая Linux и macOS.
        • Размер символа char в C и C++ составляет 1 байт, а в большинстве других языков программирования зависит от конкретного способа кодирования символов; подробнее это рассматривается в разделе \"Кодирование символов\".
        • Хотя для представления логического значения достаточно 1 бита ( \\(0\\) или \\(1\\) ), в памяти оно обычно хранится как 1 байт. Это связано с тем, что современные CPU обычно используют 1 байт как минимальную адресуемую единицу памяти.

        Какова же связь между базовыми типами данных и структурами данных? Мы знаем, что структура данных - это способ организации и хранения данных в компьютере. Подлежащее в этой фразе - \"структура\", а не \"данные\".

        Если мы хотим представить \"ряд чисел\", то естественно подумаем об использовании массива. Это связано с тем, что линейная структура массива может выразить отношения соседства и порядка между числами, а то, что именно хранится внутри - целые int , вещественные float или символы char , - к \"структуре данных\" отношения не имеет.

        Иными словами, базовые типы данных задают \"тип содержимого\" данных, а структуры данных задают \"способ организации\" данных. Например, в следующем коде мы используем одну и ту же структуру данных (массив) для хранения и представления различных базовых типов данных, включая int , float , char , bool и т.д.

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
        # Инициализируем массивы с использованием различных базовых типов данных\nnumbers: list[int] = [0] * 5\ndecimals: list[float] = [0.0] * 5\n# В Python символы фактически являются строками длины 1\ncharacters: list[str] = ['0'] * 5\nbools: list[bool] = [False] * 5\n# Списки Python могут свободно хранить различные базовые типы данных и ссылки на объекты\ndata = [0, 0.0, 'a', False, ListNode(0)]\n
        // Инициализируем массивы с использованием различных базовых типов данных\nint numbers[5];\nfloat decimals[5];\nchar characters[5];\nbool bools[5];\n
        // Инициализируем массивы с использованием различных базовых типов данных\nint[] numbers = new int[5];\nfloat[] decimals = new float[5];\nchar[] characters = new char[5];\nboolean[] bools = new boolean[5];\n
        // Инициализируем массивы с использованием различных базовых типов данных\nint[] numbers = new int[5];\nfloat[] decimals = new float[5];\nchar[] characters = new char[5];\nbool[] bools = new bool[5];\n
        // Инициализируем массивы с использованием различных базовых типов данных\nvar numbers = [5]int{}\nvar decimals = [5]float64{}\nvar characters = [5]byte{}\nvar bools = [5]bool{}\n
        // Инициализируем массивы с использованием различных базовых типов данных\nlet numbers = Array(repeating: 0, count: 5)\nlet decimals = Array(repeating: 0.0, count: 5)\nlet characters: [Character] = Array(repeating: \"a\", count: 5)\nlet bools = Array(repeating: false, count: 5)\n
        // Массивы JavaScript могут свободно хранить различные базовые типы данных и объекты\nconst array = [0, 0.0, 'a', false];\n
        // Инициализируем массивы с использованием различных базовых типов данных\nconst numbers: number[] = [];\nconst characters: string[] = [];\nconst bools: boolean[] = [];\n
        // Инициализируем массивы с использованием различных базовых типов данных\nList<int> numbers = List.filled(5, 0);\nList<double> decimals = List.filled(5, 0.0);\nList<String> characters = List.filled(5, 'a');\nList<bool> bools = List.filled(5, false);\n
        // Инициализируем массивы с использованием различных базовых типов данных\nlet numbers: Vec<i32> = vec![0; 5];\nlet decimals: Vec<f32> = vec![0.0; 5];\nlet characters: Vec<char> = vec!['0'; 5];\nlet bools: Vec<bool> = vec![false; 5];\n
        // Инициализируем массивы с использованием различных базовых типов данных\nint numbers[10];\nfloat decimals[10];\nchar characters[10];\nbool bools[10];\n
        // Инициализируем массивы с использованием различных базовых типов данных\nval numbers = IntArray(5)\nval decinals = FloatArray(5)\nval characters = CharArray(5)\nval bools = BooleanArray(5)\n
        # Списки Ruby могут свободно хранить различные базовые типы данных и ссылки на объекты\ndata = [0, 0.0, 'a', false, ListNode(0)]\n
        Визуализация выполнения

        https://pythontutor.com/render.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%D1%81%D0%B2%D1%8F%D0%B7%D0%BD%D1%8B%D0%B9%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%D1%83%D0%B7%D0%B5%D0%BB%D0%BA%D0%BB%D0%B0%D1%81%D1%81%22%22%22%0A%20%20%20%20def%20__init__%28self%2C%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%D0%97%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%D1%83%D0%B7%D0%BB%D0%B0%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%D0%A1%D1%81%D1%8B%D0%BB%D0%BA%D0%B0%20%D0%BD%D0%B0%20%D1%81%D0%BB%D0%B5%D0%B4%D1%83%D1%8E%D1%89%D0%B8%D0%B9%20%D1%83%D0%B7%D0%B5%D0%BB%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D0%BC%D0%B0%D1%81%D1%81%D0%B8%D0%B2%20%D1%81%20%D0%B8%D1%81%D0%BF%D0%BE%D0%BB%D1%8C%D0%B7%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5%D0%BC%20%D0%BD%D0%B5%D1%81%D0%BA%D0%BE%D0%BB%D1%8C%D0%BA%D0%B8%D1%85%20%D0%B1%D0%B0%D0%B7%D0%BE%D0%B2%D1%8B%D1%85%20%D1%82%D0%B8%D0%BF%D0%BE%D0%B2%20%D0%B4%D0%B0%D0%BD%D0%BD%D1%8B%D1%85%0A%20%20%20%20numbers%20%3D%20%5B0%5D%20%2A%205%0A%20%20%20%20decimals%20%3D%20%5B0.0%5D%20%2A%205%0A%20%20%20%20%23%20%D0%92%20Python%20%D1%81%D0%B8%D0%BC%D0%B2%D0%BE%D0%BB%D1%8B%20%D0%BD%D0%B0%20%D1%81%D0%B0%D0%BC%D0%BE%D0%BC%20%D0%B4%D0%B5%D0%BB%D0%B5%20%D1%8F%D0%B2%D0%BB%D1%8F%D1%8E%D1%82%D1%81%D1%8F%20%D1%81%D1%82%D1%80%D0%BE%D0%BA%D0%B0%D0%BC%D0%B8%20%D0%B4%D0%BB%D0%B8%D0%BD%D1%8B%201%0A%20%20%20%20characters%20%3D%20%5B%270%27%5D%20%2A%205%0A%20%20%20%20bools%20%3D%20%5BFalse%5D%20%2A%205%0A%20%20%20%20%23%20%D0%A1%D0%BF%D0%B8%D1%81%D0%BA%D0%B8%20%D0%B2%20Python%20%D0%BC%D0%BE%D0%B3%D1%83%D1%82%20%D1%81%D0%B2%D0%BE%D0%B1%D0%BE%D0%B4%D0%BD%D0%BE%20%D1%85%D1%80%D0%B0%D0%BD%D0%B8%D1%82%D1%8C%20%D1%80%D0%B0%D0%B7%D0%BB%D0%B8%D1%87%D0%BD%D1%8B%D0%B5%20%D0%B1%D0%B0%D0%B7%D0%BE%D0%B2%D1%8B%D0%B5%20%D1%82%D0%B8%D0%BF%D1%8B%20%D0%B4%D0%B0%D0%BD%D0%BD%D1%8B%D1%85%20%D0%B8%20%D1%81%D1%81%D1%8B%D0%BB%D0%BA%D0%B8%20%D0%BD%D0%B0%20%D0%BE%D0%B1%D1%8A%D0%B5%D0%BA%D1%82%D1%8B%0A%20%20%20%20data%20%3D%20%5B0%2C%200.0%2C%20%27a%27%2C%20False%2C%20ListNode%280%29%5D&cumulative=false&curInstr=12&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

        ","path":["Глава 3. Структуры данных","3.2   Базовые типы данных"],"tags":[]},{"location":"chapter_data_structure/character_encoding/","level":1,"title":"3.4   Кодирование символов *","text":"

        В компьютере все данные хранятся в виде двоичных чисел, и символьный тип данных char не является исключением. Для представления символов необходимо задать \"таблицу символов\", которая устанавливает взаимно-однозначное соответствие между каждым символом и двоичным числом. С помощью этой таблицы компьютер может преобразовывать двоичные числа в символы.

        ","path":["Глава 3. Структуры данных","3.4   Кодирование символов *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#341-ascii","level":2,"title":"3.4.1   Таблица символов ASCII","text":"

        Код ASCII - это самая ранняя таблица символов; ее полное название - American Standard Code for Information Interchange (американский стандартный код обмена информацией). Для представления символов в ней используются 7 двоичных битов (нижние 7 битов одного байта), что позволяет закодировать до 128 различных символов. Как показано на рисунке 3-6, ASCII включает заглавные и строчные буквы английского алфавита, цифры 0 ~ 9, некоторые знаки препинания, а также некоторые управляющие символы (например перевод строки и табуляцию).

        Рисунок 3-6   Таблица ASCII

        Однако код ASCII может представлять только английский язык. С развитием компьютерных технологий появилась таблица символов EASCII, способная охватывать больше языков. Она расширяет 7-битную основу ASCII до 8 битов и может представлять 256 различных символов.

        Во всем мире постепенно появились разные таблицы EASCII, подходящие для разных регионов. Первые 128 символов в этих таблицах одинаковы и соответствуют ASCII, а последние 128 символов определяются по-разному, чтобы удовлетворять потребностям разных языков.

        ","path":["Глава 3. Структуры данных","3.4   Кодирование символов *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#342-gbk","level":2,"title":"3.4.2   Таблица символов GBK","text":"

        Позже люди обнаружили, что кодов EASCII все равно недостаточно для количества символов во многих языках. Например, китайских иероглифов существует почти сто тысяч, а в повседневном употреблении нужны тысячи. В 1980 году Государственное управление стандартов Китая выпустило таблицу символов GB2312, включающую 6763 иероглифа, что в основном удовлетворило потребности компьютерной обработки китайского текста.

        Однако GB2312 не умеет работать с некоторыми редкими иероглифами и традиционными формами письма. Таблица символов GBK представляет собой расширение GB2312 и в общей сложности содержит 21886 иероглифов. В схеме кодирования GBK символы ASCII представляются одним байтом, а китайские иероглифы - двумя байтами.

        ","path":["Глава 3. Структуры данных","3.4   Кодирование символов *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#343-unicode","level":2,"title":"3.4.3   Таблица символов Unicode","text":"

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

        Исследователи той эпохи задумались: если создать достаточно полную таблицу символов, которая включит все языки и знаки мира, разве это не решит проблемы многоязычной среды и искаженного текста? Под влиянием этой идеи и появилась большая и всеобъемлющая таблица символов Unicode.

        Unicode по-китайски называется \"единый код\" и теоретически способен вместить более миллиона символов. Его цель - собрать символы со всего мира в единую таблицу символов, предоставить универсальный стандарт для обработки и отображения текстов на разных языках и уменьшить количество проблем с искажением текста, вызванных различиями стандартов кодирования. С момента публикации в 1991 году Unicode непрерывно расширялся, добавляя новые языки и символы. По состоянию на сентябрь 2022 года Unicode уже включал 149186 символов, в том числе буквы разных языков, знаки, а также эмодзи.

        Как универсальный набор символов, Unicode по сути присваивает каждому символу уникальную \"кодовую точку\" (числовой идентификатор символа), диапазон которой составляет от U+0000 до U+10FFFF, образуя единое пространство нумерации символов. Однако Unicode не определяет, как именно хранить эти кодовые точки в компьютере. Тут неизбежно возникает вопрос: если в одном тексте одновременно встречаются кодовые точки Unicode разной длины, как система должна разбирать символы? Например, если дан код длиной 2 байта, как понять, является ли это одним 2-байтовым символом или двумя 1-байтовыми?

        Для этой проблемы прямолинейное решение состоит в том, чтобы хранить все символы в кодировке одинаковой длины. Как показано на рисунке 3-7, каждый символ в \"Hello\" занимает 1 байт, а каждый символ в \"алгоритм\" занимает 2 байта. Мы можем дополнить старшие биты нулями и закодировать все символы в \"Hello алгоритм\" в виде 2-байтовых единиц. Тогда система сможет считывать по одному символу каждые 2 байта и восстановить эту фразу.

        Рисунок 3-7   Пример кодирования Unicode

        Однако ASCII уже показал нам, что для кодирования английского текста достаточно 1 байта. Если использовать описанную выше схему, английский текст будет занимать вдвое больше памяти, чем при ASCII, а это очень неэффективно. Поэтому нам нужен более эффективный способ кодирования Unicode.

        ","path":["Глава 3. Структуры данных","3.4   Кодирование символов *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#344-utf-8","level":2,"title":"3.4.4   Кодировка UTF-8","text":"

        Сегодня UTF-8 стала самым широко используемым способом кодирования Unicode в мире. Это кодировка переменной длины, использующая от 1 до 4 байт на символ в зависимости от его сложности. Символам ASCII нужен только 1 байт, латинским и греческим буквам - 2 байта, часто используемым китайским символам - 3 байта, а некоторым редким символам - 4 байта.

        Правила кодирования UTF-8 не слишком сложны и делятся на два случая.

        • Для символов длиной 1 байт старший бит устанавливается в \\(0\\) , а оставшиеся 7 битов содержат кодовую точку Unicode. Стоит отметить, что символы ASCII занимают первые 128 кодовых точек в наборе Unicode. Иными словами, кодировка UTF-8 обратно совместима с ASCII. Это означает, что мы можем использовать UTF-8 для разбора очень старых ASCII-текстов.
        • Для символов длиной \\(n\\) байт (где \\(n > 1\\)) старшие \\(n\\) битов первого байта устанавливаются в \\(1\\) , а \\((n + 1)\\)-й бит устанавливается в \\(0\\) ; начиная со второго байта, старшие 2 бита каждого байта устанавливаются в \\(10\\) ; все остальные биты используются для заполнения кодовой точки Unicode соответствующего символа.

        На рисунке 3-8 показана UTF-8-кодировка для строки \"Hello алгоритм\". Можно заметить, что поскольку старшие \\(n\\) битов установлены в \\(1\\) , система может определить длину символа как \\(n\\) , подсчитав число ведущих единиц.

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

        Причина выбора \\(10\\) в качестве контрольного маркера в том, что по правилам UTF-8 символ не может иметь старшие два бита, равные \\(10\\) . Это можно доказать от противного: если предположить, что у некоторого символа старшие два бита равны \\(10\\) , то длина такого символа должна быть 1 байт, то есть это ASCII. Но у ASCII старший бит обязан быть \\(0\\) , что противоречит предположению.

        Рисунок 3-8   Пример кодировки UTF-8

        Помимо UTF-8, распространены еще два следующих способа кодирования.

        • Кодировка UTF-16: использует 2 или 4 байта для представления символа. Все символы ASCII и часто используемые неанглийские символы представляются 2 байтами; небольшая часть символов требует 4 байта. Для 2-байтовых символов кодировка UTF-16 совпадает с кодовой точкой Unicode.
        • Кодировка UTF-32: каждый символ занимает 4 байта. Это означает, что UTF-32 требует больше места, чем UTF-8 и UTF-16, особенно в текстах с большой долей ASCII-символов.

        С точки зрения занимаемого места UTF-8 очень эффективна для английских символов, потому что им нужен всего 1 байт; а для некоторых неанглийских символов (например китайских) UTF-16 может быть эффективнее, потому что ей требуется только 2 байта, тогда как UTF-8 может потребовать 3 байта.

        С точки зрения совместимости у UTF-8 наилучшая универсальность, и многие инструменты и библиотеки в первую очередь поддерживают именно UTF-8.

        ","path":["Глава 3. Структуры данных","3.4   Кодирование символов *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#345","level":2,"title":"3.4.5   Кодирование символов в языках программирования","text":"

        Для большинства языков программирования прошлого строки во время выполнения программы использовали фиксированные по длине кодировки, такие как UTF-16 или UTF-32. При кодировке фиксированной длины строку можно обрабатывать как массив, и такой подход дает следующие преимущества.

        • Произвольный доступ: к строкам в UTF-16 легко осуществлять произвольный доступ. UTF-8 же является кодировкой переменной длины, поэтому, чтобы найти \\(i\\) -й символ, нужно пройти от начала строки до этого символа, а это требует \\(O(n)\\) времени.
        • Подсчет длины строки: аналогично произвольному доступу, вычисление длины строки в UTF-16 - это операция \\(O(1)\\) . А вот вычисление длины строки в UTF-8 требует обхода всей строки.
        • Строковые операции: многие операции со строками (разделение, конкатенация, вставка, удаление и т.д.) над строками в UTF-16 реализуются проще. При работе с UTF-8 обычно требуются дополнительные вычисления, чтобы не породить некорректную UTF-8-последовательность.

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

        • Тип String в Java использует кодировку UTF-16, и каждый символ занимает 2 байта. Это связано с тем, что на раннем этапе проектирования Java считалось, что 16 битов достаточно для представления всех возможных символов. Но это оказалось неверным предположением. Позднее Unicode вышел за пределы 16 битов, поэтому символы в Java теперь могут представляться парой 16-битных значений (так называемой \"суррогатной парой\").
        • Строки в JavaScript и TypeScript используют UTF-16 по причинам, похожим на Java. Когда Netscape впервые выпустила JavaScript в 1995 году, Unicode еще находился на ранней стадии развития, и 16-битного кодирования тогда было достаточно для представления всех символов Unicode.
        • C# использует UTF-16 главным образом потому, что платформа .NET была разработана Microsoft, а многие технологии Microsoft (включая Windows) широко используют именно UTF-16.

        Из-за недооценки общего числа символов перечисленным выше языкам пришлось использовать \"суррогатные пары\" для представления Unicode-символов длиной больше 16 бит. Это вынужденный компромисс. С одной стороны, в строках с суррогатными парами один символ может занимать 2 байта или 4 байта, из-за чего теряется преимущество кодировки фиксированной длины. С другой стороны, обработка суррогатных пар требует дополнительного кода, что повышает сложность разработки и отладки.

        По этим причинам некоторые языки программирования предложили иные схемы кодирования.

        • str в Python использует Unicode и гибкое строковое представление, где длина хранимого символа зависит от наибольшей кодовой точки Unicode в строке. Если все символы строки принадлежат ASCII, каждый символ занимает 1 байт; если есть символы за пределами ASCII, но все они лежат в базовой многоязычной плоскости (BMP), каждый символ занимает 2 байта; если встречаются символы за пределами BMP, каждый символ занимает 4 байта.
        • Тип string в Go внутри использует кодировку UTF-8. Язык Go также предоставляет тип rune, предназначенный для представления одной кодовой точки Unicode.
        • Типы str и String в Rust внутри используют UTF-8. В Rust также есть тип char, представляющий одну кодовую точку Unicode.

        Следует помнить, что выше обсуждался способ хранения строк внутри языков программирования, а это не то же самое, что хранение строк в файлах или передача их по сети. При файловом хранении и сетевой передаче мы обычно кодируем строки в формате UTF-8, чтобы получить наилучшую совместимость и эффективность по занимаемому месту.

        ","path":["Глава 3. Структуры данных","3.4   Кодирование символов *"],"tags":[]},{"location":"chapter_data_structure/classification_of_data_structure/","level":1,"title":"3.1   Классификация структур данных","text":"

        К распространенным структурам данных относятся массивы, связные списки, стеки, очереди, хеш-таблицы, деревья, кучи и графы. Их можно классифицировать по двум измерениям: логической структуре и физической структуре.

        ","path":["Глава 3. Структуры данных","3.1   Классификация структур данных"],"tags":[]},{"location":"chapter_data_structure/classification_of_data_structure/#311","level":2,"title":"3.1.1   Логическая структура: линейная и нелинейная","text":"

        Логическая структура раскрывает логические отношения между элементами данных. В массивах и связных списках данные расположены в определенном порядке, что отражает линейные отношения между элементами. В деревьях данные расположены по уровням сверху вниз, что демонстрирует отношения \"предок\" и \"потомок\". Графы состоят из вершин и ребер, отражая сложные сетевые отношения.

        Как показано на рисунке 3-1, логические структуры делятся на две большие категории: линейные и нелинейные. Линейные структуры более интуитивны, поскольку в них данные расположены линейно и логически связаны. Нелинейные структуры, напротив, представляют собой нелинейное расположение элементов данных.

        • Линейные структуры данных: массивы, связные списки, стеки, очереди, хеш-таблицы, в которых элементы связаны отношением \"один к одному\".
        • Нелинейные структуры данных: деревья, кучи, графы, хеш-таблицы.

        Нелинейные структуры данных можно дополнительно разделить на древовидные и сетевые.

        • Древовидные структуры: деревья, кучи, хеш-таблицы, в которых элементы связаны отношением \"один ко многим\".
        • Сетевые структуры: графы, в которых элементы связаны отношением \"многие ко многим\".

        Рисунок 3-1   Линейные и нелинейные структуры данных

        ","path":["Глава 3. Структуры данных","3.1   Классификация структур данных"],"tags":[]},{"location":"chapter_data_structure/classification_of_data_structure/#312","level":2,"title":"3.1.2   Физическая структура: непрерывная и разрозненная","text":"

        Во время выполнения программы обрабатываемые данные в основном хранятся в памяти. На рисунке 3-2 показан модуль оперативной памяти компьютера, где каждый черный блок содержит определенный участок памяти. Память можно представить как огромную таблицу Excel, в которой каждая ячейка способна хранить данные определенного размера.

        Система обращается к данным по адресам памяти соответствующих позиций. Как показано на рисунке 3-2, компьютер по определенным правилам присваивает каждой ячейке в этой таблице номер, чтобы каждый участок памяти имел уникальный адрес. Благодаря этим адресам программа получает доступ к данным, находящимся в памяти.

        Рисунок 3-2   Планка памяти, участок памяти и адрес памяти

        Tip

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

        Память - общий ресурс для всех программ. Когда некоторый участок памяти занят одной программой, другие программы обычно не могут использовать его одновременно. Поэтому при проектировании структур данных и алгоритмов память занимает важное место. Например, пиковое потребление памяти алгоритмом не должно превышать объем доступной свободной памяти системы; если не хватает непрерывных крупных участков памяти, выбранная структура данных должна уметь размещаться в разрозненных областях памяти.

        Как показано на рисунке 3-3, физическая структура отражает способ хранения данных в памяти компьютера. Ее можно разделить на хранение в непрерывном пространстве (массивы) и хранение в разрозненном пространстве (связные списки). Физическая структура на низком уровне определяет способы доступа к данным, их обновления, вставки и удаления. Эти два типа физических структур взаимно дополняют друг друга по временной и пространственной эффективности.

        Рисунок 3-3   Хранение в непрерывном и разрозненном пространстве

        Стоит отметить, что все структуры данных реализуются на основе массивов, связных списков или их комбинации. Например, стек и очередь можно реализовать как с помощью массивов, так и с помощью связных списков; реализация хеш-таблицы также может одновременно включать массивы и связные списки.

        • Можно реализовать на основе массивов: стеки, очереди, хеш-таблицы, деревья, кучи, графы, матрицы, тензоры (массивы размерности \\(\\geq 3\\) ) и т.д.
        • Можно реализовать на основе связных списков: стеки, очереди, хеш-таблицы, деревья, кучи, графы и т.д.

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

        Tip

        Если тебе пока трудно понять физическую структуру, рекомендуется сначала прочитать следующую главу, а затем вернуться к этому разделу.

        ","path":["Глава 3. Структуры данных","3.1   Классификация структур данных"],"tags":[]},{"location":"chapter_data_structure/number_encoding/","level":1,"title":"3.3   Кодирование чисел *","text":"

        Tip

        В этой книге разделы, помеченные символом *, относятся к дополнительному чтению. Если у тебя мало времени или материал кажется трудным, можно сначала пропустить их и вернуться после изучения обязательных разделов.

        ","path":["Глава 3. Структуры данных","3.3   Кодирование чисел *"],"tags":[]},{"location":"chapter_data_structure/number_encoding/#331","level":2,"title":"3.3.1   Прямой, обратный и дополнительный коды","text":"

        В таблице из предыдущего раздела можно заметить, что все целочисленные типы могут представлять на одно отрицательное число больше, чем положительных. Например, диапазон byte равен \\([-128, 127]\\) . Это выглядит не слишком интуитивно, и внутренняя причина связана с прямым, обратным и дополнительным кодами.

        Прежде всего нужно отметить, что числа хранятся в компьютере в виде \"дополнительного кода\". Прежде чем разбирать причины такого решения, сначала дадим определения всем трем способам представления.

        • Прямой код: старший бит двоичного представления числа рассматривается как знаковый, где \\(0\\) означает положительное число, а \\(1\\) - отрицательное; остальные биты представляют значение числа.
        • Обратный код: для положительного числа обратный код совпадает с прямым; для отрицательного числа он получается инверсией всех битов прямого кода, кроме знакового бита.
        • Дополнительный код: для положительного числа дополнительный код совпадает с прямым; для отрицательного числа он получается добавлением \\(1\\) к его обратному коду.

        На рисунке 3-4 показаны способы преобразования между прямым, обратным и дополнительным кодами.

        Рисунок 3-4   Преобразования между прямым, обратным и дополнительным кодами

        Прямой код (sign-magnitude), хотя и является самым наглядным, имеет определенные ограничения. С одной стороны, прямой код отрицательных чисел нельзя напрямую использовать в вычислениях. Например, при вычислении \\(1 + (-2)\\) в прямом коде результатом будет \\(-3\\) , что, очевидно, неверно.

        \\[ \\begin{aligned} & 1 + (-2) \\newline & \\rightarrow 0000 \\; 0001 + 1000 \\; 0010 \\newline & = 1000 \\; 0011 \\newline & \\rightarrow -3 \\end{aligned} \\]

        Чтобы решить эту проблему, компьютеры ввели обратный код (1's complement). Если сначала преобразовать прямой код в обратный и выполнить вычисление \\(1 + (-2)\\) в обратном коде, а затем перевести результат обратно в прямой код, то получится правильный результат \\(-1\\) .

        \\[ \\begin{aligned} & 1 + (-2) \\newline & \\rightarrow 0000 \\; 0001 \\; \\text{(прямой код)} + 1000 \\; 0010 \\; \\text{(прямой код)} \\newline & = 0000 \\; 0001 \\; \\text{(обратный код)} + 1111 \\; 1101 \\; \\text{(обратный код)} \\newline & = 1111 \\; 1110 \\; \\text{(обратный код)} \\newline & = 1000 \\; 0001 \\; \\text{(прямой код)} \\newline & \\rightarrow -1 \\end{aligned} \\]

        С другой стороны, **в прямом коде у нуля есть два представления: \\(+0\\) и \\(-0\\) **. Это означает, что числу ноль соответствуют два разных двоичных кода, что может приводить к неоднозначности. Например, если в условном выражении не различать положительный и отрицательный ноль, можно получить ошибочный результат. А если специально обрабатывать такую неоднозначность, придется вводить дополнительные проверки, что может снизить вычислительную эффективность компьютера.

        \\[ \\begin{aligned} +0 & \\rightarrow 0000 \\; 0000 \\newline -0 & \\rightarrow 1000 \\; 0000 \\end{aligned} \\]

        Как и прямой код, обратный код тоже страдает от неоднозначности положительного и отрицательного нуля, поэтому компьютеры ввели дополнительный код (2's complement). Сначала посмотрим на процесс преобразования отрицательного нуля из прямого кода в обратный, а затем в дополнительный:

        \\[ \\begin{aligned} -0 \\rightarrow \\; & 1000 \\; 0000 \\; \\text{(прямой код)} \\newline = \\; & 1111 \\; 1111 \\; \\text{(обратный код)} \\newline = 1 \\; & 0000 \\; 0000 \\; \\text{(дополнительный код)} \\newline \\end{aligned} \\]

        При добавлении \\(1\\) к обратному коду отрицательного нуля возникает перенос, но длина типа byte составляет всего 8 бит, поэтому переполнившаяся в 9-й бит единица отбрасывается. Иными словами, дополнительный код отрицательного нуля равен \\(0000 \\; 0000\\) и совпадает с дополнительным кодом положительного нуля. Значит, в представлении дополнительного кода существует только один ноль, и проблема неоднозначности положительного и отрицательного нуля тем самым устраняется.

        Остается последний вопрос: диапазон типа byte равен \\([-128, 127]\\) , откуда берется лишнее отрицательное число \\(-128\\) ? Мы замечаем, что у всех целых чисел из интервала \\([-127, +127]\\) есть соответствующие прямой, обратный и дополнительный коды, а прямой и дополнительный коды можно преобразовывать друг в друга.

        Однако дополнительный код \\(1000 \\; 0000\\) является исключением: у него нет соответствующего прямого кода. Согласно правилу преобразования, прямой код для этого дополнительного кода должен быть равен \\(0000 \\; 0000\\) . Это очевидное противоречие, потому что такой прямой код обозначает число \\(0\\) , а его дополнительный код должен совпадать с ним самим. Компьютер просто определяет, что этот особый дополнительный код \\(1000 \\; 0000\\) представляет число \\(-128\\) . На самом деле результат вычисления \\((-1) + (-127)\\) в дополнительном коде как раз и равен \\(-128\\) .

        \\[ \\begin{aligned} & (-127) + (-1) \\newline & \\rightarrow 1111 \\; 1111 \\; \\text{(прямой код)} + 1000 \\; 0001 \\; \\text{(прямой код)} \\newline & = 1000 \\; 0000 \\; \\text{(обратный код)} + 1111 \\; 1110 \\; \\text{(обратный код)} \\newline & = 1000 \\; 0001 \\; \\text{(дополнительный код)} + 1111 \\; 1111 \\; \\text{(дополнительный код)} \\newline & = 1000 \\; 0000 \\; \\text{(дополнительный код)} \\newline & \\rightarrow -128 \\end{aligned} \\]

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

        Обрати внимание: это не означает, что компьютер умеет только складывать. Комбинируя сложение с некоторыми базовыми логическими операциями, компьютер может реализовать и другие математические операции. Например, вычитание \\(a - b\\) можно преобразовать в сложение \\(a + (-b)\\) ; умножение и деление можно свести к многократному сложению или вычитанию.

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

        Идея дополнительного кода очень изящна; из-за ограничений по объему мы на этом остановимся. Если тебе интересно, стоит изучить эту тему глубже.

        ","path":["Глава 3. Структуры данных","3.3   Кодирование чисел *"],"tags":[]},{"location":"chapter_data_structure/number_encoding/#332","level":2,"title":"3.3.2   Кодирование чисел с плавающей точкой","text":"

        Внимательный читатель может заметить: int и float имеют одинаковую длину, по 4 байта , но почему диапазон значений у float намного больше, чем у int ? Это выглядит парадоксально, ведь float должен еще представлять дробные числа, а значит диапазон вроде бы должен быть меньше.

        На самом деле это связано с тем, что число с плавающей точкой float использует другой способ представления. Обозначим двоичное число длиной 32 бита как:

        \\[ b_{31} b_{30} b_{29} \\ldots b_2 b_1 b_0 \\]

        Согласно стандарту IEEE 754, 32-битный float состоит из следующих трех частей.

        • Бит знака \\(\\mathrm{S}\\) : занимает 1 бит и соответствует \\(b_{31}\\) .
        • Биты экспоненты \\(\\mathrm{E}\\) : занимают 8 бит и соответствуют \\(b_{30} b_{29} \\ldots b_{23}\\) .
        • Биты мантиссы \\(\\mathrm{N}\\) : занимают 23 бита и соответствуют \\(b_{22} b_{21} \\ldots b_0\\) .

        Формула вычисления значения, соответствующего двоичному числу float, имеет вид:

        \\[ \\text {val} = (-1)^{b_{31}} \\times 2^{\\left(b_{30} b_{29} \\ldots b_{23}\\right)_2-127} \\times\\left(1 . b_{22} b_{21} \\ldots b_0\\right)_2 \\]

        Если перейти к десятичной записи, формула вычисления будет такой:

        \\[ \\text {val}=(-1)^{\\mathrm{S}} \\times 2^{\\mathrm{E} -127} \\times (1 + \\mathrm{N}) \\]

        Диапазоны значений соответствующих частей таковы:

        \\[ \\begin{aligned} \\mathrm{S} \\in & \\{ 0, 1\\}, \\quad \\mathrm{E} \\in \\{ 1, 2, \\dots, 254 \\} \\newline (1 + \\mathrm{N}) = & (1 + \\sum_{i=1}^{23} b_{23-i} 2^{-i}) \\subset [1, 2 - 2^{-23}] \\end{aligned} \\]

        Рисунок 3-5   Пример вычисления float по стандарту IEEE 754

        Посмотрим на рисунок 3-5: если взять пример \\(\\mathrm{S} = 0\\) , \\(\\mathrm{E} = 124\\) , \\(\\mathrm{N} = 2^{-2} + 2^{-3} = 0.375\\) , то получим:

        \\[ \\text { val } = (-1)^0 \\times 2^{124 - 127} \\times (1 + 0.375) = 0.171875 \\]

        Теперь мы можем ответить на исходный вопрос: в представлении float присутствуют биты экспоненты, поэтому его диапазон значений намного больше, чем у int. Согласно приведенным выше вычислениям, максимально возможное положительное число для float равно \\(2^{254 - 127} \\times (2 - 2^{-23}) \\approx 3.4 \\times 10^{38}\\) ; если изменить бит знака, получим минимальное отрицательное число.

        Хотя число с плавающей точкой float расширяет диапазон значений, побочным эффектом становится потеря точности. Целочисленный тип int использует все 32 бита для представления числа, и числа распределены равномерно; а из-за существования битов экспоненты у float чем больше число, тем больше обычно становится разница между двумя соседними представимыми значениями.

        Как показано в таблице 3-2, значения экспоненты \\(\\mathrm{E} = 0\\) и \\(\\mathrm{E} = 255\\) имеют специальный смысл и используются для представления нуля, бесконечности, \\(\\mathrm{NaN}\\) и т.д.

        Таблица 3-2   Значение поля экспоненты

        Поле экспоненты E Поле мантиссы \\(\\mathrm{N} = 0\\) Поле мантиссы \\(\\mathrm{N} \\ne 0\\) Формула вычисления \\(0\\) \\(\\pm 0\\) Денормализованное число \\((-1)^{\\mathrm{S}} \\times 2^{-126} \\times (0.\\mathrm{N})\\) \\(1, 2, \\dots, 254\\) Нормализованное число Нормализованное число \\((-1)^{\\mathrm{S}} \\times 2^{(\\mathrm{E} -127)} \\times (1.\\mathrm{N})\\) \\(255\\) \\(\\pm \\infty\\) \\(\\mathrm{NaN}\\)

        Стоит отметить, что денормализованные числа заметно повышают точность чисел с плавающей точкой. Наименьшее положительное нормализованное число равно \\(2^{-126}\\) , а наименьшее положительное денормализованное число равно \\(2^{-126} \\times 2^{-23}\\) .

        Двойная точность double использует способ представления, аналогичный float , поэтому здесь мы не будем подробно останавливаться на нем.

        ","path":["Глава 3. Структуры данных","3.3   Кодирование чисел *"],"tags":[]},{"location":"chapter_data_structure/summary/","level":1,"title":"3.5   Резюме","text":"","path":["Глава 3. Структуры данных","3.5   Резюме"],"tags":[]},{"location":"chapter_data_structure/summary/#1","level":3,"title":"1.   Ключевые выводы","text":"
        • Структуры данных можно классифицировать с точки зрения логической и физической структуры. Логическая структура описывает логические отношения между элементами данных, а физическая структура описывает способ хранения данных в памяти компьютера.
        • К распространенным логическим структурам относятся линейные, древовидные и сетевые. Обычно структуры данных делятся на линейные (массивы, связные списки, стеки, очереди) и нелинейные (деревья, графы, кучи). Реализация хеш-таблицы может включать как линейные, так и нелинейные структуры данных.
        • При выполнении программы данные хранятся в памяти компьютера. Каждый участок памяти имеет соответствующий адрес, с помощью которого программа получает доступ к данным.
        • Физическая структура делится на хранение в непрерывном пространстве (массивы) и хранение в разрозненном пространстве (связные списки). Все структуры данных реализуются на основе массивов, связных списков или их комбинации.
        • Базовые типы данных в компьютере включают целые byte , short , int , long , числа с плавающей точкой float , double , символы char и логический тип bool . Их диапазон значений зависит от объема занимаемого пространства и способа представления.
        • Прямой код, обратный код и дополнительный код - это три способа кодирования чисел в компьютере, между которыми можно выполнять взаимные преобразования. В прямом коде старший бит целого числа является знаковым, а остальные биты представляют значение числа.
        • Целые числа в компьютере хранятся в виде дополнительного кода. В таком представлении компьютер может одинаково обрабатывать сложение положительных и отрицательных чисел без специальной аппаратной схемы для вычитания, и при этом исчезает неоднозначность положительного и отрицательного нуля.
        • Кодирование числа с плавающей точкой состоит из 1 бита знака, 8 битов экспоненты и 23 битов мантиссы. Благодаря наличию экспоненты диапазон значений у чисел с плавающей точкой намного больше, чем у целых, но это достигается ценой потери точности.
        • ASCII - это самый ранний набор английских символов длиной 1 байт, включающий в общей сложности 127 символов. Набор GBK - распространенный китайский набор символов, включающий более двадцати тысяч иероглифов. Unicode стремится предоставить единый полный стандарт набора символов, включающий символы всех языков мира, чтобы решить проблемы искаженного текста, вызванные несовместимыми способами кодирования.
        • UTF-8 - самый популярный способ кодирования Unicode, обладающий очень хорошей универсальностью. Это кодировка переменной длины, хорошо расширяемая и эффективно использующая память. UTF-16 и UTF-32 относятся к кодировкам фиксированной длины. При кодировании китайского текста UTF-16 занимает меньше места, чем UTF-8. Такие языки программирования, как Java и C#, по умолчанию используют UTF-16.
        ","path":["Глава 3. Структуры данных","3.5   Резюме"],"tags":[]},{"location":"chapter_data_structure/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

        Q: Почему хеш-таблица одновременно включает линейные и нелинейные структуры данных?

        В основе хеш-таблицы лежит массив, а для разрешения коллизий мы можем использовать \"цепочки адресации\" (об этом будет рассказано в последующем разделе \"Хеш-коллизии\"): каждый бакет массива указывает на связный список, а если длина списка превышает некоторый порог, он может быть преобразован в дерево (обычно в красно-черное дерево).

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

        Q: Длина типа char равна 1 байту?

        Длина типа char определяется используемым в языке программирования способом кодирования. Например, Java, JavaScript, TypeScript и C# используют кодировку UTF-16 (для хранения кодовых точек Unicode), поэтому длина char у них равна 2 байтам.

        Q: Не является ли двусмысленным утверждение, что структуры данных, реализованные на основе массива, также называются \"статическими структурами данных\"? Ведь стек тоже поддерживает операции push и pop, а они явно \"динамические\".

        Стек действительно может поддерживать динамические операции над данными, но сама структура данных при этом остается \"статической\" (ее длина неизменна). Хотя структуры на основе массива могут динамически добавлять и удалять элементы, их емкость фиксирована. Если количество данных превышает заранее выделенный размер, приходится создавать новый, более крупный массив и копировать в него содержимое старого.

        Q: При построении стека (очереди) его размер не задается явно, почему же его относят к \"статическим структурам данных\"?

        В языках высокого уровня нам не нужно вручную задавать начальную емкость стека (очереди): это автоматически делает сама реализация класса. Например, начальная емкость ArrayList в Java обычно равна 10. Кроме того, автоматом реализуется и расширение емкости. Подробнее это рассматривается в последующем разделе о \"списках\".

        Q: Если метод преобразования из прямого кода в дополнительный - это \"сначала инвертировать, затем прибавить 1\", то обратное преобразование из дополнительного кода в прямой, по идее, должно быть обратной операцией \"сначала вычесть 1, затем инвертировать\". Почему же дополнительный код также можно перевести в прямой тем же способом \"сначала инвертировать, затем прибавить 1\"?

        Это связано с тем, что взаимное преобразование прямого и дополнительного кодов по сути является вычислением \"дополнения\". Сначала дадим определение дополнения: если \\(a + b = c\\) , то говорят, что \\(a\\) является дополнением числа \\(b\\) до \\(c\\) ; аналогично, \\(b\\) является дополнением числа \\(a\\) до \\(c\\) .

        Для двоичного числа длины \\(n = 4\\) со значением \\(0010\\) , если рассматривать его как прямой код (не учитывая знаковый бит), то его дополнительный код получается правилом \"сначала инвертировать, затем прибавить 1\":

        \\[ 0010 \\rightarrow 1101 \\rightarrow 1110 \\]

        Мы видим, что сумма прямого и дополнительного кодов равна \\(0010 + 1110 = 10000\\) , то есть дополнительный код \\(1110\\) является \"дополнением\" прямого кода \\(0010\\) до \\(10000\\) . **Это означает, что описанная выше операция \"сначала инвертировать, затем прибавить 1\" на самом деле вычисляет дополнение до \\(10000\\) **.

        Тогда чему равно \"дополнение\" дополнительного кода \\(1110\\) до \\(10000\\) ? Мы снова можем получить его правилом \"сначала инвертировать, затем прибавить 1\":

        \\[ 1110 \\rightarrow 0001 \\rightarrow 0010 \\]

        Иначе говоря, прямой и дополнительный коды являются взаимными \"дополнениями\" друг друга до \\(10000\\) , поэтому и \"прямой код -> дополнительный код\", и \"дополнительный код -> прямой код\" можно реализовать одной и той же операцией (сначала инвертировать, затем прибавить 1).

        Разумеется, можно получить прямой код из дополнительного кода \\(1110\\) и обратной операцией, то есть \"сначала вычесть 1, затем инвертировать\":

        \\[ 1110 \\rightarrow 1101 \\rightarrow 0010 \\]

        В итоге и \"сначала инвертировать, затем прибавить 1\", и \"сначала вычесть 1, затем инвертировать\" - это два эквивалентных способа вычисления дополнения до \\(10000\\) .

        По сути операция \"инвертировать\" сама по себе вычисляет дополнение до \\(1111\\) (потому что всегда выполняется прямой код + обратный код = 1111 ); а дополнительный код, получающийся после добавления 1 к обратному коду, и есть дополнение до \\(10000\\) .

        Приведенный выше пример использовал \\(n = 4\\) , но его можно обобщить на двоичные числа любой длины.

        ","path":["Глава 3. Структуры данных","3.5   Резюме"],"tags":[]},{"location":"chapter_divide_and_conquer/","level":1,"title":"Глава 12.   Разделяй и властвуй","text":"

        Abstract

        Сложная задача раскладывается слой за слоем, и каждое новое разбиение делает ее проще.

        Принцип \"разделяй и властвуй\" показывает важный факт: если начать с простого, многое перестает быть сложным.

        ","path":["Глава 12. Разделяй и властвуй","Глава 12.   Разделяй и властвуй"],"tags":[]},{"location":"chapter_divide_and_conquer/#_1","level":2,"title":"Содержание главы","text":"
        • 12.1   Стратегия разделяй и властвуй
        • 12.2   Поисковая стратегия разделяй и властвуй
        • 12.3   Задача построения двоичного дерева
        • 12.4   Задача о Ханойской башне
        • 12.5   Резюме
        ","path":["Глава 12. Разделяй и властвуй","Глава 12.   Разделяй и властвуй"],"tags":[]},{"location":"chapter_divide_and_conquer/binary_search_recur/","level":1,"title":"12.2   Поисковая стратегия разделяй и властвуй","text":"

        Мы уже знаем, что алгоритмы поиска делятся на две большие категории.

        • Полный перебор: реализуется через обход структуры данных, временная сложность равна \\(O(n)\\) .
        • Адаптивный поиск: использует особую организацию данных или априорную информацию, временная сложность может достигать \\(O(\\log n)\\) и даже \\(O(1)\\) .

        На практике алгоритмы поиска с временной сложностью \\(O(\\log n)\\) обычно реализуются на основе стратегии \"разделяй и властвуй\", например двоичный поиск и деревья.

        • На каждом шаге двоичный поиск раскладывает задачу (поиск целевого элемента в массиве) на более мелкую задачу (поиск целевого элемента в одной половине массива), и этот процесс продолжается, пока массив не станет пустым или пока не будет найден целевой элемент.
        • Деревья являются типичными представителями идей \"разделяй и властвуй\"; в таких структурах данных, как двоичное дерево поиска, AVL-дерево и куча, временная сложность различных операций равна \\(O(\\log n)\\) .

        Стратегия \"разделяй и властвуй\" для двоичного поиска выглядит следующим образом.

        • Задача раскладывается на части: двоичный поиск рекурсивно разбивает исходную задачу (поиск в массиве) на подзадачу (поиск в одной половине массива), и это достигается сравнением среднего элемента с целевым значением.
        • Подзадачи независимы: в двоичном поиске на каждом шаге обрабатывается только одна подзадача, и она не зависит от других подзадач.
        • Решения подзадач не нужно объединять: двоичный поиск нацелен на поиск конкретного элемента, поэтому объединять решения подзадач не требуется. Как только подзадача решена, одновременно считается решенной и исходная задача.

        Иными словами, стратегия \"разделяй и властвуй\" повышает эффективность поиска потому, что при полном переборе за один шаг удается исключить только один вариант, тогда как при поиске на основе \"разделяй и властвуй\" за один шаг можно исключить половину вариантов.

        ","path":["Глава 12. Разделяй и властвуй","12.2   Поисковая стратегия разделяй и властвуй"],"tags":[]},{"location":"chapter_divide_and_conquer/binary_search_recur/#1","level":3,"title":"1.   Реализация двоичного поиска на основе \"разделяй и властвуй\"","text":"

        В предыдущих главах двоичный поиск реализовывался через итерацию. Теперь реализуем его с помощью стратегии \"разделяй и властвуй\", то есть через рекурсию.

        Question

        Дан отсортированный массив nums длины \\(n\\) , в котором все элементы уникальны. Найдите элемент target .

        С точки зрения стратегии \"разделяй и властвуй\" обозначим подзадачу, соответствующую интервалу поиска \\([i, j]\\) , через \\(f(i, j)\\) .

        Начиная с исходной задачи \\(f(0, n-1)\\) , выполняем двоичный поиск по следующим шагам.

        1. Вычислить середину \\(m\\) интервала поиска \\([i, j]\\) и с ее помощью исключить половину интервала.
        2. Рекурсивно решить подзадачу вдвое меньшего размера; это может быть либо \\(f(i, m-1)\\) , либо \\(f(m+1, j)\\) .
        3. Повторять шаг 1. и шаг 2. , пока не будет найден target или пока интервал не станет пустым.

        На рисунке 12-4 показан процесс применения стратегии \"разделяй и властвуй\" для поиска элемента \\(6\\) в массиве.

        Рисунок 12-4   Процесс двоичного поиска в стиле разделяй и властвуй

        В реализации кода мы объявляем рекурсивную функцию dfs() для решения задачи \\(f(i, j)\\) :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_recur.py
        def dfs(nums: list[int], target: int, i: int, j: int) -> int:\n    \"\"\"Бинарный поиск: задача f(i, j)\"\"\"\n    # Если интервал пуст, целевой элемент отсутствует, вернуть -1\n    if i > j:\n        return -1\n    # Вычислить индекс середины m\n    m = (i + j) // 2\n    if nums[m] < target:\n        # Рекурсивная подзадача f(m+1, j)\n        return dfs(nums, target, m + 1, j)\n    elif nums[m] > target:\n        # Рекурсивная подзадача f(i, m-1)\n        return dfs(nums, target, i, m - 1)\n    else:\n        # Целевой элемент найден, вернуть его индекс\n        return m\n\ndef binary_search(nums: list[int], target: int) -> int:\n    \"\"\"Бинарный поиск\"\"\"\n    n = len(nums)\n    # Решить задачу f(0, n-1)\n    return dfs(nums, target, 0, n - 1)\n
        binary_search_recur.cpp
        /* Бинарный поиск: задача f(i, j) */\nint dfs(vector<int> &nums, int target, int i, int j) {\n    // Если интервал пуст, целевой элемент отсутствует, вернуть -1\n    if (i > j) {\n        return -1;\n    }\n    // Вычислить индекс середины m\n    int m = (i + j) / 2;\n    if (nums[m] < target) {\n        // Рекурсивная подзадача f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // Рекурсивная подзадача f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // Целевой элемент найден, вернуть его индекс\n        return m;\n    }\n}\n\n/* Бинарный поиск */\nint binarySearch(vector<int> &nums, int target) {\n    int n = nums.size();\n    // Решить задачу f(0, n-1)\n    return dfs(nums, target, 0, n - 1);\n}\n
        binary_search_recur.java
        /* Бинарный поиск: задача f(i, j) */\nint dfs(int[] nums, int target, int i, int j) {\n    // Если интервал пуст, целевой элемент отсутствует, вернуть -1\n    if (i > j) {\n        return -1;\n    }\n    // Вычислить индекс середины m\n    int m = (i + j) / 2;\n    if (nums[m] < target) {\n        // Рекурсивная подзадача f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // Рекурсивная подзадача f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // Целевой элемент найден, вернуть его индекс\n        return m;\n    }\n}\n\n/* Бинарный поиск */\nint binarySearch(int[] nums, int target) {\n    int n = nums.length;\n    // Решить задачу f(0, n-1)\n    return dfs(nums, target, 0, n - 1);\n}\n
        binary_search_recur.cs
        /* Бинарный поиск: задача f(i, j) */\nint DFS(int[] nums, int target, int i, int j) {\n    // Если интервал пуст, целевой элемент отсутствует, вернуть -1\n    if (i > j) {\n        return -1;\n    }\n    // Вычислить индекс середины m\n    int m = (i + j) / 2;\n    if (nums[m] < target) {\n        // Рекурсивная подзадача f(m+1, j)\n        return DFS(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // Рекурсивная подзадача f(i, m-1)\n        return DFS(nums, target, i, m - 1);\n    } else {\n        // Целевой элемент найден, вернуть его индекс\n        return m;\n    }\n}\n\n/* Бинарный поиск */\nint BinarySearch(int[] nums, int target) {\n    int n = nums.Length;\n    // Решить задачу f(0, n-1)\n    return DFS(nums, target, 0, n - 1);\n}\n
        binary_search_recur.go
        /* Бинарный поиск: задача f(i, j) */\nfunc dfs(nums []int, target, i, j int) int {\n    // Если интервал пуст, это означает отсутствие целевого элемента, вернуть -1\n    if i > j {\n        return -1\n    }\n    // Вычислить средний индекс\n    m := i + ((j - i) >> 1)\n    // Сравнить середину и целевой элемент\n    if nums[m] < target {\n        // Если меньше, рекурсивно обрабатывать правую половину массива\n        // Рекурсивная подзадача f(m+1, j)\n        return dfs(nums, target, m+1, j)\n    } else if nums[m] > target {\n        // Если больше, рекурсивно обработать левую половину массива\n        // Рекурсивная подзадача f(i, m-1)\n        return dfs(nums, target, i, m-1)\n    } else {\n        // Целевой элемент найден, вернуть его индекс\n        return m\n    }\n}\n\n/* Бинарный поиск */\nfunc binarySearch(nums []int, target int) int {\n    n := len(nums)\n    return dfs(nums, target, 0, n-1)\n}\n
        binary_search_recur.swift
        /* Бинарный поиск: задача f(i, j) */\nfunc dfs(nums: [Int], target: Int, i: Int, j: Int) -> Int {\n    // Если интервал пуст, целевой элемент отсутствует, вернуть -1\n    if i > j {\n        return -1\n    }\n    // Вычислить индекс середины m\n    let m = (i + j) / 2\n    if nums[m] < target {\n        // Рекурсивная подзадача f(m+1, j)\n        return dfs(nums: nums, target: target, i: m + 1, j: j)\n    } else if nums[m] > target {\n        // Рекурсивная подзадача f(i, m-1)\n        return dfs(nums: nums, target: target, i: i, j: m - 1)\n    } else {\n        // Целевой элемент найден, вернуть его индекс\n        return m\n    }\n}\n\n/* Бинарный поиск */\nfunc binarySearch(nums: [Int], target: Int) -> Int {\n    // Решить задачу f(0, n-1)\n    dfs(nums: nums, target: target, i: nums.startIndex, j: nums.endIndex - 1)\n}\n
        binary_search_recur.js
        /* Бинарный поиск: задача f(i, j) */\nfunction dfs(nums, target, i, j) {\n    // Если интервал пуст, целевой элемент отсутствует, вернуть -1\n    if (i > j) {\n        return -1;\n    }\n    // Вычислить индекс середины m\n    const m = i + ((j - i) >> 1);\n    if (nums[m] < target) {\n        // Рекурсивная подзадача f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // Рекурсивная подзадача f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // Целевой элемент найден, вернуть его индекс\n        return m;\n    }\n}\n\n/* Бинарный поиск */\nfunction binarySearch(nums, target) {\n    const n = nums.length;\n    // Решить задачу f(0, n-1)\n    return dfs(nums, target, 0, n - 1);\n}\n
        binary_search_recur.ts
        /* Бинарный поиск: задача f(i, j) */\nfunction dfs(nums: number[], target: number, i: number, j: number): number {\n    // Если интервал пуст, целевой элемент отсутствует, вернуть -1\n    if (i > j) {\n        return -1;\n    }\n    // Вычислить индекс середины m\n    const m = i + ((j - i) >> 1);\n    if (nums[m] < target) {\n        // Рекурсивная подзадача f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // Рекурсивная подзадача f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // Целевой элемент найден, вернуть его индекс\n        return m;\n    }\n}\n\n/* Бинарный поиск */\nfunction binarySearch(nums: number[], target: number): number {\n    const n = nums.length;\n    // Решить задачу f(0, n-1)\n    return dfs(nums, target, 0, n - 1);\n}\n
        binary_search_recur.dart
        /* Бинарный поиск: задача f(i, j) */\nint dfs(List<int> nums, int target, int i, int j) {\n  // Если интервал пуст, целевой элемент отсутствует, вернуть -1\n  if (i > j) {\n    return -1;\n  }\n  // Вычислить индекс середины m\n  int m = (i + j) ~/ 2;\n  if (nums[m] < target) {\n    // Рекурсивная подзадача f(m+1, j)\n    return dfs(nums, target, m + 1, j);\n  } else if (nums[m] > target) {\n    // Рекурсивная подзадача f(i, m-1)\n    return dfs(nums, target, i, m - 1);\n  } else {\n    // Целевой элемент найден, вернуть его индекс\n    return m;\n  }\n}\n\n/* Бинарный поиск */\nint binarySearch(List<int> nums, int target) {\n  int n = nums.length;\n  // Решить задачу f(0, n-1)\n  return dfs(nums, target, 0, n - 1);\n}\n
        binary_search_recur.rs
        /* Бинарный поиск: задача f(i, j) */\nfn dfs(nums: &[i32], target: i32, i: i32, j: i32) -> i32 {\n    // Если интервал пуст, целевой элемент отсутствует, вернуть -1\n    if i > j {\n        return -1;\n    }\n    let m: i32 = i + (j - i) / 2;\n    if nums[m as usize] < target {\n        // Рекурсивная подзадача f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if nums[m as usize] > target {\n        // Рекурсивная подзадача f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // Целевой элемент найден, вернуть его индекс\n        return m;\n    }\n}\n\n/* Бинарный поиск */\nfn binary_search(nums: &[i32], target: i32) -> i32 {\n    let n = nums.len() as i32;\n    // Решить задачу f(0, n-1)\n    dfs(nums, target, 0, n - 1)\n}\n
        binary_search_recur.c
        /* Бинарный поиск: задача f(i, j) */\nint dfs(int nums[], int target, int i, int j) {\n    // Если интервал пуст, целевой элемент отсутствует, вернуть -1\n    if (i > j) {\n        return -1;\n    }\n    // Вычислить индекс середины m\n    int m = (i + j) / 2;\n    if (nums[m] < target) {\n        // Рекурсивная подзадача f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // Рекурсивная подзадача f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // Целевой элемент найден, вернуть его индекс\n        return m;\n    }\n}\n\n/* Бинарный поиск */\nint binarySearch(int nums[], int target, int numsSize) {\n    int n = numsSize;\n    // Решить задачу f(0, n-1)\n    return dfs(nums, target, 0, n - 1);\n}\n
        binary_search_recur.kt
        /* Бинарный поиск: задача f(i, j) */\nfun dfs(\n    nums: IntArray,\n    target: Int,\n    i: Int,\n    j: Int\n): Int {\n    // Если интервал пуст, целевой элемент отсутствует, вернуть -1\n    if (i > j) {\n        return -1\n    }\n    // Вычислить индекс середины m\n    val m = (i + j) / 2\n    return if (nums[m] < target) {\n        // Рекурсивная подзадача f(m+1, j)\n        dfs(nums, target, m + 1, j)\n    } else if (nums[m] > target) {\n        // Рекурсивная подзадача f(i, m-1)\n        dfs(nums, target, i, m - 1)\n    } else {\n        // Целевой элемент найден, вернуть его индекс\n        m\n    }\n}\n\n/* Бинарный поиск */\nfun binarySearch(nums: IntArray, target: Int): Int {\n    val n = nums.size\n    // Решить задачу f(0, n-1)\n    return dfs(nums, target, 0, n - 1)\n}\n
        binary_search_recur.rb
        ### Бинарный поиск: задача f(i, j) ###\ndef dfs(nums, target, i, j)\n  # Если интервал пуст, целевой элемент отсутствует, вернуть -1\n  return -1 if i > j\n\n  # Вычислить индекс середины m\n  m = (i + j) / 2\n\n  if nums[m] < target\n    # Рекурсивная подзадача f(m+1, j)\n    return dfs(nums, target, m + 1, j)\n  elsif nums[m] > target\n    # Рекурсивная подзадача f(i, m-1)\n    return dfs(nums, target, i, m - 1)\n  else\n    # Целевой элемент найден, вернуть его индекс\n    return m\n  end\nend\n\n### Бинарный поиск ###\ndef binary_search(nums, target)\n  n = nums.length\n  # Решить задачу f(0, n-1)\n  dfs(nums, target, 0, n - 1)\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 12. Разделяй и властвуй","12.2   Поисковая стратегия разделяй и властвуй"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/","level":1,"title":"12.3   Задача построения двоичного дерева","text":"

        Question

        Даны прямой обход preorder и симметричный обход inorder некоторого двоичного дерева. Постройте по ним двоичное дерево и верните его корневой узел. Предполагается, что в дереве нет узлов с одинаковыми значениями (как показано на рисунке 12-5).

        Рисунок 12-5   Пример данных для построения двоичного дерева

        ","path":["Глава 12. Разделяй и властвуй","12.3   Задача построения двоичного дерева"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#1","level":3,"title":"1.   Проверка, является ли это задачей \"разделяй и властвуй\"","text":"

        Исходная задача - построить двоичное дерево по preorder и inorder - является типичной задачей для стратегии \"разделяй и властвуй\".

        • Задача раскладывается на части: если смотреть с точки зрения стратегии \"разделяй и властвуй\", исходную задачу можно разбить на две подзадачи: построение левого поддерева и построение правого поддерева, плюс одно действие: инициализация корневого узла. Для каждого поддерева (подзадачи) можно использовать тот же способ разбиения, пока не будет достигнута наименьшая подзадача (пустое поддерево).
        • Подзадачи независимы: левое и правое поддеревья независимы друг от друга и не пересекаются. При построении левого поддерева нам нужно смотреть только на ту часть прямого и симметричного обходов, которая соответствует левому поддереву. Для правого поддерева рассуждение аналогично.
        • Решения подзадач можно объединить: когда левое и правое поддеревья (решения подзадач) уже построены, их можно присоединить к корневому узлу и тем самым получить решение исходной задачи.
        ","path":["Глава 12. Разделяй и властвуй","12.3   Задача построения двоичного дерева"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#2","level":3,"title":"2.   Как разделить поддеревья","text":"

        Из анализа выше видно, что эта задача действительно решается через \"разделяй и властвуй\", но как именно, имея прямой обход preorder и симметричный обход inorder, отделить левое и правое поддеревья?

        По определению и preorder , и inorder можно разбить на три части.

        • Прямой обход: [ корневой узел | левое поддерево | правое поддерево ] , например для дерева на рисунке 12-5 это [ 3 | 9 | 2 1 7 ] .
        • Симметричный обход: [ левое поддерево | корневой узел | правое поддерево ] , например для дерева на рисунке 12-5 это [ 9 | 3 | 1 2 7 ] .

        На примере данных с рисунка можно получить результат разбиения по следующим шагам.

        1. Первый элемент прямого обхода, равный 3, является значением корневого узла.
        2. Найти индекс корневого узла 3 в inorder ; используя этот индекс, можно разбить inorder на [ 9 | 3 | 1 2 7 ] .
        3. По результату разбиения inorder нетрудно определить, что число узлов в левом и правом поддеревьях равно 1 и 3 соответственно, а значит, preorder можно разбить как [ 3 | 9 | 2 1 7 ] .

        Рисунок 12-6   Разбиение поддеревьев в прямом и симметричном обходах

        ","path":["Глава 12. Разделяй и властвуй","12.3   Задача построения двоичного дерева"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#3","level":3,"title":"3.   Описание интервалов поддеревьев через переменные","text":"

        Согласно описанному выше способу разбиения, мы уже получили интервалы индексов корневого узла, левого и правого поддеревьев в preorder и inorder. Чтобы описывать эти интервалы, нам понадобится несколько указателей-переменных.

        • Обозначим индекс корневого узла текущего дерева в preorder через \\(i\\) .
        • Обозначим индекс корневого узла текущего дерева в inorder через \\(m\\) .
        • Обозначим интервал индексов текущего дерева в inorder через \\([l, r]\\) .

        Как показано в таблице 12-1, этих переменных достаточно для описания индекса корневого узла в preorder и интервалов поддеревьев в inorder .

        Таблица 12-1   Индексы корневого узла и поддеревьев в прямом и симметричном обходах

        Индекс корневого узла в preorder Интервал индексов поддерева в inorder Текущее дерево \\(i\\) \\([l, r]\\) Левое поддерево \\(i + 1\\) \\([l, m-1]\\) Правое поддерево \\(i + 1 + (m - l)\\) \\([m+1, r]\\)

        Стоит отметить, что \\((m-l)\\) в индексе корневого узла правого поддерева означает число узлов в левом поддереве; лучше всего понимать это выражение вместе с рисунком ниже.

        Рисунок 12-7   Представление индексных интервалов корня и поддеревьев

        ","path":["Глава 12. Разделяй и властвуй","12.3   Задача построения двоичного дерева"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#4","level":3,"title":"4.   Реализация кода","text":"

        Чтобы ускорить поиск \\(m\\) , мы используем хеш-таблицу hmap для хранения отображения значений массива inorder в индексы:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby build_tree.py
        def dfs(\n    preorder: list[int],\n    inorder_map: dict[int, int],\n    i: int,\n    l: int,\n    r: int,\n) -> TreeNode | None:\n    \"\"\"Построить двоичное дерево: разделяй и властвуй\"\"\"\n    # Завершить при пустом диапазоне поддерева\n    if r - l < 0:\n        return None\n    # Инициализировать корневой узел\n    root = TreeNode(preorder[i])\n    # Найти m, чтобы разделить левое и правое поддеревья\n    m = inorder_map[preorder[i]]\n    # Подзадача: построить левое поддерево\n    root.left = dfs(preorder, inorder_map, i + 1, l, m - 1)\n    # Подзадача: построить правое поддерево\n    root.right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r)\n    # Вернуть корневой узел\n    return root\n\ndef build_tree(preorder: list[int], inorder: list[int]) -> TreeNode | None:\n    \"\"\"Построить двоичное дерево\"\"\"\n    # Инициализировать хеш-таблицу для хранения соответствия элементов inorder их индексам\n    inorder_map = {val: i for i, val in enumerate(inorder)}\n    root = dfs(preorder, inorder_map, 0, 0, len(inorder) - 1)\n    return root\n
        build_tree.cpp
        /* Построить двоичное дерево: разделяй и властвуй */\nTreeNode *dfs(vector<int> &preorder, unordered_map<int, int> &inorderMap, int i, int l, int r) {\n    // Завершить при пустом диапазоне поддерева\n    if (r - l < 0)\n        return NULL;\n    // Инициализировать корневой узел\n    TreeNode *root = new TreeNode(preorder[i]);\n    // Найти m, чтобы разделить левое и правое поддеревья\n    int m = inorderMap[preorder[i]];\n    // Подзадача: построить левое поддерево\n    root->left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n    // Подзадача: построить правое поддерево\n    root->right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // Вернуть корневой узел\n    return root;\n}\n\n/* Построить двоичное дерево */\nTreeNode *buildTree(vector<int> &preorder, vector<int> &inorder) {\n    // Инициализировать хеш-таблицу для хранения соответствия элементов inorder их индексам\n    unordered_map<int, int> inorderMap;\n    for (int i = 0; i < inorder.size(); i++) {\n        inorderMap[inorder[i]] = i;\n    }\n    TreeNode *root = dfs(preorder, inorderMap, 0, 0, inorder.size() - 1);\n    return root;\n}\n
        build_tree.java
        /* Построить двоичное дерево: разделяй и властвуй */\nTreeNode dfs(int[] preorder, Map<Integer, Integer> inorderMap, int i, int l, int r) {\n    // Завершить при пустом диапазоне поддерева\n    if (r - l < 0)\n        return null;\n    // Инициализировать корневой узел\n    TreeNode root = new TreeNode(preorder[i]);\n    // Найти m, чтобы разделить левое и правое поддеревья\n    int m = inorderMap.get(preorder[i]);\n    // Подзадача: построить левое поддерево\n    root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n    // Подзадача: построить правое поддерево\n    root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // Вернуть корневой узел\n    return root;\n}\n\n/* Построить двоичное дерево */\nTreeNode buildTree(int[] preorder, int[] inorder) {\n    // Инициализировать хеш-таблицу для хранения соответствия элементов inorder их индексам\n    Map<Integer, Integer> inorderMap = new HashMap<>();\n    for (int i = 0; i < inorder.length; i++) {\n        inorderMap.put(inorder[i], i);\n    }\n    TreeNode root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);\n    return root;\n}\n
        build_tree.cs
        /* Построить двоичное дерево: разделяй и властвуй */\nTreeNode? DFS(int[] preorder, Dictionary<int, int> inorderMap, int i, int l, int r) {\n    // Завершить при пустом диапазоне поддерева\n    if (r - l < 0)\n        return null;\n    // Инициализировать корневой узел\n    TreeNode root = new(preorder[i]);\n    // Найти m, чтобы разделить левое и правое поддеревья\n    int m = inorderMap[preorder[i]];\n    // Подзадача: построить левое поддерево\n    root.left = DFS(preorder, inorderMap, i + 1, l, m - 1);\n    // Подзадача: построить правое поддерево\n    root.right = DFS(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // Вернуть корневой узел\n    return root;\n}\n\n/* Построить двоичное дерево */\nTreeNode? BuildTree(int[] preorder, int[] inorder) {\n    // Инициализировать хеш-таблицу для хранения соответствия элементов inorder их индексам\n    Dictionary<int, int> inorderMap = [];\n    for (int i = 0; i < inorder.Length; i++) {\n        inorderMap.TryAdd(inorder[i], i);\n    }\n    TreeNode? root = DFS(preorder, inorderMap, 0, 0, inorder.Length - 1);\n    return root;\n}\n
        build_tree.go
        /* Построить двоичное дерево: разделяй и властвуй */\nfunc dfsBuildTree(preorder []int, inorderMap map[int]int, i, l, r int) *TreeNode {\n    // Завершить при пустом диапазоне поддерева\n    if r-l < 0 {\n        return nil\n    }\n    // Инициализировать корневой узел\n    root := NewTreeNode(preorder[i])\n    // Найти m, чтобы разделить левое и правое поддеревья\n    m := inorderMap[preorder[i]]\n    // Подзадача: построить левое поддерево\n    root.Left = dfsBuildTree(preorder, inorderMap, i+1, l, m-1)\n    // Подзадача: построить правое поддерево\n    root.Right = dfsBuildTree(preorder, inorderMap, i+1+m-l, m+1, r)\n    // Вернуть корневой узел\n    return root\n}\n\n/* Построить двоичное дерево */\nfunc buildTree(preorder, inorder []int) *TreeNode {\n    // Инициализировать хеш-таблицу для хранения соответствия элементов inorder их индексам\n    inorderMap := make(map[int]int, len(inorder))\n    for i := 0; i < len(inorder); i++ {\n        inorderMap[inorder[i]] = i\n    }\n\n    root := dfsBuildTree(preorder, inorderMap, 0, 0, len(inorder)-1)\n    return root\n}\n
        build_tree.swift
        /* Построить двоичное дерево: разделяй и властвуй */\nfunc dfs(preorder: [Int], inorderMap: [Int: Int], i: Int, l: Int, r: Int) -> TreeNode? {\n    // Завершить при пустом диапазоне поддерева\n    if r - l < 0 {\n        return nil\n    }\n    // Инициализировать корневой узел\n    let root = TreeNode(x: preorder[i])\n    // Найти m, чтобы разделить левое и правое поддеревья\n    let m = inorderMap[preorder[i]]!\n    // Подзадача: построить левое поддерево\n    root.left = dfs(preorder: preorder, inorderMap: inorderMap, i: i + 1, l: l, r: m - 1)\n    // Подзадача: построить правое поддерево\n    root.right = dfs(preorder: preorder, inorderMap: inorderMap, i: i + 1 + m - l, l: m + 1, r: r)\n    // Вернуть корневой узел\n    return root\n}\n\n/* Построить двоичное дерево */\nfunc buildTree(preorder: [Int], inorder: [Int]) -> TreeNode? {\n    // Инициализировать хеш-таблицу для хранения соответствия элементов inorder их индексам\n    let inorderMap = inorder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset }\n    return dfs(preorder: preorder, inorderMap: inorderMap, i: inorder.startIndex, l: inorder.startIndex, r: inorder.endIndex - 1)\n}\n
        build_tree.js
        /* Построить двоичное дерево: разделяй и властвуй */\nfunction dfs(preorder, inorderMap, i, l, r) {\n    // Завершить при пустом диапазоне поддерева\n    if (r - l < 0) return null;\n    // Инициализировать корневой узел\n    const root = new TreeNode(preorder[i]);\n    // Найти m, чтобы разделить левое и правое поддеревья\n    const m = inorderMap.get(preorder[i]);\n    // Подзадача: построить левое поддерево\n    root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n    // Подзадача: построить правое поддерево\n    root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // Вернуть корневой узел\n    return root;\n}\n\n/* Построить двоичное дерево */\nfunction buildTree(preorder, inorder) {\n    // Инициализировать хеш-таблицу для хранения соответствия элементов inorder их индексам\n    let inorderMap = new Map();\n    for (let i = 0; i < inorder.length; i++) {\n        inorderMap.set(inorder[i], i);\n    }\n    const root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);\n    return root;\n}\n
        build_tree.ts
        /* Построить двоичное дерево: разделяй и властвуй */\nfunction dfs(\n    preorder: number[],\n    inorderMap: Map<number, number>,\n    i: number,\n    l: number,\n    r: number\n): TreeNode | null {\n    // Завершить при пустом диапазоне поддерева\n    if (r - l < 0) return null;\n    // Инициализировать корневой узел\n    const root: TreeNode = new TreeNode(preorder[i]);\n    // Найти m, чтобы разделить левое и правое поддеревья\n    const m = inorderMap.get(preorder[i]);\n    // Подзадача: построить левое поддерево\n    root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n    // Подзадача: построить правое поддерево\n    root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // Вернуть корневой узел\n    return root;\n}\n\n/* Построить двоичное дерево */\nfunction buildTree(preorder: number[], inorder: number[]): TreeNode | null {\n    // Инициализировать хеш-таблицу для хранения соответствия элементов inorder их индексам\n    let inorderMap = new Map<number, number>();\n    for (let i = 0; i < inorder.length; i++) {\n        inorderMap.set(inorder[i], i);\n    }\n    const root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);\n    return root;\n}\n
        build_tree.dart
        /* Построить двоичное дерево: разделяй и властвуй */\nTreeNode? dfs(\n  List<int> preorder,\n  Map<int, int> inorderMap,\n  int i,\n  int l,\n  int r,\n) {\n  // Завершить при пустом диапазоне поддерева\n  if (r - l < 0) {\n    return null;\n  }\n  // Инициализировать корневой узел\n  TreeNode? root = TreeNode(preorder[i]);\n  // Найти m, чтобы разделить левое и правое поддеревья\n  int m = inorderMap[preorder[i]]!;\n  // Подзадача: построить левое поддерево\n  root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n  // Подзадача: построить правое поддерево\n  root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n  // Вернуть корневой узел\n  return root;\n}\n\n/* Построить двоичное дерево */\nTreeNode? buildTree(List<int> preorder, List<int> inorder) {\n  // Инициализировать хеш-таблицу для хранения соответствия элементов inorder их индексам\n  Map<int, int> inorderMap = {};\n  for (int i = 0; i < inorder.length; i++) {\n    inorderMap[inorder[i]] = i;\n  }\n  TreeNode? root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);\n  return root;\n}\n
        build_tree.rs
        /* Построить двоичное дерево: разделяй и властвуй */\nfn dfs(\n    preorder: &[i32],\n    inorder_map: &HashMap<i32, i32>,\n    i: i32,\n    l: i32,\n    r: i32,\n) -> Option<Rc<RefCell<TreeNode>>> {\n    // Завершить при пустом диапазоне поддерева\n    if r - l < 0 {\n        return None;\n    }\n    // Инициализировать корневой узел\n    let root = TreeNode::new(preorder[i as usize]);\n    // Найти m, чтобы разделить левое и правое поддеревья\n    let m = inorder_map.get(&preorder[i as usize]).unwrap();\n    // Подзадача: построить левое поддерево\n    root.borrow_mut().left = dfs(preorder, inorder_map, i + 1, l, m - 1);\n    // Подзадача: построить правое поддерево\n    root.borrow_mut().right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r);\n    // Вернуть корневой узел\n    Some(root)\n}\n\n/* Построить двоичное дерево */\nfn build_tree(preorder: &[i32], inorder: &[i32]) -> Option<Rc<RefCell<TreeNode>>> {\n    // Инициализировать хеш-таблицу для хранения соответствия элементов inorder их индексам\n    let mut inorder_map: HashMap<i32, i32> = HashMap::new();\n    for i in 0..inorder.len() {\n        inorder_map.insert(inorder[i], i as i32);\n    }\n    let root = dfs(preorder, &inorder_map, 0, 0, inorder.len() as i32 - 1);\n    root\n}\n
        build_tree.c
        /* Построить двоичное дерево: разделяй и властвуй */\nTreeNode *dfs(int *preorder, int *inorderMap, int i, int l, int r, int size) {\n    // Завершить при пустом диапазоне поддерева\n    if (r - l < 0)\n        return NULL;\n    // Инициализировать корневой узел\n    TreeNode *root = (TreeNode *)malloc(sizeof(TreeNode));\n    root->val = preorder[i];\n    root->left = NULL;\n    root->right = NULL;\n    // Найти m, чтобы разделить левое и правое поддеревья\n    int m = inorderMap[preorder[i]];\n    // Подзадача: построить левое поддерево\n    root->left = dfs(preorder, inorderMap, i + 1, l, m - 1, size);\n    // Подзадача: построить правое поддерево\n    root->right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r, size);\n    // Вернуть корневой узел\n    return root;\n}\n\n/* Построить двоичное дерево */\nTreeNode *buildTree(int *preorder, int preorderSize, int *inorder, int inorderSize) {\n    // Инициализировать хеш-таблицу для хранения соответствия элементов inorder их индексам\n    int *inorderMap = (int *)malloc(sizeof(int) * MAX_SIZE);\n    for (int i = 0; i < inorderSize; i++) {\n        inorderMap[inorder[i]] = i;\n    }\n    TreeNode *root = dfs(preorder, inorderMap, 0, 0, inorderSize - 1, inorderSize);\n    free(inorderMap);\n    return root;\n}\n
        build_tree.kt
        /* Построить двоичное дерево: разделяй и властвуй */\nfun dfs(\n    preorder: IntArray,\n    inorderMap: Map<Int?, Int?>,\n    i: Int,\n    l: Int,\n    r: Int\n): TreeNode? {\n    // Завершить при пустом диапазоне поддерева\n    if (r - l < 0) return null\n    // Инициализировать корневой узел\n    val root = TreeNode(preorder[i])\n    // Найти m, чтобы разделить левое и правое поддеревья\n    val m = inorderMap[preorder[i]]!!\n    // Подзадача: построить левое поддерево\n    root.left = dfs(preorder, inorderMap, i + 1, l, m - 1)\n    // Подзадача: построить правое поддерево\n    root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r)\n    // Вернуть корневой узел\n    return root\n}\n\n/* Построить двоичное дерево */\nfun buildTree(preorder: IntArray, inorder: IntArray): TreeNode? {\n    // Инициализировать хеш-таблицу для хранения соответствия элементов inorder их индексам\n    val inorderMap = HashMap<Int?, Int?>()\n    for (i in inorder.indices) {\n        inorderMap[inorder[i]] = i\n    }\n    val root = dfs(preorder, inorderMap, 0, 0, inorder.size - 1)\n    return root\n}\n
        build_tree.rb
        ### Построить двоичное дерево: разделяй и властвуй ###\ndef dfs(preorder, inorder_map, i, l, r)\n  # Завершить при пустом диапазоне поддерева\n  return if r - l < 0\n\n  # Инициализировать корневой узел\n  root = TreeNode.new(preorder[i])\n  # Найти m, чтобы разделить левое и правое поддеревья\n  m = inorder_map[preorder[i]]\n  # Подзадача: построить левое поддерево\n  root.left = dfs(preorder, inorder_map, i + 1, l, m - 1)\n  # Подзадача: построить правое поддерево\n  root.right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r)\n\n  # Вернуть корневой узел\n  root\nend\n\n### Построить двоичное дерево ###\ndef build_tree(preorder, inorder)\n  # Инициализировать хеш-таблицу для хранения соответствия элементов inorder их индексам\n  inorder_map = {}\n  inorder.each_with_index { |val, i| inorder_map[val] = i }\n  dfs(preorder, inorder_map, 0, 0, inorder.length - 1)\nend\n
        Визуализация кода

        Во весь экран >

        На рисунке 12-8 показан рекурсивный процесс построения двоичного дерева: каждый узел создается в фазе \"спуска\", а каждое ребро (ссылка) формируется в фазе \"подъема\".

        <1><2><3><4><5><6><7><8><9>

        Рисунок 12-8   Рекурсивный процесс построения двоичного дерева

        Результаты разбиения preorder и inorder внутри каждого рекурсивного вызова показаны на рисунке 12-9.

        Рисунок 12-9   Результаты разбиения в каждом рекурсивном вызове

        Пусть число узлов дерева равно \\(n\\) ; инициализация каждого узла (то есть выполнение одного рекурсивного вызова dfs() ) занимает \\(O(1)\\) времени. Следовательно, общая временная сложность равна \\(O(n)\\) .

        Хеш-таблица хранит отображение значений inorder в индексы, поэтому ее пространственная сложность равна \\(O(n)\\) . В худшем случае, когда двоичное дерево вырождается в связный список, глубина рекурсии достигает \\(n\\) и требует \\(O(n)\\) памяти стека. Следовательно, общая пространственная сложность также равна \\(O(n)\\) .

        ","path":["Глава 12. Разделяй и властвуй","12.3   Задача построения двоичного дерева"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/","level":1,"title":"12.1   Стратегия разделяй и властвуй","text":"

        Разделяй и властвуй (divide and conquer) - это очень важная и широко используемая стратегия построения алгоритмов. Обычно она реализуется через рекурсию и включает два этапа: \"разделение\" и \"объединение\".

        1. Разделение (этап декомпозиции): рекурсивно разбить исходную задачу на две или более подзадачи, пока не будет достигнута наименьшая подзадача.
        2. Объединение (этап синтеза): начиная с уже известных решений наименьших подзадач, снизу вверх объединять решения подзадач и тем самым получать решение исходной задачи.

        Как показано на рисунке 12-1, \"сортировка слиянием\" является одним из типичных примеров применения стратегии \"разделяй и властвуй\".

        1. Разделение: рекурсивно разделить исходный массив (исходную задачу) на два подмассива (подзадачи), пока в подмассиве не останется только один элемент (наименьшая подзадача).
        2. Объединение: снизу вверх объединять упорядоченные подмассивы (решения подзадач), чтобы получить упорядоченный исходный массив (решение исходной задачи).

        Рисунок 12-1   Стратегия разделяй и властвуй в сортировке слиянием

        ","path":["Глава 12. Разделяй и властвуй","12.1   Стратегия разделяй и властвуй"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1211","level":2,"title":"12.1.1   Как определить задачу \"разделяй и властвуй\"","text":"

        Чтобы понять, подходит ли задача для решения методом \"разделяй и властвуй\", обычно можно ориентироваться на следующие критерии.

        1. Задача раскладывается на части: исходную задачу можно разбить на более мелкие и похожие подзадачи, причем такое разбиение можно применять рекурсивно.
        2. Подзадачи независимы: подзадачи не пересекаются, не зависят друг от друга и могут решаться независимо.
        3. Решения подзадач можно объединить: решение исходной задачи получается объединением решений подзадач.

        Очевидно, что сортировка слиянием удовлетворяет всем трем критериям.

        1. Задача раскладывается на части: массив (исходная задача) рекурсивно делится на два подмассива (подзадачи).
        2. Подзадачи независимы: каждый подмассив можно сортировать отдельно (то есть каждую подзадачу можно решать независимо).
        3. Решения подзадач можно объединить: два упорядоченных подмассива (решения подзадач) можно объединить в один упорядоченный массив (решение исходной задачи).
        ","path":["Глава 12. Разделяй и властвуй","12.1   Стратегия разделяй и властвуй"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1212","level":2,"title":"12.1.2   Повышение эффективности с помощью \"разделяй и властвуй\"","text":"

        Стратегия \"разделяй и властвуй\" не только позволяет эффективно решать алгоритмические задачи, но и часто повышает эффективность самих алгоритмов. Именно поэтому быстрая сортировка, сортировка слиянием и пирамидальная сортировка обычно работают быстрее, чем сортировка выбором, пузырьком и вставками.

        Тогда возникает естественный вопрос: почему стратегия \"разделяй и властвуй\" повышает эффективность алгоритма и какова внутренняя логика этого подхода? Иными словами, почему разбиение большой задачи на несколько подзадач, решение этих подзадач и последующее объединение их решений оказывается эффективнее, чем прямое решение исходной задачи? Этот вопрос можно рассмотреть с двух сторон: через число операций и через параллельные вычисления.

        ","path":["Глава 12. Разделяй и властвуй","12.1   Стратегия разделяй и властвуй"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1","level":3,"title":"1.   Оптимизация числа операций","text":"

        Рассмотрим \"сортировку пузырьком\": для массива длины \\(n\\) ей требуется \\(O(n^2)\\) времени. Предположим, что мы разделим массив на два подмассива в середине, как показано на рисунке 12-2. Тогда само разбиение потребует \\(O(n)\\) времени, сортировка каждого подмассива займет \\(O((n / 2)^2)\\) времени, а объединение двух подмассивов потребует еще \\(O(n)\\) времени. Общая временная сложность будет равна:

        \\[ O(n + (\\frac{n}{2})^2 \\times 2 + n) = O(\\frac{n^2}{2} + 2n) \\]

        Рисунок 12-2   Сортировка пузырьком до и после разбиения массива

        Теперь рассмотрим следующее неравенство, в котором левая и правая части обозначают общее число операций до разбиения и после него:

        \\[ \\begin{aligned} n^2 & > \\frac{n^2}{2} + 2n \\newline n^2 - \\frac{n^2}{2} - 2n & > 0 \\newline n(n - 4) & > 0 \\end{aligned} \\]

        Это означает, что при \\(n > 4\\) число операций после разбиения становится меньше, а значит, сортировка должна работать быстрее. При этом важно заметить, что временная сложность после разбиения все еще остается квадратичной, то есть \\(O(n^2)\\) ; уменьшается лишь константный множитель.

        Если пойти дальше и продолжать делить каждый подмассив пополам, пока в нем не останется только один элемент, то мы фактически получим \"сортировку слиянием\", чья временная сложность равна \\(O(n \\log n)\\) .

        Можно пойти еще дальше и спросить: что если задать несколько точек разделения и равномерно разбить исходный массив на \\(k\\) подмассивов? Такая ситуация очень похожа на блочную сортировку, которая особенно хорошо подходит для сортировки очень больших объемов данных и теоретически может достигать временной сложности \\(O(n + k)\\) .

        ","path":["Глава 12. Разделяй и властвуй","12.1   Стратегия разделяй и властвуй"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#2","level":3,"title":"2.   Оптимизация параллельных вычислений","text":"

        Мы знаем, что подзадачи, порождаемые стратегией \"разделяй и властвуй\", являются независимыми, а значит, их обычно можно решать параллельно. Иначе говоря, \"разделяй и властвуй\" не только может уменьшить временную сложность алгоритма, но и хорошо сочетается с параллельной оптимизацией на уровне системы.

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

        Например, в показанной ниже \"блочной сортировке\" большой объем данных равномерно распределяется по блокам. Тогда сортировку каждого блока можно поручить отдельным вычислительным единицам, а после завершения просто объединить результаты.

        Рисунок 12-3   Параллельные вычисления в блочной сортировке

        ","path":["Глава 12. Разделяй и властвуй","12.1   Стратегия разделяй и властвуй"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1213","level":2,"title":"12.1.3   Типичные применения стратегии \"разделяй и властвуй\"","text":"

        С одной стороны, стратегию \"разделяй и властвуй\" можно использовать для решения многих классических алгоритмических задач.

        • Поиск ближайшей пары точек: сначала множество точек делится на две части, затем ищется ближайшая пара в каждой части, а затем ближайшая пара, пересекающая границу между двумя частями.
        • Умножение больших чисел: например, алгоритм Карацубы, который раскладывает умножение больших чисел на несколько умножений и сложений меньших чисел.
        • Умножение матриц: например, алгоритм Штрассена, который раскладывает умножение больших матриц на несколько умножений и сложений матриц меньшего размера.
        • Задача о Ханойской башне: задача о Ханойской башне решается рекурсивно и является типичным примером применения стратегии \"разделяй и властвуй\".
        • Подсчет инверсий: если в последовательности предыдущее число больше следующего, то такая пара образует инверсию. Эту задачу можно решить с помощью идей \"разделяй и властвуй\", опираясь на сортировку слиянием.

        С другой стороны, стратегия \"разделяй и властвуй\" очень широко применяется при проектировании алгоритмов и структур данных.

        • Двоичный поиск: двоичный поиск делит отсортированный массив на две части по индексу середины, а затем, в зависимости от результата сравнения целевого значения со средним элементом, исключает одну из половин и повторяет ту же операцию на оставшемся интервале.
        • Сортировка слиянием: она уже была рассмотрена в начале этого раздела, поэтому не будем повторяться.
        • Быстрая сортировка: в ней выбирается опорное значение, после чего массив делится на два подмассива: один содержит элементы меньше опорного, а другой - больше. Затем такая же операция повторяется для обеих частей, пока в подмассиве не останется один элемент.
        • Блочная сортировка: ее основная идея заключается в распределении данных по нескольким блокам, сортировке элементов внутри каждого блока и последующем последовательном извлечении элементов из блоков для построения отсортированного массива.
        • Деревья: например, двоичные деревья поиска, AVL-деревья, красно-черные деревья, B-деревья, B+ деревья и т.д. Их операции поиска, вставки и удаления можно рассматривать как применение стратегии \"разделяй и властвуй\".
        • Кучи: куча является особым видом полного двоичного дерева, а такие операции, как вставка, удаление и упорядочивание, по сути содержат идеи \"разделяй и властвуй\".
        • Хеш-таблицы: хотя хеш-таблицы напрямую не используют стратегию \"разделяй и властвуй\", некоторые способы разрешения коллизий косвенно опираются на эту идею. Например, длинные цепочки в методе цепочек могут преобразовываться в красно-черные деревья для повышения эффективности поиска.

        Нетрудно заметить, что \"разделяй и властвуй\" - это \"тихая\" алгоритмическая идея, скрыто присутствующая внутри самых разных алгоритмов и структур данных.

        ","path":["Глава 12. Разделяй и властвуй","12.1   Стратегия разделяй и властвуй"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/","level":1,"title":"12.4   Задача о Ханойской башне","text":"

        В задачах сортировки слиянием и построения двоичного дерева мы делили исходную задачу на две подзадачи, каждая из которых имела размер, равный примерно половине исходной задачи. Однако для задачи о Ханойской башне используется другая стратегия разбиения.

        Question

        Даны три стержня, обозначенные как A , B и C . В начальном состоянии на стержне A находятся \\(n\\) дисков, расположенных сверху вниз в порядке от меньшего к большему. Нужно переместить эти \\(n\\) дисков на стержень C , сохранив их исходный порядок (как показано на рисунке 12-10). Во время перемещения дисков необходимо соблюдать следующие правила.

        1. Диск можно снять только с вершины одного стержня и положить только на вершину другого стержня.
        2. За один раз можно перемещать только один диск.
        3. Меньший диск всегда должен лежать на большем.

        Рисунок 12-10   Пример задачи о Ханойской башне

        Обозначим задачу о Ханойской башне размера \\(i\\) как \\(f(i)\\) . Например, \\(f(3)\\) означает задачу перемещения 3 дисков со стержня A на стержень C .

        ","path":["Глава 12. Разделяй и властвуй","12.4   Задача о Ханойской башне"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/#1","level":3,"title":"1.   Рассмотрим базовые случаи","text":"

        Как показано на рисунке 12-11, для задачи \\(f(1)\\) , то есть когда имеется только один диск, достаточно просто переместить его напрямую со стержня A на стержень C .

        <1><2>

        Рисунок 12-11   Решение задачи размера 1

        Как показано на рисунке 12-12, для задачи \\(f(2)\\) , то есть когда есть два диска, поскольку меньший диск все время должен лежать на большем, приходится использовать B как вспомогательный стержень.

        1. Сначала переместить верхний маленький диск с A на B .
        2. Затем переместить большой диск с A на C .
        3. Наконец, переместить маленький диск с B на C .
        <1><2><3><4>

        Рисунок 12-12   Решение задачи размера 2

        Процесс решения задачи \\(f(2)\\) можно кратко описать так: переместить два диска с A на C с помощью B . Здесь C называется целевым стержнем, а B - буферным стержнем.

        ","path":["Глава 12. Разделяй и властвуй","12.4   Задача о Ханойской башне"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/#2","level":3,"title":"2.   Разбиение на подзадачи","text":"

        Для задачи \\(f(3)\\) , то есть когда имеется три диска, ситуация становится сложнее.

        Поскольку решения \\(f(1)\\) и \\(f(2)\\) уже известны, можно подойти к задаче с точки зрения стратегии \"разделяй и властвуй\" и рассматривать два верхних диска на A как единое целое, выполняя шаги, показанные на рисунке 12-13. Так три диска успешно перемещаются с A на C .

        1. Сделать B целевым стержнем, а C буферным, и переместить два диска с A на B .
        2. Переместить оставшийся один диск с A напрямую на C .
        3. Сделать C целевым стержнем, а A буферным, и переместить два диска с B на C .
        <1><2><3><4>

        Рисунок 12-13   Решение задачи размера 3

        Иначе говоря, мы разбиваем задачу \\(f(3)\\) на две подзадачи \\(f(2)\\) и одну подзадачу \\(f(1)\\) . Если последовательно решить эти три подзадачи, исходная задача тоже будет решена. Это показывает, что подзадачи независимы и что их решения можно объединить.

        Таким образом, можно сформулировать показанную на рисунке 12-14 стратегию \"разделяй и властвуй\" для задачи о Ханойской башне: исходная задача \\(f(n)\\) разбивается на две подзадачи \\(f(n-1)\\) и одну подзадачу \\(f(1)\\) , которые затем решаются в следующем порядке.

        1. Переместить \\(n-1\\) дисков с A на B с помощью C .
        2. Переместить оставшийся \\(1\\) диск напрямую с A на C .
        3. Переместить \\(n-1\\) дисков с B на C с помощью A .

        Для двух подзадач \\(f(n-1)\\) можно применять тот же способ рекурсивного разбиения, пока не будет достигнута наименьшая подзадача \\(f(1)\\) . А решение для \\(f(1)\\) уже известно и требует всего одного перемещения.

        Рисунок 12-14   Стратегия разделяй и властвуй для решения задачи о Ханойской башне

        ","path":["Глава 12. Разделяй и властвуй","12.4   Задача о Ханойской башне"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/#3","level":3,"title":"3.   Реализация кода","text":"

        В коде мы объявляем рекурсивную функцию dfs(i, src, buf, tar) , которая перемещает \\(i\\) верхних дисков со стержня src на целевой стержень tar с помощью буферного стержня buf :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hanota.py
        def move(src: list[int], tar: list[int]):\n    \"\"\"Переместить один диск\"\"\"\n    # Снять диск с вершины src\n    pan = src.pop()\n    # Положить диск на вершину tar\n    tar.append(pan)\n\ndef dfs(i: int, src: list[int], buf: list[int], tar: list[int]):\n    \"\"\"Решить задачу Ханойской башни f(i)\"\"\"\n    # Если в src остался только один диск, сразу переместить его в tar\n    if i == 1:\n        move(src, tar)\n        return\n    # Подзадача f(i-1): переместить верхние i-1 дисков из src в buf с помощью tar\n    dfs(i - 1, src, tar, buf)\n    # Подзадача f(1): переместить оставшийся один диск из src в tar\n    move(src, tar)\n    # Подзадача f(i-1): переместить верхние i-1 дисков из buf в tar с помощью src\n    dfs(i - 1, buf, src, tar)\n\ndef solve_hanota(A: list[int], B: list[int], C: list[int]):\n    \"\"\"Решить задачу Ханойской башни\"\"\"\n    n = len(A)\n    # Переместить верхние n дисков из A в C с помощью B\n    dfs(n, A, B, C)\n
        hanota.cpp
        /* Переместить один диск */\nvoid move(vector<int> &src, vector<int> &tar) {\n    // Снять диск с вершины src\n    int pan = src.back();\n    src.pop_back();\n    // Положить диск на вершину tar\n    tar.push_back(pan);\n}\n\n/* Решить задачу Ханойской башни f(i) */\nvoid dfs(int i, vector<int> &src, vector<int> &buf, vector<int> &tar) {\n    // Если в src остался только один диск, сразу переместить его в tar\n    if (i == 1) {\n        move(src, tar);\n        return;\n    }\n    // Подзадача f(i-1): переместить верхние i-1 дисков из src в buf с помощью tar\n    dfs(i - 1, src, tar, buf);\n    // Подзадача f(1): переместить оставшийся один диск из src в tar\n    move(src, tar);\n    // Подзадача f(i-1): переместить верхние i-1 дисков из buf в tar с помощью src\n    dfs(i - 1, buf, src, tar);\n}\n\n/* Решить задачу Ханойской башни */\nvoid solveHanota(vector<int> &A, vector<int> &B, vector<int> &C) {\n    int n = A.size();\n    // Переместить верхние n дисков из A в C с помощью B\n    dfs(n, A, B, C);\n}\n
        hanota.java
        /* Переместить один диск */\nvoid move(List<Integer> src, List<Integer> tar) {\n    // Снять диск с вершины src\n    Integer pan = src.remove(src.size() - 1);\n    // Положить диск на вершину tar\n    tar.add(pan);\n}\n\n/* Решить задачу Ханойской башни f(i) */\nvoid dfs(int i, List<Integer> src, List<Integer> buf, List<Integer> tar) {\n    // Если в src остался только один диск, сразу переместить его в tar\n    if (i == 1) {\n        move(src, tar);\n        return;\n    }\n    // Подзадача f(i-1): переместить верхние i-1 дисков из src в buf с помощью tar\n    dfs(i - 1, src, tar, buf);\n    // Подзадача f(1): переместить оставшийся один диск из src в tar\n    move(src, tar);\n    // Подзадача f(i-1): переместить верхние i-1 дисков из buf в tar с помощью src\n    dfs(i - 1, buf, src, tar);\n}\n\n/* Решить задачу Ханойской башни */\nvoid solveHanota(List<Integer> A, List<Integer> B, List<Integer> C) {\n    int n = A.size();\n    // Переместить верхние n дисков из A в C с помощью B\n    dfs(n, A, B, C);\n}\n
        hanota.cs
        /* Переместить один диск */\nvoid Move(List<int> src, List<int> tar) {\n    // Снять диск с вершины src\n    int pan = src[^1];\n    src.RemoveAt(src.Count - 1);\n    // Положить диск на вершину tar\n    tar.Add(pan);\n}\n\n/* Решить задачу Ханойской башни f(i) */\nvoid DFS(int i, List<int> src, List<int> buf, List<int> tar) {\n    // Если в src остался только один диск, сразу переместить его в tar\n    if (i == 1) {\n        Move(src, tar);\n        return;\n    }\n    // Подзадача f(i-1): переместить верхние i-1 дисков из src в buf с помощью tar\n    DFS(i - 1, src, tar, buf);\n    // Подзадача f(1): переместить оставшийся один диск из src в tar\n    Move(src, tar);\n    // Подзадача f(i-1): переместить верхние i-1 дисков из buf в tar с помощью src\n    DFS(i - 1, buf, src, tar);\n}\n\n/* Решить задачу Ханойской башни */\nvoid SolveHanota(List<int> A, List<int> B, List<int> C) {\n    int n = A.Count;\n    // Переместить верхние n дисков из A в C с помощью B\n    DFS(n, A, B, C);\n}\n
        hanota.go
        /* Переместить один диск */\nfunc move(src, tar *list.List) {\n    // Снять диск с вершины src\n    pan := src.Back()\n    // Положить диск на вершину tar\n    tar.PushBack(pan.Value)\n    // Убрать верхний диск из src\n    src.Remove(pan)\n}\n\n/* Решить задачу Ханойской башни f(i) */\nfunc dfsHanota(i int, src, buf, tar *list.List) {\n    // Если в src остался только один диск, сразу переместить его в tar\n    if i == 1 {\n        move(src, tar)\n        return\n    }\n    // Подзадача f(i-1): переместить верхние i-1 дисков из src в buf с помощью tar\n    dfsHanota(i-1, src, tar, buf)\n    // Подзадача f(1): переместить оставшийся один диск из src в tar\n    move(src, tar)\n    // Подзадача f(i-1): переместить верхние i-1 дисков из buf в tar с помощью src\n    dfsHanota(i-1, buf, src, tar)\n}\n\n/* Решить задачу Ханойской башни */\nfunc solveHanota(A, B, C *list.List) {\n    n := A.Len()\n    // Переместить верхние n дисков из A в C с помощью B\n    dfsHanota(n, A, B, C)\n}\n
        hanota.swift
        /* Переместить один диск */\nfunc move(src: inout [Int], tar: inout [Int]) {\n    // Снять диск с вершины src\n    let pan = src.popLast()!\n    // Положить диск на вершину tar\n    tar.append(pan)\n}\n\n/* Решить задачу Ханойской башни f(i) */\nfunc dfs(i: Int, src: inout [Int], buf: inout [Int], tar: inout [Int]) {\n    // Если в src остался только один диск, сразу переместить его в tar\n    if i == 1 {\n        move(src: &src, tar: &tar)\n        return\n    }\n    // Подзадача f(i-1): переместить верхние i-1 дисков из src в buf с помощью tar\n    dfs(i: i - 1, src: &src, buf: &tar, tar: &buf)\n    // Подзадача f(1): переместить оставшийся один диск из src в tar\n    move(src: &src, tar: &tar)\n    // Подзадача f(i-1): переместить верхние i-1 дисков из buf в tar с помощью src\n    dfs(i: i - 1, src: &buf, buf: &src, tar: &tar)\n}\n\n/* Решить задачу Ханойской башни */\nfunc solveHanota(A: inout [Int], B: inout [Int], C: inout [Int]) {\n    let n = A.count\n    // Хвост списка соответствует вершине столбца\n    // Переместить верхние n дисков из src в C с помощью B\n    dfs(i: n, src: &A, buf: &B, tar: &C)\n}\n
        hanota.js
        /* Переместить один диск */\nfunction move(src, tar) {\n    // Снять диск с вершины src\n    const pan = src.pop();\n    // Положить диск на вершину tar\n    tar.push(pan);\n}\n\n/* Решить задачу Ханойской башни f(i) */\nfunction dfs(i, src, buf, tar) {\n    // Если в src остался только один диск, сразу переместить его в tar\n    if (i === 1) {\n        move(src, tar);\n        return;\n    }\n    // Подзадача f(i-1): переместить верхние i-1 дисков из src в buf с помощью tar\n    dfs(i - 1, src, tar, buf);\n    // Подзадача f(1): переместить оставшийся один диск из src в tar\n    move(src, tar);\n    // Подзадача f(i-1): переместить верхние i-1 дисков из buf в tar с помощью src\n    dfs(i - 1, buf, src, tar);\n}\n\n/* Решить задачу Ханойской башни */\nfunction solveHanota(A, B, C) {\n    const n = A.length;\n    // Переместить верхние n дисков из A в C с помощью B\n    dfs(n, A, B, C);\n}\n
        hanota.ts
        /* Переместить один диск */\nfunction move(src: number[], tar: number[]): void {\n    // Снять диск с вершины src\n    const pan = src.pop();\n    // Положить диск на вершину tar\n    tar.push(pan);\n}\n\n/* Решить задачу Ханойской башни f(i) */\nfunction dfs(i: number, src: number[], buf: number[], tar: number[]): void {\n    // Если в src остался только один диск, сразу переместить его в tar\n    if (i === 1) {\n        move(src, tar);\n        return;\n    }\n    // Подзадача f(i-1): переместить верхние i-1 дисков из src в buf с помощью tar\n    dfs(i - 1, src, tar, buf);\n    // Подзадача f(1): переместить оставшийся один диск из src в tar\n    move(src, tar);\n    // Подзадача f(i-1): переместить верхние i-1 дисков из buf в tar с помощью src\n    dfs(i - 1, buf, src, tar);\n}\n\n/* Решить задачу Ханойской башни */\nfunction solveHanota(A: number[], B: number[], C: number[]): void {\n    const n = A.length;\n    // Переместить верхние n дисков из A в C с помощью B\n    dfs(n, A, B, C);\n}\n
        hanota.dart
        /* Переместить один диск */\nvoid move(List<int> src, List<int> tar) {\n  // Снять диск с вершины src\n  int pan = src.removeLast();\n  // Положить диск на вершину tar\n  tar.add(pan);\n}\n\n/* Решить задачу Ханойской башни f(i) */\nvoid dfs(int i, List<int> src, List<int> buf, List<int> tar) {\n  // Если в src остался только один диск, сразу переместить его в tar\n  if (i == 1) {\n    move(src, tar);\n    return;\n  }\n  // Подзадача f(i-1): переместить верхние i-1 дисков из src в buf с помощью tar\n  dfs(i - 1, src, tar, buf);\n  // Подзадача f(1): переместить оставшийся один диск из src в tar\n  move(src, tar);\n  // Подзадача f(i-1): переместить верхние i-1 дисков из buf в tar с помощью src\n  dfs(i - 1, buf, src, tar);\n}\n\n/* Решить задачу Ханойской башни */\nvoid solveHanota(List<int> A, List<int> B, List<int> C) {\n  int n = A.length;\n  // Переместить верхние n дисков из A в C с помощью B\n  dfs(n, A, B, C);\n}\n
        hanota.rs
        /* Переместить один диск */\nfn move_pan(src: &mut Vec<i32>, tar: &mut Vec<i32>) {\n    // Снять диск с вершины src\n    let pan = src.pop().unwrap();\n    // Положить диск на вершину tar\n    tar.push(pan);\n}\n\n/* Решить задачу Ханойской башни f(i) */\nfn dfs(i: i32, src: &mut Vec<i32>, buf: &mut Vec<i32>, tar: &mut Vec<i32>) {\n    // Если в src остался только один диск, сразу переместить его в tar\n    if i == 1 {\n        move_pan(src, tar);\n        return;\n    }\n    // Подзадача f(i-1): переместить верхние i-1 дисков из src в buf с помощью tar\n    dfs(i - 1, src, tar, buf);\n    // Подзадача f(1): переместить оставшийся один диск из src в tar\n    move_pan(src, tar);\n    // Подзадача f(i-1): переместить верхние i-1 дисков из buf в tar с помощью src\n    dfs(i - 1, buf, src, tar);\n}\n\n/* Решить задачу Ханойской башни */\nfn solve_hanota(A: &mut Vec<i32>, B: &mut Vec<i32>, C: &mut Vec<i32>) {\n    let n = A.len() as i32;\n    // Переместить верхние n дисков из A в C с помощью B\n    dfs(n, A, B, C);\n}\n
        hanota.c
        /* Переместить один диск */\nvoid move(int *src, int *srcSize, int *tar, int *tarSize) {\n    // Снять диск с вершины src\n    int pan = src[*srcSize - 1];\n    src[*srcSize - 1] = 0;\n    (*srcSize)--;\n    // Положить диск на вершину tar\n    tar[*tarSize] = pan;\n    (*tarSize)++;\n}\n\n/* Решить задачу Ханойской башни f(i) */\nvoid dfs(int i, int *src, int *srcSize, int *buf, int *bufSize, int *tar, int *tarSize) {\n    // Если в src остался только один диск, сразу переместить его в tar\n    if (i == 1) {\n        move(src, srcSize, tar, tarSize);\n        return;\n    }\n    // Подзадача f(i-1): переместить верхние i-1 дисков из src в buf с помощью tar\n    dfs(i - 1, src, srcSize, tar, tarSize, buf, bufSize);\n    // Подзадача f(1): переместить оставшийся один диск из src в tar\n    move(src, srcSize, tar, tarSize);\n    // Подзадача f(i-1): переместить верхние i-1 дисков из buf в tar с помощью src\n    dfs(i - 1, buf, bufSize, src, srcSize, tar, tarSize);\n}\n\n/* Решить задачу Ханойской башни */\nvoid solveHanota(int *A, int *ASize, int *B, int *BSize, int *C, int *CSize) {\n    // Переместить верхние n дисков из A в C с помощью B\n    dfs(*ASize, A, ASize, B, BSize, C, CSize);\n}\n
        hanota.kt
        /* Переместить один диск */\nfun move(src: MutableList<Int>, tar: MutableList<Int>) {\n    // Снять диск с вершины src\n    val pan = src.removeAt(src.size - 1)\n    // Положить диск на вершину tar\n    tar.add(pan)\n}\n\n/* Решить задачу Ханойской башни f(i) */\nfun dfs(i: Int, src: MutableList<Int>, buf: MutableList<Int>, tar: MutableList<Int>) {\n    // Если в src остался только один диск, сразу переместить его в tar\n    if (i == 1) {\n        move(src, tar)\n        return\n    }\n    // Подзадача f(i-1): переместить верхние i-1 дисков из src в buf с помощью tar\n    dfs(i - 1, src, tar, buf)\n    // Подзадача f(1): переместить оставшийся один диск из src в tar\n    move(src, tar)\n    // Подзадача f(i-1): переместить верхние i-1 дисков из buf в tar с помощью src\n    dfs(i - 1, buf, src, tar)\n}\n\n/* Решить задачу Ханойской башни */\nfun solveHanota(A: MutableList<Int>, B: MutableList<Int>, C: MutableList<Int>) {\n    val n = A.size\n    // Переместить верхние n дисков из A в C с помощью B\n    dfs(n, A, B, C)\n}\n
        hanota.rb
        ### Переместить один диск ###\ndef move(src, tar)\n  # Снять диск с вершины src\n  pan = src.pop\n  # Положить диск на вершину tar\n  tar << pan\nend\n\n### Решить задачу Ханойской башни f(i) ###\ndef dfs(i, src, buf, tar)\n  # Если в src остался только один диск, сразу переместить его в tar\n  if i == 1\n    move(src, tar)\n    return\n  end\n\n  # Подзадача f(i-1): переместить верхние i-1 дисков из src в buf с помощью tar\n  dfs(i - 1, src, tar, buf)\n  # Подзадача f(1): переместить оставшийся один диск из src в tar\n  move(src, tar)\n  # Подзадача f(i-1): переместить верхние i-1 дисков из buf в tar с помощью src\n  dfs(i - 1, buf, src, tar)\nend\n\n### Решить задачу Ханойской башни ###\ndef solve_hanota(_A, _B, _C)\n  n = _A.length\n  # Переместить верхние n дисков из A в C с помощью B\n  dfs(n, _A, _B, _C)\nend\n
        Визуализация кода

        Во весь экран >

        Как показано на рисунке 12-15, задача о Ханойской башне формирует дерево рекурсии высоты \\(n\\) , в котором каждый узел представляет подзадачу и соответствует одному открытому вызову dfs() ; поэтому временная сложность равна \\(O(2^n)\\) , а пространственная сложность равна \\(O(n)\\) .

        Рисунок 12-15   Дерево рекурсии задачи о Ханойской башне

        Quote

        Задача о Ханойской башне происходит из древней легенды. В одном из храмов древней Индии монахи имели три высоких алмазных стержня и \\(64\\) золотых диска разного размера. Монахи непрерывно перекладывали диски и верили, что в тот момент, когда последний диск будет правильно перенесен, мир подойдет к концу.

        Однако даже если бы монахи перемещали по одному диску в секунду, им понадобилось бы примерно \\(2^{64} \\approx 1.84×10^{19}\\) секунд, то есть около \\(585\\) миллиардов лет, что намного превышает текущую оценку возраста Вселенной. Поэтому, если легенда и верна, нам, вероятно, пока не о чем беспокоиться.

        ","path":["Глава 12. Разделяй и властвуй","12.4   Задача о Ханойской башне"],"tags":[]},{"location":"chapter_divide_and_conquer/summary/","level":1,"title":"12.5   Резюме","text":"","path":["Глава 12. Разделяй и властвуй","12.5   Резюме"],"tags":[]},{"location":"chapter_divide_and_conquer/summary/#1","level":3,"title":"1.   Ключевые выводы","text":"
        • \"Разделяй и властвуй\" - это распространенная стратегия проектирования алгоритмов, которая включает два этапа: разделение (декомпозицию) и объединение (синтез), и обычно реализуется с помощью рекурсии.
        • Критерии применимости этой стратегии к задаче включают: возможность разложения задачи, независимость подзадач и возможность объединения их решений.
        • Сортировка слиянием является типичным применением стратегии \"разделяй и властвуй\": она рекурсивно делит массив на два равных по длине подмассива, пока не останется массив из одного элемента, после чего начинает поэтапное объединение.
        • Использование стратегии \"разделяй и властвуй\" часто позволяет повысить эффективность алгоритма. С одной стороны, она уменьшает число операций; с другой - после разбиения способствует параллельной оптимизации на уровне системы.
        • \"Разделяй и властвуй\" не только помогает решать многие алгоритмические задачи, но и широко используется при проектировании структур данных и алгоритмов, поэтому его можно встретить буквально повсюду.
        • По сравнению с полным перебором адаптивный поиск работает эффективнее. Алгоритмы поиска со сложностью \\(O(\\log n)\\) обычно реализуются на основе стратегии \"разделяй и властвуй\".
        • Двоичный поиск - еще одно типичное применение стратегии \"разделяй и властвуй\", в котором отсутствует шаг объединения решений подзадач. Его можно реализовать рекурсивно, опираясь на эту стратегию.
        • В задаче построения двоичного дерева исходная задача построения дерева может быть разбита на две подзадачи: построение левого и правого поддеревьев, а реализуется это через разбиение индексных интервалов прямого и симметричного обходов.
        • В задаче о Ханойской башне задача размера \\(n\\) разбивается на две подзадачи размера \\(n-1\\) и одну подзадачу размера \\(1\\) . После последовательного решения этих трех подзадач исходная задача также оказывается решенной.
        ","path":["Глава 12. Разделяй и властвуй","12.5   Резюме"],"tags":[]},{"location":"chapter_dynamic_programming/","level":1,"title":"Глава 14.   Динамическое программирование","text":"

        Abstract

        Ручьи впадают в реки, а реки вливаются в море.

        Динамическое программирование собирает решения малых задач в ответ на большую задачу и шаг за шагом ведет нас к ее решению.

        ","path":["Глава 14. Динамическое программирование","Глава 14.   Динамическое программирование"],"tags":[]},{"location":"chapter_dynamic_programming/#_1","level":2,"title":"Содержание главы","text":"
        • 14.1   Первое знакомство с динамическим программированием
        • 14.2   Свойства задач динамического программирования
        • 14.3   Подход к решению задач динамического программирования
        • 14.4   Задача о рюкзаке 0-1
        • 14.5   Задача о полном рюкзаке
        • 14.6   Задача о расстоянии редактирования
        • 14.7   Резюме
        ","path":["Глава 14. Динамическое программирование","Глава 14.   Динамическое программирование"],"tags":[]},{"location":"chapter_dynamic_programming/dp_problem_features/","level":1,"title":"14.2   Свойства задач динамического программирования","text":"

        В предыдущем разделе мы увидели, как динамическое программирование решает исходную задачу через разложение на подзадачи. На самом деле разложение на подзадачи - это общий алгоритмический подход, но в методе \"разделяй и властвуй\", динамическом программировании и поиске с возвратом акценты расставлены по-разному.

        • Алгоритмы \"разделяй и властвуй\" рекурсивно раскладывают исходную задачу на несколько независимых подзадач, пока не будет достигнута наименьшая подзадача, а затем в процессе возврата объединяют решения подзадач в решение исходной задачи.
        • Динамическое программирование тоже раскладывает задачу рекурсивно, но его главное отличие от метода \"разделяй и властвуй\" в том, что подзадачи здесь зависят друг от друга и в процессе разложения возникает много перекрывающихся подзадач.
        • Алгоритм поиска с возвратом перебирает все возможные решения через попытки и откат и с помощью обрезки избегает ненужных ветвей поиска. Решение исходной задачи состоит из последовательности решений, и подзадачей можно считать префикс этой последовательности решений.

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

        ","path":["Глава 14. Динамическое программирование","14.2   Свойства задач динамического программирования"],"tags":[]},{"location":"chapter_dynamic_programming/dp_problem_features/#1421","level":2,"title":"14.2.1   Оптимальная подструктура","text":"

        Немного изменим задачу о подъеме по лестнице, чтобы нагляднее показать понятие оптимальной подструктуры.

        Минимальная стоимость подъема по лестнице

        Дана лестница, по которой можно подниматься на \\(1\\) или на \\(2\\) ступени за раз. На каждой ступени указано неотрицательное целое число, обозначающее цену попадания на эту ступень. Дан массив неотрицательных целых чисел \\(cost\\) , где \\(cost[i]\\) - это цена для ступени \\(i\\) , а \\(cost[0]\\) соответствует земле (начальной позиции). Найдите минимальную суммарную стоимость, необходимую для достижения вершины.

        Как показано на рисунке 14-6, если цены для ступеней \\(1\\) , \\(2\\) и \\(3\\) равны соответственно \\(1\\) , \\(10\\) и \\(1\\) , то минимальная стоимость подъема с земли на третью ступень равна \\(2\\) .

        Рисунок 14-6   Минимальная стоимость подъема на 3-ю ступень

        Пусть \\(dp[i]\\) обозначает накопленную стоимость подъема на ступень \\(i\\) . Поскольку на ступень \\(i\\) можно прийти только со ступени \\(i - 1\\) или со ступени \\(i - 2\\) , значение \\(dp[i]\\) может быть либо \\(dp[i - 1] + cost[i]\\) , либо \\(dp[i - 2] + cost[i]\\) . Чтобы минимизировать стоимость, нужно выбрать меньший из этих двух вариантов:

        \\[ dp[i] = \\min(dp[i-1], dp[i-2]) + cost[i] \\]

        Отсюда и возникает смысл оптимальной подструктуры: оптимальное решение исходной задачи строится из оптимальных решений подзадач.

        Очевидно, что эта задача обладает оптимальной подструктурой: мы берем лучшее из двух оптимальных решений подзадач \\(dp[i-1]\\) и \\(dp[i-2]\\) и на его основе строим оптимальное решение исходной задачи \\(dp[i]\\) .

        А обладает ли оптимальной подструктурой исходная задача о числе способов подъема по лестнице из прошлого раздела? Формально она не про оптимум, а про подсчет количества. Но если переформулировать ее как \"найдите максимальное количество способов\", мы неожиданно увидим, что хотя исходная задача осталась по сути той же, оптимальная подструктура стала явной: максимальное число способов добраться до ступени \\(n\\) равно сумме максимальных чисел способов добраться до ступеней \\(n-1\\) и \\(n-2\\) . То есть объяснение оптимальной подструктуры в разных задачах может быть довольно гибким.

        Зная уравнение перехода состояния, а также начальные состояния \\(dp[1] = cost[1]\\) и \\(dp[2] = cost[2]\\) , мы можем сразу написать код динамического программирования:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_cost_climbing_stairs_dp.py
        def min_cost_climbing_stairs_dp(cost: list[int]) -> int:\n    \"\"\"Минимальная стоимость подъема по лестнице: динамическое программирование\"\"\"\n    n = len(cost) - 1\n    if n == 1 or n == 2:\n        return cost[n]\n    # Инициализация таблицы dp для хранения решений подзадач\n    dp = [0] * (n + 1)\n    # Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1], dp[2] = cost[1], cost[2]\n    # Переход состояний: постепенное решение больших подзадач через меньшие\n    for i in range(3, n + 1):\n        dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]\n    return dp[n]\n
        min_cost_climbing_stairs_dp.cpp
        /* Минимальная стоимость подъема по лестнице: динамическое программирование */\nint minCostClimbingStairsDP(vector<int> &cost) {\n    int n = cost.size() - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    // Инициализация таблицы dp для хранения решений подзадач\n    vector<int> dp(n + 1);\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (int i = 3; i <= n; i++) {\n        dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    return dp[n];\n}\n
        min_cost_climbing_stairs_dp.java
        /* Минимальная стоимость подъема по лестнице: динамическое программирование */\nint minCostClimbingStairsDP(int[] cost) {\n    int n = cost.length - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    // Инициализация таблицы dp для хранения решений подзадач\n    int[] dp = new int[n + 1];\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (int i = 3; i <= n; i++) {\n        dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    return dp[n];\n}\n
        min_cost_climbing_stairs_dp.cs
        /* Минимальная стоимость подъема по лестнице: динамическое программирование */\nint MinCostClimbingStairsDP(int[] cost) {\n    int n = cost.Length - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    // Инициализация таблицы dp для хранения решений подзадач\n    int[] dp = new int[n + 1];\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (int i = 3; i <= n; i++) {\n        dp[i] = Math.Min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    return dp[n];\n}\n
        min_cost_climbing_stairs_dp.go
        /* Минимальная стоимость подъема по лестнице: динамическое программирование */\nfunc minCostClimbingStairsDP(cost []int) int {\n    n := len(cost) - 1\n    if n == 1 || n == 2 {\n        return cost[n]\n    }\n    min := func(a, b int) int {\n        if a < b {\n            return a\n        }\n        return b\n    }\n    // Инициализация таблицы dp для хранения решений подзадач\n    dp := make([]int, n+1)\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1] = cost[1]\n    dp[2] = cost[2]\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for i := 3; i <= n; i++ {\n        dp[i] = min(dp[i-1], dp[i-2]) + cost[i]\n    }\n    return dp[n]\n}\n
        min_cost_climbing_stairs_dp.swift
        /* Минимальная стоимость подъема по лестнице: динамическое программирование */\nfunc minCostClimbingStairsDP(cost: [Int]) -> Int {\n    let n = cost.count - 1\n    if n == 1 || n == 2 {\n        return cost[n]\n    }\n    // Инициализация таблицы dp для хранения решений подзадач\n    var dp = Array(repeating: 0, count: n + 1)\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1] = cost[1]\n    dp[2] = cost[2]\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for i in 3 ... n {\n        dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]\n    }\n    return dp[n]\n}\n
        min_cost_climbing_stairs_dp.js
        /* Минимальная стоимость подъема по лестнице: динамическое программирование */\nfunction minCostClimbingStairsDP(cost) {\n    const n = cost.length - 1;\n    if (n === 1 || n === 2) {\n        return cost[n];\n    }\n    // Инициализация таблицы dp для хранения решений подзадач\n    const dp = new Array(n + 1);\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (let i = 3; i <= n; i++) {\n        dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    return dp[n];\n}\n
        min_cost_climbing_stairs_dp.ts
        /* Минимальная стоимость подъема по лестнице: динамическое программирование */\nfunction minCostClimbingStairsDP(cost: Array<number>): number {\n    const n = cost.length - 1;\n    if (n === 1 || n === 2) {\n        return cost[n];\n    }\n    // Инициализация таблицы dp для хранения решений подзадач\n    const dp = new Array(n + 1);\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (let i = 3; i <= n; i++) {\n        dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    return dp[n];\n}\n
        min_cost_climbing_stairs_dp.dart
        /* Минимальная стоимость подъема по лестнице: динамическое программирование */\nint minCostClimbingStairsDP(List<int> cost) {\n  int n = cost.length - 1;\n  if (n == 1 || n == 2) return cost[n];\n  // Инициализация таблицы dp для хранения решений подзадач\n  List<int> dp = List.filled(n + 1, 0);\n  // Начальное состояние: заранее задать решения наименьших подзадач\n  dp[1] = cost[1];\n  dp[2] = cost[2];\n  // Переход состояний: постепенное решение больших подзадач через меньшие\n  for (int i = 3; i <= n; i++) {\n    dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];\n  }\n  return dp[n];\n}\n
        min_cost_climbing_stairs_dp.rs
        /* Минимальная стоимость подъема по лестнице: динамическое программирование */\nfn min_cost_climbing_stairs_dp(cost: &[i32]) -> i32 {\n    let n = cost.len() - 1;\n    if n == 1 || n == 2 {\n        return cost[n];\n    }\n    // Инициализация таблицы dp для хранения решений подзадач\n    let mut dp = vec![-1; n + 1];\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for i in 3..=n {\n        dp[i] = cmp::min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    dp[n]\n}\n
        min_cost_climbing_stairs_dp.c
        /* Минимальная стоимость подъема по лестнице: динамическое программирование */\nint minCostClimbingStairsDP(int cost[], int costSize) {\n    int n = costSize - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    // Инициализация таблицы dp для хранения решений подзадач\n    int *dp = calloc(n + 1, sizeof(int));\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (int i = 3; i <= n; i++) {\n        dp[i] = myMin(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    int res = dp[n];\n    // Освободить память\n    free(dp);\n    return res;\n}\n
        min_cost_climbing_stairs_dp.kt
        /* Минимальная стоимость подъема по лестнице: динамическое программирование */\nfun minCostClimbingStairsDP(cost: IntArray): Int {\n    val n = cost.size - 1\n    if (n == 1 || n == 2) return cost[n]\n    // Инициализация таблицы dp для хранения решений подзадач\n    val dp = IntArray(n + 1)\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1] = cost[1]\n    dp[2] = cost[2]\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (i in 3..n) {\n        dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]\n    }\n    return dp[n]\n}\n
        min_cost_climbing_stairs_dp.rb
        ### Минимальная стоимость подъема по лестнице: динамическое программирование ###\ndef min_cost_climbing_stairs_dp(cost)\n  n = cost.length - 1\n  return cost[n] if n == 1 || n == 2\n  # Инициализация таблицы dp для хранения решений подзадач\n  dp = Array.new(n + 1, 0)\n  # Начальное состояние: заранее задать решения наименьших подзадач\n  dp[1], dp[2] = cost[1], cost[2]\n  # Переход состояний: постепенное решение больших подзадач через меньшие\n  (3...(n + 1)).each { |i| dp[i] = [dp[i - 1], dp[i - 2]].min + cost[i] }\n  dp[n]\nend\n
        Визуализация кода

        Во весь экран >

        На рисунке 14-7 показан процесс динамического программирования для этой задачи.

        Рисунок 14-7   Процесс динамического программирования для минимальной стоимости подъема

        В этой задаче тоже можно оптимизировать пространство, сжав одномерное состояние в нулевое измерение и тем самым уменьшив пространственную сложность с \\(O(n)\\) до \\(O(1)\\) :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_cost_climbing_stairs_dp.py
        def min_cost_climbing_stairs_dp_comp(cost: list[int]) -> int:\n    \"\"\"Минимальная стоимость подъема по лестнице: динамическое программирование с оптимизацией памяти\"\"\"\n    n = len(cost) - 1\n    if n == 1 or n == 2:\n        return cost[n]\n    a, b = cost[1], cost[2]\n    for i in range(3, n + 1):\n        a, b = b, min(a, b) + cost[i]\n    return b\n
        min_cost_climbing_stairs_dp.cpp
        /* Минимальная стоимость подъема по лестнице: динамическое программирование с оптимизацией памяти */\nint minCostClimbingStairsDPComp(vector<int> &cost) {\n    int n = cost.size() - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    int a = cost[1], b = cost[2];\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
        min_cost_climbing_stairs_dp.java
        /* Минимальная стоимость подъема по лестнице: динамическое программирование с оптимизацией памяти */\nint minCostClimbingStairsDPComp(int[] cost) {\n    int n = cost.length - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    int a = cost[1], b = cost[2];\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = Math.min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
        min_cost_climbing_stairs_dp.cs
        /* Минимальная стоимость подъема по лестнице: динамическое программирование с оптимизацией памяти */\nint MinCostClimbingStairsDPComp(int[] cost) {\n    int n = cost.Length - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    int a = cost[1], b = cost[2];\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = Math.Min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
        min_cost_climbing_stairs_dp.go
        /* Минимальная стоимость подъема по лестнице: динамическое программирование с оптимизацией памяти */\nfunc minCostClimbingStairsDPComp(cost []int) int {\n    n := len(cost) - 1\n    if n == 1 || n == 2 {\n        return cost[n]\n    }\n    min := func(a, b int) int {\n        if a < b {\n            return a\n        }\n        return b\n    }\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    a, b := cost[1], cost[2]\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for i := 3; i <= n; i++ {\n        tmp := b\n        b = min(a, tmp) + cost[i]\n        a = tmp\n    }\n    return b\n}\n
        min_cost_climbing_stairs_dp.swift
        /* Минимальная стоимость подъема по лестнице: динамическое программирование с оптимизацией памяти */\nfunc minCostClimbingStairsDPComp(cost: [Int]) -> Int {\n    let n = cost.count - 1\n    if n == 1 || n == 2 {\n        return cost[n]\n    }\n    var (a, b) = (cost[1], cost[2])\n    for i in 3 ... n {\n        (a, b) = (b, min(a, b) + cost[i])\n    }\n    return b\n}\n
        min_cost_climbing_stairs_dp.js
        /* Минимальная стоимость подъема по лестнице: динамическое программирование с оптимизацией памяти */\nfunction minCostClimbingStairsDPComp(cost) {\n    const n = cost.length - 1;\n    if (n === 1 || n === 2) {\n        return cost[n];\n    }\n    let a = cost[1],\n        b = cost[2];\n    for (let i = 3; i <= n; i++) {\n        const tmp = b;\n        b = Math.min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
        min_cost_climbing_stairs_dp.ts
        /* Минимальная стоимость подъема по лестнице: динамическое программирование с оптимизацией памяти */\nfunction minCostClimbingStairsDPComp(cost: Array<number>): number {\n    const n = cost.length - 1;\n    if (n === 1 || n === 2) {\n        return cost[n];\n    }\n    let a = cost[1],\n        b = cost[2];\n    for (let i = 3; i <= n; i++) {\n        const tmp = b;\n        b = Math.min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
        min_cost_climbing_stairs_dp.dart
        /* Минимальная стоимость подъема по лестнице: динамическое программирование с оптимизацией памяти */\nint minCostClimbingStairsDPComp(List<int> cost) {\n  int n = cost.length - 1;\n  if (n == 1 || n == 2) return cost[n];\n  int a = cost[1], b = cost[2];\n  for (int i = 3; i <= n; i++) {\n    int tmp = b;\n    b = min(a, tmp) + cost[i];\n    a = tmp;\n  }\n  return b;\n}\n
        min_cost_climbing_stairs_dp.rs
        /* Минимальная стоимость подъема по лестнице: динамическое программирование с оптимизацией памяти */\nfn min_cost_climbing_stairs_dp_comp(cost: &[i32]) -> i32 {\n    let n = cost.len() - 1;\n    if n == 1 || n == 2 {\n        return cost[n];\n    };\n    let (mut a, mut b) = (cost[1], cost[2]);\n    for i in 3..=n {\n        let tmp = b;\n        b = cmp::min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    b\n}\n
        min_cost_climbing_stairs_dp.c
        /* Минимальная стоимость подъема по лестнице: динамическое программирование с оптимизацией памяти */\nint minCostClimbingStairsDPComp(int cost[], int costSize) {\n    int n = costSize - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    int a = cost[1], b = cost[2];\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = myMin(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
        min_cost_climbing_stairs_dp.kt
        /* Минимальная стоимость подъема по лестнице: динамическое программирование с оптимизацией памяти */\nfun minCostClimbingStairsDPComp(cost: IntArray): Int {\n    val n = cost.size - 1\n    if (n == 1 || n == 2) return cost[n]\n    var a = cost[1]\n    var b = cost[2]\n    for (i in 3..n) {\n        val tmp = b\n        b = min(a, tmp) + cost[i]\n        a = tmp\n    }\n    return b\n}\n
        min_cost_climbing_stairs_dp.rb
        ### Минимальная стоимость подъема по лестнице: динамическое программирование ###\ndef min_cost_climbing_stairs_dp(cost)\n  n = cost.length - 1\n  return cost[n] if n == 1 || n == 2\n  # Инициализация таблицы dp для хранения решений подзадач\n  dp = Array.new(n + 1, 0)\n  # Начальное состояние: заранее задать решения наименьших подзадач\n  dp[1], dp[2] = cost[1], cost[2]\n  # Переход состояний: постепенное решение больших подзадач через меньшие\n  (3...(n + 1)).each { |i| dp[i] = [dp[i - 1], dp[i - 2]].min + cost[i] }\n  dp[n]\nend\n\n# Минимальная стоимость подъема по лестнице: динамическое программирование с оптимизацией памяти\ndef min_cost_climbing_stairs_dp_comp(cost)\n  n = cost.length - 1\n  return cost[n] if n == 1 || n == 2\n  a, b = cost[1], cost[2]\n  (3...(n + 1)).each { |i| a, b = b, [a, b].min + cost[i] }\n  b\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 14. Динамическое программирование","14.2   Свойства задач динамического программирования"],"tags":[]},{"location":"chapter_dynamic_programming/dp_problem_features/#1422","level":2,"title":"14.2.2   Отсутствие последствий","text":"

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

        Для примера снова рассмотрим задачу о лестнице. Если дано состояние \\(i\\) , то из него можно перейти в состояния \\(i+1\\) и \\(i+2\\) , соответствующие прыжкам на \\(1\\) и на \\(2\\) ступени. Чтобы сделать один из этих выборов, не нужно знать, какими были состояния до \\(i\\) ; на будущее влияет только текущее состояние \\(i\\) .

        Однако если добавить в задачу дополнительное ограничение, ситуация изменится.

        Подъем по лестнице с ограничением

        Дана лестница из \\(n\\) ступеней. За один шаг можно подняться на \\(1\\) или на \\(2\\) ступени, но нельзя два раунда подряд прыгать на \\(1\\) ступень. Сколькими способами можно добраться до вершины?

        Как показано на рисунке 14-8, на третью ступень теперь существует только \\(2\\) допустимых способа добраться: вариант с тремя последовательными прыжками на \\(1\\) не удовлетворяет ограничению и потому отбрасывается.

        Рисунок 14-8   Число способов подняться на 3-ю ступень при наличии ограничения

        В этой задаче, если в предыдущем раунде был сделан прыжок на \\(1\\) ступень, то в следующем раунде уже обязательно нужно прыгнуть на \\(2\\) ступени. Иными словами, следующий выбор уже нельзя определить только по текущему состоянию (текущему номеру ступени) - он зависит еще и от предыдущего состояния (с какой ступени мы пришли в прошлый раз).

        Нетрудно заметить, что в таком виде задача больше не удовлетворяет свойству отсутствия последствий, а уравнение перехода состояния \\(dp[i] = dp[i-1] + dp[i-2]\\) перестает работать, потому что \\(dp[i-1]\\) соответствует прыжку на \\(1\\) ступень, но при этом включает множество вариантов, где предыдущий раунд тоже был прыжком на \\(1\\) ступень. Такие варианты уже нельзя напрямую учитывать в \\(dp[i]\\) , если мы хотим соблюдать ограничение.

        Поэтому нам нужно расширить определение состояния: состояние \\([i, j]\\) означает, что мы находимся на ступени \\(i\\) и в предыдущем раунде прыгнули на \\(j\\) ступеней, где \\(j \\in \\{1, 2\\}\\) . Такое определение состояния эффективно различает, был ли в прошлом раунде прыжок на \\(1\\) или на \\(2\\) ступени, и позволяет корректно определить, откуда произошло текущее состояние.

        • Если в предыдущем раунде был прыжок на \\(1\\) ступень, то в раунде перед ним мог быть только прыжок на \\(2\\) ступени, то есть \\(dp[i, 1]\\) может перейти только из \\(dp[i-1, 2]\\) .
        • Если в предыдущем раунде был прыжок на \\(2\\) ступени, то еще шагом раньше можно было прыгнуть либо на \\(1\\) , либо на \\(2\\) ступени, то есть \\(dp[i, 2]\\) может переходить из \\(dp[i-2, 1]\\) или из \\(dp[i-2, 2]\\) .

        Как показано на рисунке 14-9, при таком определении \\(dp[i, j]\\) обозначает число способов для состояния \\([i, j]\\) . Тогда уравнение перехода состояния имеет вид:

        \\[ \\begin{cases} dp[i, 1] = dp[i-1, 2] \\\\ dp[i, 2] = dp[i-2, 1] + dp[i-2, 2] \\end{cases} \\]

        Рисунок 14-9   Рекуррентная связь с учетом ограничения

        В конце достаточно вернуть \\(dp[n, 1] + dp[n, 2]\\) ; эта сумма и представляет общее число способов добраться до ступени \\(n\\) :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_constraint_dp.py
        def climbing_stairs_constraint_dp(n: int) -> int:\n    \"\"\"Подъем по лестнице с ограничениями: динамическое программирование\"\"\"\n    if n == 1 or n == 2:\n        return 1\n    # Инициализация таблицы dp для хранения решений подзадач\n    dp = [[0] * 3 for _ in range(n + 1)]\n    # Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1][1], dp[1][2] = 1, 0\n    dp[2][1], dp[2][2] = 0, 1\n    # Переход состояний: постепенное решение больших подзадач через меньшие\n    for i in range(3, n + 1):\n        dp[i][1] = dp[i - 1][2]\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2]\n    return dp[n][1] + dp[n][2]\n
        climbing_stairs_constraint_dp.cpp
        /* Подъем по лестнице с ограничениями: динамическое программирование */\nint climbingStairsConstraintDP(int n) {\n    if (n == 1 || n == 2) {\n        return 1;\n    }\n    // Инициализация таблицы dp для хранения решений подзадач\n    vector<vector<int>> dp(n + 1, vector<int>(3, 0));\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (int i = 3; i <= n; i++) {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    return dp[n][1] + dp[n][2];\n}\n
        climbing_stairs_constraint_dp.java
        /* Подъем по лестнице с ограничениями: динамическое программирование */\nint climbingStairsConstraintDP(int n) {\n    if (n == 1 || n == 2) {\n        return 1;\n    }\n    // Инициализация таблицы dp для хранения решений подзадач\n    int[][] dp = new int[n + 1][3];\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (int i = 3; i <= n; i++) {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    return dp[n][1] + dp[n][2];\n}\n
        climbing_stairs_constraint_dp.cs
        /* Подъем по лестнице с ограничениями: динамическое программирование */\nint ClimbingStairsConstraintDP(int n) {\n    if (n == 1 || n == 2) {\n        return 1;\n    }\n    // Инициализация таблицы dp для хранения решений подзадач\n    int[,] dp = new int[n + 1, 3];\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1, 1] = 1;\n    dp[1, 2] = 0;\n    dp[2, 1] = 0;\n    dp[2, 2] = 1;\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (int i = 3; i <= n; i++) {\n        dp[i, 1] = dp[i - 1, 2];\n        dp[i, 2] = dp[i - 2, 1] + dp[i - 2, 2];\n    }\n    return dp[n, 1] + dp[n, 2];\n}\n
        climbing_stairs_constraint_dp.go
        /* Подъем по лестнице с ограничениями: динамическое программирование */\nfunc climbingStairsConstraintDP(n int) int {\n    if n == 1 || n == 2 {\n        return 1\n    }\n    // Инициализация таблицы dp для хранения решений подзадач\n    dp := make([][3]int, n+1)\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1][1] = 1\n    dp[1][2] = 0\n    dp[2][1] = 0\n    dp[2][2] = 1\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for i := 3; i <= n; i++ {\n        dp[i][1] = dp[i-1][2]\n        dp[i][2] = dp[i-2][1] + dp[i-2][2]\n    }\n    return dp[n][1] + dp[n][2]\n}\n
        climbing_stairs_constraint_dp.swift
        /* Подъем по лестнице с ограничениями: динамическое программирование */\nfunc climbingStairsConstraintDP(n: Int) -> Int {\n    if n == 1 || n == 2 {\n        return 1\n    }\n    // Инициализация таблицы dp для хранения решений подзадач\n    var dp = Array(repeating: Array(repeating: 0, count: 3), count: n + 1)\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1][1] = 1\n    dp[1][2] = 0\n    dp[2][1] = 0\n    dp[2][2] = 1\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for i in 3 ... n {\n        dp[i][1] = dp[i - 1][2]\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2]\n    }\n    return dp[n][1] + dp[n][2]\n}\n
        climbing_stairs_constraint_dp.js
        /* Подъем по лестнице с ограничениями: динамическое программирование */\nfunction climbingStairsConstraintDP(n) {\n    if (n === 1 || n === 2) {\n        return 1;\n    }\n    // Инициализация таблицы dp для хранения решений подзадач\n    const dp = Array.from(new Array(n + 1), () => new Array(3));\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (let i = 3; i <= n; i++) {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    return dp[n][1] + dp[n][2];\n}\n
        climbing_stairs_constraint_dp.ts
        /* Подъем по лестнице с ограничениями: динамическое программирование */\nfunction climbingStairsConstraintDP(n: number): number {\n    if (n === 1 || n === 2) {\n        return 1;\n    }\n    // Инициализация таблицы dp для хранения решений подзадач\n    const dp = Array.from({ length: n + 1 }, () => new Array(3));\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (let i = 3; i <= n; i++) {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    return dp[n][1] + dp[n][2];\n}\n
        climbing_stairs_constraint_dp.dart
        /* Подъем по лестнице с ограничениями: динамическое программирование */\nint climbingStairsConstraintDP(int n) {\n  if (n == 1 || n == 2) {\n    return 1;\n  }\n  // Инициализация таблицы dp для хранения решений подзадач\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(3, 0));\n  // Начальное состояние: заранее задать решения наименьших подзадач\n  dp[1][1] = 1;\n  dp[1][2] = 0;\n  dp[2][1] = 0;\n  dp[2][2] = 1;\n  // Переход состояний: постепенное решение больших подзадач через меньшие\n  for (int i = 3; i <= n; i++) {\n    dp[i][1] = dp[i - 1][2];\n    dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n  }\n  return dp[n][1] + dp[n][2];\n}\n
        climbing_stairs_constraint_dp.rs
        /* Подъем по лестнице с ограничениями: динамическое программирование */\nfn climbing_stairs_constraint_dp(n: usize) -> i32 {\n    if n == 1 || n == 2 {\n        return 1;\n    };\n    // Инициализация таблицы dp для хранения решений подзадач\n    let mut dp = vec![vec![-1; 3]; n + 1];\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for i in 3..=n {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    dp[n][1] + dp[n][2]\n}\n
        climbing_stairs_constraint_dp.c
        /* Подъем по лестнице с ограничениями: динамическое программирование */\nint climbingStairsConstraintDP(int n) {\n    if (n == 1 || n == 2) {\n        return 1;\n    }\n    // Инициализация таблицы dp для хранения решений подзадач\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(3, sizeof(int));\n    }\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (int i = 3; i <= n; i++) {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    int res = dp[n][1] + dp[n][2];\n    // Освободить память\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    free(dp);\n    return res;\n}\n
        climbing_stairs_constraint_dp.kt
        /* Подъем по лестнице с ограничениями: динамическое программирование */\nfun climbingStairsConstraintDP(n: Int): Int {\n    if (n == 1 || n == 2) {\n        return 1\n    }\n    // Инициализация таблицы dp для хранения решений подзадач\n    val dp = Array(n + 1) { IntArray(3) }\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1][1] = 1\n    dp[1][2] = 0\n    dp[2][1] = 0\n    dp[2][2] = 1\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (i in 3..n) {\n        dp[i][1] = dp[i - 1][2]\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2]\n    }\n    return dp[n][1] + dp[n][2]\n}\n
        climbing_stairs_constraint_dp.rb
        ### Подъем по лестнице с ограничениями: динамическое программирование ###\ndef climbing_stairs_constraint_dp(n)\n  return 1 if n == 1 || n == 2\n\n  # Инициализация таблицы dp для хранения решений подзадач\n  dp = Array.new(n + 1) { Array.new(3, 0) }\n  # Начальное состояние: заранее задать решения наименьших подзадач\n  dp[1][1], dp[1][2] = 1, 0\n  dp[2][1], dp[2][2] = 0, 1\n  # Переход состояний: постепенное решение больших подзадач через меньшие\n  for i in 3...(n + 1)\n    dp[i][1] = dp[i - 1][2]\n    dp[i][2] = dp[i - 2][1] + dp[i - 2][2]\n  end\n\n  dp[n][1] + dp[n][2]\nend\n
        Визуализация кода

        Во весь экран >

        В этом примере достаточно дополнительно учитывать только одно предыдущее состояние, поэтому после расширения определения состояния задача снова начинает удовлетворять свойству отсутствия последствий. Однако в некоторых задачах \"зависимость от прошлого\" бывает гораздо серьезнее.

        Подъем по лестнице с порождением препятствий

        Дана лестница из \\(n\\) ступеней. За один шаг можно подняться на \\(1\\) или на \\(2\\) ступени. При этом, если вы попали на ступень \\(i\\) , система автоматически создает препятствие на ступени \\(2i\\) , и на всех последующих шагах становиться на ступень \\(2i\\) уже нельзя. Например, если в первых двух раундах вы попали на ступени \\(2\\) и \\(3\\) , то после этого нельзя будет попадать на ступени \\(4\\) и \\(6\\) . Сколько существует способов добраться до вершины?

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

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

        ","path":["Глава 14. Динамическое программирование","14.2   Свойства задач динамического программирования"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/","level":1,"title":"14.3   Подход к решению задач динамического программирования","text":"

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

        1. Как определить, является ли некоторая задача задачей динамического программирования?
        2. С чего начинать решение такой задачи и как выглядит полный процесс решения?
        ","path":["Глава 14. Динамическое программирование","14.3   Подход к решению задач динамического программирования"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#1431","level":2,"title":"14.3.1   Определение задачи","text":"

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

        Задачи, подходящие для поиска с возвратом, обычно удовлетворяют \"модели дерева решений\". Такие задачи можно описать деревом, где каждый узел представляет одно решение, а каждый путь представляет последовательность решений.

        Иначе говоря, если в задаче есть четко выраженные решения и ответ порождается последовательностью таких решений, то она удовлетворяет модели дерева решений и обычно допускает решение через поиск с возвратом.

        Поверх этого у задач динамического программирования есть и некоторые дополнительные \"плюсы\".

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

        Соответственно, существуют и некоторые \"минусы\".

        • Цель задачи состоит в поиске всех возможных решений, а не одного оптимального решения.
        • В формулировке явно присутствуют признаки комбинаторного перечисления, и требуется вернуть сразу много конкретных вариантов.

        Если задача удовлетворяет модели дерева решений и имеет достаточно явные \"плюсы\", мы можем предположить, что это задача динамического программирования, а затем проверить это предположение уже в процессе решения.

        ","path":["Глава 14. Динамическое программирование","14.3   Подход к решению задач динамического программирования"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#1432","level":2,"title":"14.3.2   Этапы решения задачи","text":"

        Конкретный процесс решения задач динамического программирования зависит от природы и сложности задачи, но обычно включает следующие шаги: описание решений, определение состояний, построение таблицы \\(dp\\) , вывод уравнения перехода состояния, определение граничных условий и порядка переходов.

        Чтобы нагляднее показать этот процесс, рассмотрим классическую задачу \"минимальная сумма пути\".

        Question

        Дана двумерная сетка grid размера \\(n \\times m\\) , в каждой клетке которой записано неотрицательное целое число, означающее стоимость прохождения через эту клетку. Робот стартует из левой верхней клетки и за один шаг может двигаться только вправо или вниз, пока не достигнет правой нижней клетки. Верните минимальную сумму пути от левой верхней клетки до правой нижней.

        На рисунке 14-10 показан пример, в котором минимальная сумма пути равна \\(13\\) .

        Рисунок 14-10   Пример данных для задачи о минимальной сумме пути

        Шаг 1: понять решения на каждом раунде, определить состояние и тем самым получить таблицу \\(dp\\)

        В этой задаче на каждом раунде решение состоит в том, чтобы из текущей клетки сделать один шаг вниз или вправо. Пусть индексы строки и столбца текущей клетки равны \\([i, j]\\) ; тогда после шага вниз или вправо индексы становятся равными \\([i+1, j]\\) или \\([i, j+1]\\) . Значит, состояние должно включать два переменных индекса: строки и столбца, то есть состояние обозначается как \\([i, j]\\) .

        Подзадача, соответствующая состоянию \\([i, j]\\) , такова: минимальная сумма пути от стартовой клетки \\([0, 0]\\) до клетки \\([i, j]\\) . Ее решение обозначается через \\(dp[i, j]\\) .

        На этом этапе мы получаем двумерную матрицу \\(dp\\) , показанную на рисунке 14-11, размер которой совпадает с размером входной сетки grid .

        Рисунок 14-11   Определение состояния и таблицы dp

        Note

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

        Каждому состоянию соответствует некоторая подзадача, и для хранения решений всех подзадач мы определяем таблицу \\(dp\\) ; каждая независимая переменная состояния становится одним измерением таблицы \\(dp\\) . По сути таблица \\(dp\\) - это отображение от состояния к решению соответствующей подзадачи.

        Шаг 2: найти оптимальную подструктуру и на ее основе вывести уравнение перехода состояния

        Для состояния \\([i, j]\\) возможны только два источника: клетка сверху \\([i-1, j]\\) и клетка слева \\([i, j-1]\\) . Следовательно, оптимальная подструктура выглядит так: минимальная сумма пути до \\([i, j]\\) определяется меньшим из двух значений - минимальной суммы пути до \\([i-1, j]\\) и минимальной суммы пути до \\([i, j-1]\\) .

        По этому рассуждению получается уравнение перехода состояния, показанное на рисунке 14-12:

        \\[ dp[i, j] = \\min(dp[i-1, j], dp[i, j-1]) + grid[i, j] \\]

        Рисунок 14-12   Оптимальная подструктура и уравнение перехода состояния

        Note

        Опираясь на уже определенную таблицу \\(dp\\) , нужно продумать отношение между исходной задачей и подзадачами и найти способ построить оптимальное решение исходной задачи из оптимальных решений подзадач, то есть найти оптимальную подструктуру.

        Как только оптимальная подструктура найдена, на ее основе можно построить уравнение перехода состояния.

        Шаг 3: определить граничные условия и порядок переходов

        В этой задаче состояния в первой строке могут переходить только из клетки слева, а состояния в первом столбце - только из клетки сверху, поэтому первая строка \\(i = 0\\) и первый столбец \\(j = 0\\) образуют граничные условия.

        Как показано на рисунке 14-13, поскольку каждая клетка получается из клетки слева и клетки сверху, мы можем проходить матрицу циклами: внешний цикл по строкам, внутренний - по столбцам.

        Рисунок 14-13   Граничные условия и порядок перехода состояний

        Note

        В динамическом программировании граничные условия используются для инициализации таблицы \\(dp\\) , а в поиске - для обрезки.

        Смысл порядка перехода состояния в том, чтобы к моменту вычисления текущей подзадачи все более мелкие подзадачи, от которых она зависит, уже были вычислены корректно.

        После этого анализа мы уже можем напрямую написать код динамического программирования. Однако разложение на подзадачи - это мышление \"сверху вниз\", поэтому с точки зрения мышления более естественно реализовывать задачу в порядке \"полный перебор \\(\\rightarrow\\) поиск с мемоизацией \\(\\rightarrow\\) динамическое программирование\".

        ","path":["Глава 14. Динамическое программирование","14.3   Подход к решению задач динамического программирования"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#1-1","level":3,"title":"1.   Метод 1: полный перебор","text":"

        Начав со состояния \\([i, j]\\) , мы непрерывно раскладываем его на меньшие состояния \\([i-1, j]\\) и \\([i, j-1]\\) . Рекурсивная функция при этом имеет следующие элементы.

        • Параметры рекурсии: состояние \\([i, j]\\) .
        • Возвращаемое значение: минимальная сумма пути до \\([i, j]\\) , то есть \\(dp[i, j]\\) .
        • Условие завершения: когда \\(i = 0\\) и \\(j = 0\\) , возвращается стоимость \\(grid[0, 0]\\) .
        • Обрезка: если \\(i < 0\\) или \\(j < 0\\) , индекс выходит за границы, и в этом случае возвращается стоимость \\(+\\infty\\) , обозначающая невозможность.

        Код реализации:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py
        def min_path_sum_dfs(grid: list[list[int]], i: int, j: int) -> int:\n    \"\"\"Минимальная сумма пути: полный перебор\"\"\"\n    # Если это верхняя левая ячейка, завершить поиск\n    if i == 0 and j == 0:\n        return grid[0][0]\n    # Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if i < 0 or j < 0:\n        return inf\n    # Вычислить минимальную стоимость пути из левого верхнего угла до (i-1, j) и (i, j-1)\n    up = min_path_sum_dfs(grid, i - 1, j)\n    left = min_path_sum_dfs(grid, i, j - 1)\n    # Вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    return min(left, up) + grid[i][j]\n
        min_path_sum.cpp
        /* Минимальная сумма пути: полный перебор */\nint minPathSumDFS(vector<vector<int>> &grid, int i, int j) {\n    // Если это верхняя левая ячейка, завершить поиск\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if (i < 0 || j < 0) {\n        return INT_MAX;\n    }\n    // Вычислить минимальную стоимость пути из левого верхнего угла до (i-1, j) и (i, j-1)\n    int up = minPathSumDFS(grid, i - 1, j);\n    int left = minPathSumDFS(grid, i, j - 1);\n    // Вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    return min(left, up) != INT_MAX ? min(left, up) + grid[i][j] : INT_MAX;\n}\n
        min_path_sum.java
        /* Минимальная сумма пути: полный перебор */\nint minPathSumDFS(int[][] grid, int i, int j) {\n    // Если это верхняя левая ячейка, завершить поиск\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if (i < 0 || j < 0) {\n        return Integer.MAX_VALUE;\n    }\n    // Вычислить минимальную стоимость пути из левого верхнего угла до (i-1, j) и (i, j-1)\n    int up = minPathSumDFS(grid, i - 1, j);\n    int left = minPathSumDFS(grid, i, j - 1);\n    // Вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    return Math.min(left, up) + grid[i][j];\n}\n
        min_path_sum.cs
        /* Минимальная сумма пути: полный перебор */\nint MinPathSumDFS(int[][] grid, int i, int j) {\n    // Если это верхняя левая ячейка, завершить поиск\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if (i < 0 || j < 0) {\n        return int.MaxValue;\n    }\n    // Вычислить минимальную стоимость пути из левого верхнего угла до (i-1, j) и (i, j-1)\n    int up = MinPathSumDFS(grid, i - 1, j);\n    int left = MinPathSumDFS(grid, i, j - 1);\n    // Вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    return Math.Min(left, up) + grid[i][j];\n}\n
        min_path_sum.go
        /* Минимальная сумма пути: полный перебор */\nfunc minPathSumDFS(grid [][]int, i, j int) int {\n    // Если это верхняя левая ячейка, завершить поиск\n    if i == 0 && j == 0 {\n        return grid[0][0]\n    }\n    // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if i < 0 || j < 0 {\n        return math.MaxInt\n    }\n    // Вычислить минимальную стоимость пути из левого верхнего угла до (i-1, j) и (i, j-1)\n    up := minPathSumDFS(grid, i-1, j)\n    left := minPathSumDFS(grid, i, j-1)\n    // Вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    return int(math.Min(float64(left), float64(up))) + grid[i][j]\n}\n
        min_path_sum.swift
        /* Минимальная сумма пути: полный перебор */\nfunc minPathSumDFS(grid: [[Int]], i: Int, j: Int) -> Int {\n    // Если это верхняя левая ячейка, завершить поиск\n    if i == 0, j == 0 {\n        return grid[0][0]\n    }\n    // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if i < 0 || j < 0 {\n        return .max\n    }\n    // Вычислить минимальную стоимость пути из левого верхнего угла до (i-1, j) и (i, j-1)\n    let up = minPathSumDFS(grid: grid, i: i - 1, j: j)\n    let left = minPathSumDFS(grid: grid, i: i, j: j - 1)\n    // Вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    return min(left, up) + grid[i][j]\n}\n
        min_path_sum.js
        /* Минимальная сумма пути: полный перебор */\nfunction minPathSumDFS(grid, i, j) {\n    // Если это верхняя левая ячейка, завершить поиск\n    if (i === 0 && j === 0) {\n        return grid[0][0];\n    }\n    // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if (i < 0 || j < 0) {\n        return Infinity;\n    }\n    // Вычислить минимальную стоимость пути из левого верхнего угла до (i-1, j) и (i, j-1)\n    const up = minPathSumDFS(grid, i - 1, j);\n    const left = minPathSumDFS(grid, i, j - 1);\n    // Вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    return Math.min(left, up) + grid[i][j];\n}\n
        min_path_sum.ts
        /* Минимальная сумма пути: полный перебор */\nfunction minPathSumDFS(\n    grid: Array<Array<number>>,\n    i: number,\n    j: number\n): number {\n    // Если это верхняя левая ячейка, завершить поиск\n    if (i === 0 && j == 0) {\n        return grid[0][0];\n    }\n    // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if (i < 0 || j < 0) {\n        return Infinity;\n    }\n    // Вычислить минимальную стоимость пути из левого верхнего угла до (i-1, j) и (i, j-1)\n    const up = minPathSumDFS(grid, i - 1, j);\n    const left = minPathSumDFS(grid, i, j - 1);\n    // Вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    return Math.min(left, up) + grid[i][j];\n}\n
        min_path_sum.dart
        /* Минимальная сумма пути: полный перебор */\nint minPathSumDFS(List<List<int>> grid, int i, int j) {\n  // Если это верхняя левая ячейка, завершить поиск\n  if (i == 0 && j == 0) {\n    return grid[0][0];\n  }\n  // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n  if (i < 0 || j < 0) {\n    // В Dart тип int — целое число фиксированного диапазона; значения, представляющего «бесконечность», не существует\n    return BigInt.from(2).pow(31).toInt();\n  }\n  // Вычислить минимальную стоимость пути из левого верхнего угла до (i-1, j) и (i, j-1)\n  int up = minPathSumDFS(grid, i - 1, j);\n  int left = minPathSumDFS(grid, i, j - 1);\n  // Вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n  return min(left, up) + grid[i][j];\n}\n
        min_path_sum.rs
        /* Минимальная сумма пути: полный перебор */\nfn min_path_sum_dfs(grid: &Vec<Vec<i32>>, i: i32, j: i32) -> i32 {\n    // Если это верхняя левая ячейка, завершить поиск\n    if i == 0 && j == 0 {\n        return grid[0][0];\n    }\n    // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if i < 0 || j < 0 {\n        return i32::MAX;\n    }\n    // Вычислить минимальную стоимость пути из левого верхнего угла до (i-1, j) и (i, j-1)\n    let up = min_path_sum_dfs(grid, i - 1, j);\n    let left = min_path_sum_dfs(grid, i, j - 1);\n    // Вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    std::cmp::min(left, up) + grid[i as usize][j as usize]\n}\n
        min_path_sum.c
        /* Минимальная сумма пути: полный перебор */\nint minPathSumDFS(int grid[MAX_SIZE][MAX_SIZE], int i, int j) {\n    // Если это верхняя левая ячейка, завершить поиск\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if (i < 0 || j < 0) {\n        return INT_MAX;\n    }\n    // Вычислить минимальную стоимость пути из левого верхнего угла до (i-1, j) и (i, j-1)\n    int up = minPathSumDFS(grid, i - 1, j);\n    int left = minPathSumDFS(grid, i, j - 1);\n    // Вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    return myMin(left, up) != INT_MAX ? myMin(left, up) + grid[i][j] : INT_MAX;\n}\n
        min_path_sum.kt
        /* Минимальная сумма пути: полный перебор */\nfun minPathSumDFS(grid: Array<IntArray>, i: Int, j: Int): Int {\n    // Если это верхняя левая ячейка, завершить поиск\n    if (i == 0 && j == 0) {\n        return grid[0][0]\n    }\n    // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if (i < 0 || j < 0) {\n        return Int.MAX_VALUE\n    }\n    // Вычислить минимальную стоимость пути из левого верхнего угла до (i-1, j) и (i, j-1)\n    val up = minPathSumDFS(grid, i - 1, j)\n    val left = minPathSumDFS(grid, i, j - 1)\n    // Вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    return min(left, up) + grid[i][j]\n}\n
        min_path_sum.rb
        ### Минимальная сумма пути: полный перебор ###\ndef min_path_sum_dfs(grid, i, j)\n  # Если это верхняя левая ячейка, завершить поиск\n  return grid[i][j] if i == 0 && j == 0\n  # Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n  return Float::INFINITY if i < 0 || j < 0\n  # Вычислить минимальную стоимость пути из левого верхнего угла до (i-1, j) и (i, j-1)\n  up = min_path_sum_dfs(grid, i - 1, j)\n  left = min_path_sum_dfs(grid, i, j - 1)\n  # Вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n  [left, up].min + grid[i][j]\nend\n
        Визуализация кода

        Во весь экран >

        На рисунке 14-14 показано дерево рекурсии с корнем в \\(dp[2, 1]\\) ; в нем содержатся перекрывающиеся подзадачи, и их число будет резко расти вместе с размером сетки grid .

        По своей сути причина появления перекрывающихся подзадач такова: существует много разных путей от левого верхнего угла до одной и той же клетки.

        Рисунок 14-14   Дерево рекурсии полного перебора

        У каждого состояния есть два выбора - вниз и вправо, а от левого верхнего угла до правого нижнего нужно сделать всего \\(m + n - 2\\) шагов, поэтому худшая временная сложность равна \\(O(2^{m + n})\\) , где \\(n\\) и \\(m\\) - число строк и столбцов сетки соответственно. Заметим, что в этой оценке не учитывается близость к границам сетки: у граничных клеток остается только один выбор, так что фактическое число путей будет несколько меньше.

        ","path":["Глава 14. Динамическое программирование","14.3   Подход к решению задач динамического программирования"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#2-2","level":3,"title":"2.   Метод 2: поиск с мемоизацией","text":"

        Введем список памяти mem того же размера, что и сетка grid , для хранения решений всех подзадач и отсечения перекрывающихся подзадач:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py
        def min_path_sum_dfs_mem(\n    grid: list[list[int]], mem: list[list[int]], i: int, j: int\n) -> int:\n    \"\"\"Минимальная сумма пути: поиск с мемоизацией\"\"\"\n    # Если это верхняя левая ячейка, завершить поиск\n    if i == 0 and j == 0:\n        return grid[0][0]\n    # Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if i < 0 or j < 0:\n        return inf\n    # Если запись уже есть, вернуть сразу\n    if mem[i][j] != -1:\n        return mem[i][j]\n    # Минимальная стоимость пути для левой и верхней ячеек\n    up = min_path_sum_dfs_mem(grid, mem, i - 1, j)\n    left = min_path_sum_dfs_mem(grid, mem, i, j - 1)\n    # Сохранить и вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    mem[i][j] = min(left, up) + grid[i][j]\n    return mem[i][j]\n
        min_path_sum.cpp
        /* Минимальная сумма пути: поиск с мемоизацией */\nint minPathSumDFSMem(vector<vector<int>> &grid, vector<vector<int>> &mem, int i, int j) {\n    // Если это верхняя левая ячейка, завершить поиск\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if (i < 0 || j < 0) {\n        return INT_MAX;\n    }\n    // Если запись уже есть, вернуть сразу\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // Минимальная стоимость пути для левой и верхней ячеек\n    int up = minPathSumDFSMem(grid, mem, i - 1, j);\n    int left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // Сохранить и вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    mem[i][j] = min(left, up) != INT_MAX ? min(left, up) + grid[i][j] : INT_MAX;\n    return mem[i][j];\n}\n
        min_path_sum.java
        /* Минимальная сумма пути: поиск с мемоизацией */\nint minPathSumDFSMem(int[][] grid, int[][] mem, int i, int j) {\n    // Если это верхняя левая ячейка, завершить поиск\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if (i < 0 || j < 0) {\n        return Integer.MAX_VALUE;\n    }\n    // Если запись уже есть, вернуть сразу\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // Минимальная стоимость пути для левой и верхней ячеек\n    int up = minPathSumDFSMem(grid, mem, i - 1, j);\n    int left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // Сохранить и вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    mem[i][j] = Math.min(left, up) + grid[i][j];\n    return mem[i][j];\n}\n
        min_path_sum.cs
        /* Минимальная сумма пути: поиск с мемоизацией */\nint MinPathSumDFSMem(int[][] grid, int[][] mem, int i, int j) {\n    // Если это верхняя левая ячейка, завершить поиск\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if (i < 0 || j < 0) {\n        return int.MaxValue;\n    }\n    // Если запись уже есть, вернуть сразу\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // Минимальная стоимость пути для левой и верхней ячеек\n    int up = MinPathSumDFSMem(grid, mem, i - 1, j);\n    int left = MinPathSumDFSMem(grid, mem, i, j - 1);\n    // Сохранить и вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    mem[i][j] = Math.Min(left, up) + grid[i][j];\n    return mem[i][j];\n}\n
        min_path_sum.go
        /* Минимальная сумма пути: поиск с мемоизацией */\nfunc minPathSumDFSMem(grid, mem [][]int, i, j int) int {\n    // Если это верхняя левая ячейка, завершить поиск\n    if i == 0 && j == 0 {\n        return grid[0][0]\n    }\n    // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if i < 0 || j < 0 {\n        return math.MaxInt\n    }\n    // Если запись уже есть, вернуть сразу\n    if mem[i][j] != -1 {\n        return mem[i][j]\n    }\n    // Минимальная стоимость пути для левой и верхней ячеек\n    up := minPathSumDFSMem(grid, mem, i-1, j)\n    left := minPathSumDFSMem(grid, mem, i, j-1)\n    // Сохранить и вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    mem[i][j] = int(math.Min(float64(left), float64(up))) + grid[i][j]\n    return mem[i][j]\n}\n
        min_path_sum.swift
        /* Минимальная сумма пути: поиск с мемоизацией */\nfunc minPathSumDFSMem(grid: [[Int]], mem: inout [[Int]], i: Int, j: Int) -> Int {\n    // Если это верхняя левая ячейка, завершить поиск\n    if i == 0, j == 0 {\n        return grid[0][0]\n    }\n    // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if i < 0 || j < 0 {\n        return .max\n    }\n    // Если запись уже есть, вернуть сразу\n    if mem[i][j] != -1 {\n        return mem[i][j]\n    }\n    // Минимальная стоимость пути для левой и верхней ячеек\n    let up = minPathSumDFSMem(grid: grid, mem: &mem, i: i - 1, j: j)\n    let left = minPathSumDFSMem(grid: grid, mem: &mem, i: i, j: j - 1)\n    // Сохранить и вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    mem[i][j] = min(left, up) + grid[i][j]\n    return mem[i][j]\n}\n
        min_path_sum.js
        /* Минимальная сумма пути: поиск с мемоизацией */\nfunction minPathSumDFSMem(grid, mem, i, j) {\n    // Если это верхняя левая ячейка, завершить поиск\n    if (i === 0 && j === 0) {\n        return grid[0][0];\n    }\n    // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if (i < 0 || j < 0) {\n        return Infinity;\n    }\n    // Если запись уже есть, вернуть сразу\n    if (mem[i][j] !== -1) {\n        return mem[i][j];\n    }\n    // Минимальная стоимость пути для левой и верхней ячеек\n    const up = minPathSumDFSMem(grid, mem, i - 1, j);\n    const left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // Сохранить и вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    mem[i][j] = Math.min(left, up) + grid[i][j];\n    return mem[i][j];\n}\n
        min_path_sum.ts
        /* Минимальная сумма пути: поиск с мемоизацией */\nfunction minPathSumDFSMem(\n    grid: Array<Array<number>>,\n    mem: Array<Array<number>>,\n    i: number,\n    j: number\n): number {\n    // Если это верхняя левая ячейка, завершить поиск\n    if (i === 0 && j === 0) {\n        return grid[0][0];\n    }\n    // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if (i < 0 || j < 0) {\n        return Infinity;\n    }\n    // Если запись уже есть, вернуть сразу\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // Минимальная стоимость пути для левой и верхней ячеек\n    const up = minPathSumDFSMem(grid, mem, i - 1, j);\n    const left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // Сохранить и вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    mem[i][j] = Math.min(left, up) + grid[i][j];\n    return mem[i][j];\n}\n
        min_path_sum.dart
        /* Минимальная сумма пути: поиск с мемоизацией */\nint minPathSumDFSMem(List<List<int>> grid, List<List<int>> mem, int i, int j) {\n  // Если это верхняя левая ячейка, завершить поиск\n  if (i == 0 && j == 0) {\n    return grid[0][0];\n  }\n  // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n  if (i < 0 || j < 0) {\n    // В Dart тип int — целое число фиксированного диапазона; значения, представляющего «бесконечность», не существует\n    return BigInt.from(2).pow(31).toInt();\n  }\n  // Если запись уже есть, вернуть сразу\n  if (mem[i][j] != -1) {\n    return mem[i][j];\n  }\n  // Минимальная стоимость пути для левой и верхней ячеек\n  int up = minPathSumDFSMem(grid, mem, i - 1, j);\n  int left = minPathSumDFSMem(grid, mem, i, j - 1);\n  // Сохранить и вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n  mem[i][j] = min(left, up) + grid[i][j];\n  return mem[i][j];\n}\n
        min_path_sum.rs
        /* Минимальная сумма пути: поиск с мемоизацией */\nfn min_path_sum_dfs_mem(grid: &Vec<Vec<i32>>, mem: &mut Vec<Vec<i32>>, i: i32, j: i32) -> i32 {\n    // Если это верхняя левая ячейка, завершить поиск\n    if i == 0 && j == 0 {\n        return grid[0][0];\n    }\n    // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if i < 0 || j < 0 {\n        return i32::MAX;\n    }\n    // Если запись уже есть, вернуть сразу\n    if mem[i as usize][j as usize] != -1 {\n        return mem[i as usize][j as usize];\n    }\n    // Минимальная стоимость пути для левой и верхней ячеек\n    let up = min_path_sum_dfs_mem(grid, mem, i - 1, j);\n    let left = min_path_sum_dfs_mem(grid, mem, i, j - 1);\n    // Сохранить и вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    mem[i as usize][j as usize] = std::cmp::min(left, up) + grid[i as usize][j as usize];\n    mem[i as usize][j as usize]\n}\n
        min_path_sum.c
        /* Минимальная сумма пути: поиск с мемоизацией */\nint minPathSumDFSMem(int grid[MAX_SIZE][MAX_SIZE], int mem[MAX_SIZE][MAX_SIZE], int i, int j) {\n    // Если это верхняя левая ячейка, завершить поиск\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if (i < 0 || j < 0) {\n        return INT_MAX;\n    }\n    // Если запись уже есть, вернуть сразу\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // Минимальная стоимость пути для левой и верхней ячеек\n    int up = minPathSumDFSMem(grid, mem, i - 1, j);\n    int left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // Сохранить и вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    mem[i][j] = myMin(left, up) != INT_MAX ? myMin(left, up) + grid[i][j] : INT_MAX;\n    return mem[i][j];\n}\n
        min_path_sum.kt
        /* Минимальная сумма пути: поиск с мемоизацией */\nfun minPathSumDFSMem(\n    grid: Array<IntArray>,\n    mem: Array<IntArray>,\n    i: Int,\n    j: Int\n): Int {\n    // Если это верхняя левая ячейка, завершить поиск\n    if (i == 0 && j == 0) {\n        return grid[0][0]\n    }\n    // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if (i < 0 || j < 0) {\n        return Int.MAX_VALUE\n    }\n    // Если запись уже есть, вернуть сразу\n    if (mem[i][j] != -1) {\n        return mem[i][j]\n    }\n    // Минимальная стоимость пути для левой и верхней ячеек\n    val up = minPathSumDFSMem(grid, mem, i - 1, j)\n    val left = minPathSumDFSMem(grid, mem, i, j - 1)\n    // Сохранить и вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    mem[i][j] = min(left, up) + grid[i][j]\n    return mem[i][j]\n}\n
        min_path_sum.rb
        ### Минимальная сумма пути: поиск с мемоизацией ###\ndef min_path_sum_dfs_mem(grid, mem, i, j)\n  # Если это верхняя левая ячейка, завершить поиск\n  return grid[0][0] if i == 0 && j == 0\n  # Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n  return Float::INFINITY if i < 0 || j < 0\n  # Если запись уже есть, вернуть сразу\n  return mem[i][j] if mem[i][j] != -1\n  # Минимальная стоимость пути для левой и верхней ячеек\n  up = min_path_sum_dfs_mem(grid, mem, i - 1, j)\n  left = min_path_sum_dfs_mem(grid, mem, i, j - 1)\n  # Сохранить и вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n  mem[i][j] = [left, up].min + grid[i][j]\nend\n
        Визуализация кода

        Во весь экран >

        Как показано на рисунке 14-15, после добавления мемоизации решение каждой подзадачи вычисляется только один раз, поэтому временная сложность определяется общим числом состояний, то есть равна \\(O(nm)\\) .

        Рисунок 14-15   Дерево рекурсии для поиска с мемоизацией

        ","path":["Глава 14. Динамическое программирование","14.3   Подход к решению задач динамического программирования"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#3-3","level":3,"title":"3.   Метод 3: динамическое программирование","text":"

        Итеративная реализация динамического программирования выглядит так:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py
        def min_path_sum_dp(grid: list[list[int]]) -> int:\n    \"\"\"Минимальная сумма пути: динамическое программирование\"\"\"\n    n, m = len(grid), len(grid[0])\n    # Инициализация таблицы dp\n    dp = [[0] * m for _ in range(n)]\n    dp[0][0] = grid[0][0]\n    # Переход состояний: первая строка\n    for j in range(1, m):\n        dp[0][j] = dp[0][j - 1] + grid[0][j]\n    # Переход состояний: первый столбец\n    for i in range(1, n):\n        dp[i][0] = dp[i - 1][0] + grid[i][0]\n    # Переход состояний: остальные строки и столбцы\n    for i in range(1, n):\n        for j in range(1, m):\n            dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]\n    return dp[n - 1][m - 1]\n
        min_path_sum.cpp
        /* Минимальная сумма пути: динамическое программирование */\nint minPathSumDP(vector<vector<int>> &grid) {\n    int n = grid.size(), m = grid[0].size();\n    // Инициализация таблицы dp\n    vector<vector<int>> dp(n, vector<int>(m));\n    dp[0][0] = grid[0][0];\n    // Переход состояний: первая строка\n    for (int j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // Переход состояний: первый столбец\n    for (int i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (int i = 1; i < n; i++) {\n        for (int j = 1; j < m; j++) {\n            dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    return dp[n - 1][m - 1];\n}\n
        min_path_sum.java
        /* Минимальная сумма пути: динамическое программирование */\nint minPathSumDP(int[][] grid) {\n    int n = grid.length, m = grid[0].length;\n    // Инициализация таблицы dp\n    int[][] dp = new int[n][m];\n    dp[0][0] = grid[0][0];\n    // Переход состояний: первая строка\n    for (int j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // Переход состояний: первый столбец\n    for (int i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (int i = 1; i < n; i++) {\n        for (int j = 1; j < m; j++) {\n            dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    return dp[n - 1][m - 1];\n}\n
        min_path_sum.cs
        /* Минимальная сумма пути: динамическое программирование */\nint MinPathSumDP(int[][] grid) {\n    int n = grid.Length, m = grid[0].Length;\n    // Инициализация таблицы dp\n    int[,] dp = new int[n, m];\n    dp[0, 0] = grid[0][0];\n    // Переход состояний: первая строка\n    for (int j = 1; j < m; j++) {\n        dp[0, j] = dp[0, j - 1] + grid[0][j];\n    }\n    // Переход состояний: первый столбец\n    for (int i = 1; i < n; i++) {\n        dp[i, 0] = dp[i - 1, 0] + grid[i][0];\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (int i = 1; i < n; i++) {\n        for (int j = 1; j < m; j++) {\n            dp[i, j] = Math.Min(dp[i, j - 1], dp[i - 1, j]) + grid[i][j];\n        }\n    }\n    return dp[n - 1, m - 1];\n}\n
        min_path_sum.go
        /* Минимальная сумма пути: динамическое программирование */\nfunc minPathSumDP(grid [][]int) int {\n    n, m := len(grid), len(grid[0])\n    // Инициализация таблицы dp\n    dp := make([][]int, n)\n    for i := 0; i < n; i++ {\n        dp[i] = make([]int, m)\n    }\n    dp[0][0] = grid[0][0]\n    // Переход состояний: первая строка\n    for j := 1; j < m; j++ {\n        dp[0][j] = dp[0][j-1] + grid[0][j]\n    }\n    // Переход состояний: первый столбец\n    for i := 1; i < n; i++ {\n        dp[i][0] = dp[i-1][0] + grid[i][0]\n    }\n    // Переход состояний: остальные строки и столбцы\n    for i := 1; i < n; i++ {\n        for j := 1; j < m; j++ {\n            dp[i][j] = int(math.Min(float64(dp[i][j-1]), float64(dp[i-1][j]))) + grid[i][j]\n        }\n    }\n    return dp[n-1][m-1]\n}\n
        min_path_sum.swift
        /* Минимальная сумма пути: динамическое программирование */\nfunc minPathSumDP(grid: [[Int]]) -> Int {\n    let n = grid.count\n    let m = grid[0].count\n    // Инициализация таблицы dp\n    var dp = Array(repeating: Array(repeating: 0, count: m), count: n)\n    dp[0][0] = grid[0][0]\n    // Переход состояний: первая строка\n    for j in 1 ..< m {\n        dp[0][j] = dp[0][j - 1] + grid[0][j]\n    }\n    // Переход состояний: первый столбец\n    for i in 1 ..< n {\n        dp[i][0] = dp[i - 1][0] + grid[i][0]\n    }\n    // Переход состояний: остальные строки и столбцы\n    for i in 1 ..< n {\n        for j in 1 ..< m {\n            dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]\n        }\n    }\n    return dp[n - 1][m - 1]\n}\n
        min_path_sum.js
        /* Минимальная сумма пути: динамическое программирование */\nfunction minPathSumDP(grid) {\n    const n = grid.length,\n        m = grid[0].length;\n    // Инициализация таблицы dp\n    const dp = Array.from({ length: n }, () =>\n        Array.from({ length: m }, () => 0)\n    );\n    dp[0][0] = grid[0][0];\n    // Переход состояний: первая строка\n    for (let j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // Переход состояний: первый столбец\n    for (let i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (let i = 1; i < n; i++) {\n        for (let j = 1; j < m; j++) {\n            dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    return dp[n - 1][m - 1];\n}\n
        min_path_sum.ts
        /* Минимальная сумма пути: динамическое программирование */\nfunction minPathSumDP(grid: Array<Array<number>>): number {\n    const n = grid.length,\n        m = grid[0].length;\n    // Инициализация таблицы dp\n    const dp = Array.from({ length: n }, () =>\n        Array.from({ length: m }, () => 0)\n    );\n    dp[0][0] = grid[0][0];\n    // Переход состояний: первая строка\n    for (let j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // Переход состояний: первый столбец\n    for (let i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (let i = 1; i < n; i++) {\n        for (let j: number = 1; j < m; j++) {\n            dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    return dp[n - 1][m - 1];\n}\n
        min_path_sum.dart
        /* Минимальная сумма пути: динамическое программирование */\nint minPathSumDP(List<List<int>> grid) {\n  int n = grid.length, m = grid[0].length;\n  // Инициализация таблицы dp\n  List<List<int>> dp = List.generate(n, (i) => List.filled(m, 0));\n  dp[0][0] = grid[0][0];\n  // Переход состояний: первая строка\n  for (int j = 1; j < m; j++) {\n    dp[0][j] = dp[0][j - 1] + grid[0][j];\n  }\n  // Переход состояний: первый столбец\n  for (int i = 1; i < n; i++) {\n    dp[i][0] = dp[i - 1][0] + grid[i][0];\n  }\n  // Переход состояний: остальные строки и столбцы\n  for (int i = 1; i < n; i++) {\n    for (int j = 1; j < m; j++) {\n      dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n    }\n  }\n  return dp[n - 1][m - 1];\n}\n
        min_path_sum.rs
        /* Минимальная сумма пути: динамическое программирование */\nfn min_path_sum_dp(grid: &Vec<Vec<i32>>) -> i32 {\n    let (n, m) = (grid.len(), grid[0].len());\n    // Инициализация таблицы dp\n    let mut dp = vec![vec![0; m]; n];\n    dp[0][0] = grid[0][0];\n    // Переход состояний: первая строка\n    for j in 1..m {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // Переход состояний: первый столбец\n    for i in 1..n {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // Переход состояний: остальные строки и столбцы\n    for i in 1..n {\n        for j in 1..m {\n            dp[i][j] = std::cmp::min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    dp[n - 1][m - 1]\n}\n
        min_path_sum.c
        /* Минимальная сумма пути: динамическое программирование */\nint minPathSumDP(int grid[MAX_SIZE][MAX_SIZE], int n, int m) {\n    // Инициализация таблицы dp\n    int **dp = malloc(n * sizeof(int *));\n    for (int i = 0; i < n; i++) {\n        dp[i] = calloc(m, sizeof(int));\n    }\n    dp[0][0] = grid[0][0];\n    // Переход состояний: первая строка\n    for (int j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // Переход состояний: первый столбец\n    for (int i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (int i = 1; i < n; i++) {\n        for (int j = 1; j < m; j++) {\n            dp[i][j] = myMin(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    int res = dp[n - 1][m - 1];\n    // Освободить память\n    for (int i = 0; i < n; i++) {\n        free(dp[i]);\n    }\n    return res;\n}\n
        min_path_sum.kt
        /* Минимальная сумма пути: динамическое программирование */\nfun minPathSumDP(grid: Array<IntArray>): Int {\n    val n = grid.size\n    val m = grid[0].size\n    // Инициализация таблицы dp\n    val dp = Array(n) { IntArray(m) }\n    dp[0][0] = grid[0][0]\n    // Переход состояний: первая строка\n    for (j in 1..<m) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j]\n    }\n    // Переход состояний: первый столбец\n    for (i in 1..<n) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0]\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (i in 1..<n) {\n        for (j in 1..<m) {\n            dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]\n        }\n    }\n    return dp[n - 1][m - 1]\n}\n
        min_path_sum.rb
        ### Минимальная сумма пути: динамическое программирование ###\ndef min_path_sum_dp(grid)\n  n, m = grid.length, grid.first.length\n  # Инициализация таблицы dp\n  dp = Array.new(n) { Array.new(m, 0) }\n  dp[0][0] = grid[0][0]\n  # Переход состояний: первая строка\n  (1...m).each { |j| dp[0][j] = dp[0][j - 1] + grid[0][j] }\n  # Переход состояний: первый столбец\n  (1...n).each { |i| dp[i][0] = dp[i - 1][0] + grid[i][0] }\n  # Переход состояний: остальные строки и столбцы\n  for i in 1...n\n    for j in 1...m\n      dp[i][j] = [dp[i][j - 1], dp[i - 1][j]].min + grid[i][j]\n    end\n  end\n  dp[n -1][m -1]\nend\n
        Визуализация кода

        Во весь экран >

        На рисунке 14-16 показан процесс переходов состояний в задаче о минимальной сумме пути. Он проходит по всей сетке, поэтому временная сложность равна \\(O(nm)\\) .

        Размер массива dp равен \\(n \\times m\\) , поэтому пространственная сложность равна \\(O(nm)\\) .

        <1><2><3><4><5><6><7><8><9><10><11><12>

        Рисунок 14-16   Процесс динамического программирования для минимальной суммы пути

        ","path":["Глава 14. Динамическое программирование","14.3   Подход к решению задач динамического программирования"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#4","level":3,"title":"4.   Оптимизация пространства","text":"

        Поскольку каждая клетка зависит только от клетки слева и клетки сверху, таблицу \\(dp\\) можно реализовать с помощью одномерного массива, соответствующего одной строке.

        Обратите внимание: поскольку массив dp теперь может представлять только одну строку состояний, мы не можем заранее инициализировать состояния первого столбца, а должны обновлять их по мере обхода каждой строки:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py
        def min_path_sum_dp_comp(grid: list[list[int]]) -> int:\n    \"\"\"Минимальная сумма пути: динамическое программирование с оптимизацией памяти\"\"\"\n    n, m = len(grid), len(grid[0])\n    # Инициализация таблицы dp\n    dp = [0] * m\n    # Переход состояний: первая строка\n    dp[0] = grid[0][0]\n    for j in range(1, m):\n        dp[j] = dp[j - 1] + grid[0][j]\n    # Переход состояний: остальные строки\n    for i in range(1, n):\n        # Переход состояний: первый столбец\n        dp[0] = dp[0] + grid[i][0]\n        # Переход состояний: остальные столбцы\n        for j in range(1, m):\n            dp[j] = min(dp[j - 1], dp[j]) + grid[i][j]\n    return dp[m - 1]\n
        min_path_sum.cpp
        /* Минимальная сумма пути: динамическое программирование с оптимизацией памяти */\nint minPathSumDPComp(vector<vector<int>> &grid) {\n    int n = grid.size(), m = grid[0].size();\n    // Инициализация таблицы dp\n    vector<int> dp(m);\n    // Переход состояний: первая строка\n    dp[0] = grid[0][0];\n    for (int j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // Переход состояний: остальные строки\n    for (int i = 1; i < n; i++) {\n        // Переход состояний: первый столбец\n        dp[0] = dp[0] + grid[i][0];\n        // Переход состояний: остальные столбцы\n        for (int j = 1; j < m; j++) {\n            dp[j] = min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    return dp[m - 1];\n}\n
        min_path_sum.java
        /* Минимальная сумма пути: динамическое программирование с оптимизацией памяти */\nint minPathSumDPComp(int[][] grid) {\n    int n = grid.length, m = grid[0].length;\n    // Инициализация таблицы dp\n    int[] dp = new int[m];\n    // Переход состояний: первая строка\n    dp[0] = grid[0][0];\n    for (int j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // Переход состояний: остальные строки\n    for (int i = 1; i < n; i++) {\n        // Переход состояний: первый столбец\n        dp[0] = dp[0] + grid[i][0];\n        // Переход состояний: остальные столбцы\n        for (int j = 1; j < m; j++) {\n            dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    return dp[m - 1];\n}\n
        min_path_sum.cs
        /* Минимальная сумма пути: динамическое программирование с оптимизацией памяти */\nint MinPathSumDPComp(int[][] grid) {\n    int n = grid.Length, m = grid[0].Length;\n    // Инициализация таблицы dp\n    int[] dp = new int[m];\n    dp[0] = grid[0][0];\n    // Переход состояний: первая строка\n    for (int j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // Переход состояний: остальные строки\n    for (int i = 1; i < n; i++) {\n        // Переход состояний: первый столбец\n        dp[0] = dp[0] + grid[i][0];\n        // Переход состояний: остальные столбцы\n        for (int j = 1; j < m; j++) {\n            dp[j] = Math.Min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    return dp[m - 1];\n}\n
        min_path_sum.go
        /* Минимальная сумма пути: динамическое программирование с оптимизацией памяти */\nfunc minPathSumDPComp(grid [][]int) int {\n    n, m := len(grid), len(grid[0])\n    // Инициализация таблицы dp\n    dp := make([]int, m)\n    // Переход состояний: первая строка\n    dp[0] = grid[0][0]\n    for j := 1; j < m; j++ {\n        dp[j] = dp[j-1] + grid[0][j]\n    }\n    // Переход состояний: остальные строки и столбцы\n    for i := 1; i < n; i++ {\n        // Переход состояний: первый столбец\n        dp[0] = dp[0] + grid[i][0]\n        // Переход состояний: остальные столбцы\n        for j := 1; j < m; j++ {\n            dp[j] = int(math.Min(float64(dp[j-1]), float64(dp[j]))) + grid[i][j]\n        }\n    }\n    return dp[m-1]\n}\n
        min_path_sum.swift
        /* Минимальная сумма пути: динамическое программирование с оптимизацией памяти */\nfunc minPathSumDPComp(grid: [[Int]]) -> Int {\n    let n = grid.count\n    let m = grid[0].count\n    // Инициализация таблицы dp\n    var dp = Array(repeating: 0, count: m)\n    // Переход состояний: первая строка\n    dp[0] = grid[0][0]\n    for j in 1 ..< m {\n        dp[j] = dp[j - 1] + grid[0][j]\n    }\n    // Переход состояний: остальные строки\n    for i in 1 ..< n {\n        // Переход состояний: первый столбец\n        dp[0] = dp[0] + grid[i][0]\n        // Переход состояний: остальные столбцы\n        for j in 1 ..< m {\n            dp[j] = min(dp[j - 1], dp[j]) + grid[i][j]\n        }\n    }\n    return dp[m - 1]\n}\n
        min_path_sum.js
        /* Минимальная сумма пути: динамическое программирование с оптимизацией памяти */\nfunction minPathSumDPComp(grid) {\n    const n = grid.length,\n        m = grid[0].length;\n    // Инициализация таблицы dp\n    const dp = new Array(m);\n    // Переход состояний: первая строка\n    dp[0] = grid[0][0];\n    for (let j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // Переход состояний: остальные строки\n    for (let i = 1; i < n; i++) {\n        // Переход состояний: первый столбец\n        dp[0] = dp[0] + grid[i][0];\n        // Переход состояний: остальные столбцы\n        for (let j = 1; j < m; j++) {\n            dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    return dp[m - 1];\n}\n
        min_path_sum.ts
        /* Минимальная сумма пути: динамическое программирование с оптимизацией памяти */\nfunction minPathSumDPComp(grid: Array<Array<number>>): number {\n    const n = grid.length,\n        m = grid[0].length;\n    // Инициализация таблицы dp\n    const dp = new Array(m);\n    // Переход состояний: первая строка\n    dp[0] = grid[0][0];\n    for (let j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // Переход состояний: остальные строки\n    for (let i = 1; i < n; i++) {\n        // Переход состояний: первый столбец\n        dp[0] = dp[0] + grid[i][0];\n        // Переход состояний: остальные столбцы\n        for (let j = 1; j < m; j++) {\n            dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    return dp[m - 1];\n}\n
        min_path_sum.dart
        /* Минимальная сумма пути: динамическое программирование с оптимизацией памяти */\nint minPathSumDPComp(List<List<int>> grid) {\n  int n = grid.length, m = grid[0].length;\n  // Инициализация таблицы dp\n  List<int> dp = List.filled(m, 0);\n  dp[0] = grid[0][0];\n  for (int j = 1; j < m; j++) {\n    dp[j] = dp[j - 1] + grid[0][j];\n  }\n  // Переход состояний: остальные строки\n  for (int i = 1; i < n; i++) {\n    // Переход состояний: первый столбец\n    dp[0] = dp[0] + grid[i][0];\n    // Переход состояний: остальные столбцы\n    for (int j = 1; j < m; j++) {\n      dp[j] = min(dp[j - 1], dp[j]) + grid[i][j];\n    }\n  }\n  return dp[m - 1];\n}\n
        min_path_sum.rs
        /* Минимальная сумма пути: динамическое программирование с оптимизацией памяти */\nfn min_path_sum_dp_comp(grid: &Vec<Vec<i32>>) -> i32 {\n    let (n, m) = (grid.len(), grid[0].len());\n    // Инициализация таблицы dp\n    let mut dp = vec![0; m];\n    // Переход состояний: первая строка\n    dp[0] = grid[0][0];\n    for j in 1..m {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // Переход состояний: остальные строки\n    for i in 1..n {\n        // Переход состояний: первый столбец\n        dp[0] = dp[0] + grid[i][0];\n        // Переход состояний: остальные столбцы\n        for j in 1..m {\n            dp[j] = std::cmp::min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    dp[m - 1]\n}\n
        min_path_sum.c
        /* Минимальная сумма пути: динамическое программирование с оптимизацией памяти */\nint minPathSumDPComp(int grid[MAX_SIZE][MAX_SIZE], int n, int m) {\n    // Инициализация таблицы dp\n    int *dp = calloc(m, sizeof(int));\n    // Переход состояний: первая строка\n    dp[0] = grid[0][0];\n    for (int j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // Переход состояний: остальные строки\n    for (int i = 1; i < n; i++) {\n        // Переход состояний: первый столбец\n        dp[0] = dp[0] + grid[i][0];\n        // Переход состояний: остальные столбцы\n        for (int j = 1; j < m; j++) {\n            dp[j] = myMin(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    int res = dp[m - 1];\n    // Освободить память\n    free(dp);\n    return res;\n}\n
        min_path_sum.kt
        /* Минимальная сумма пути: динамическое программирование с оптимизацией памяти */\nfun minPathSumDPComp(grid: Array<IntArray>): Int {\n    val n = grid.size\n    val m = grid[0].size\n    // Инициализация таблицы dp\n    val dp = IntArray(m)\n    // Переход состояний: первая строка\n    dp[0] = grid[0][0]\n    for (j in 1..<m) {\n        dp[j] = dp[j - 1] + grid[0][j]\n    }\n    // Переход состояний: остальные строки\n    for (i in 1..<n) {\n        // Переход состояний: первый столбец\n        dp[0] = dp[0] + grid[i][0]\n        // Переход состояний: остальные столбцы\n        for (j in 1..<m) {\n            dp[j] = min(dp[j - 1], dp[j]) + grid[i][j]\n        }\n    }\n    return dp[m - 1]\n}\n
        min_path_sum.rb
        ### Минимальная сумма пути: динамическое программирование с оптимизацией памяти ###\ndef min_path_sum_dp_comp(grid)\n  n, m = grid.length, grid.first.length\n  # Инициализация таблицы dp\n  dp = Array.new(m, 0)\n  # Переход состояний: первая строка\n  dp[0] = grid[0][0]\n  (1...m).each { |j| dp[j] = dp[j - 1] + grid[0][j] }\n  # Переход состояний: остальные строки\n  for i in 1...n\n    # Переход состояний: первый столбец\n    dp[0] = dp[0] + grid[i][0]\n    # Переход состояний: остальные столбцы\n    (1...m).each { |j| dp[j] = [dp[j - 1], dp[j]].min + grid[i][j] }\n  end\n  dp[m - 1]\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 14. Динамическое программирование","14.3   Подход к решению задач динамического программирования"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/","level":1,"title":"14.6   Задача о расстоянии редактирования","text":"

        Расстояние редактирования, также называемое расстоянием Левенштейна, - это минимальное количество изменений, необходимых для преобразования одной строки в другую. Обычно оно используется для измерения сходства двух последовательностей в информационном поиске и обработке естественного языка.

        Question

        Даны две строки \\(s\\) и \\(t\\) . Верните минимальное число шагов редактирования, необходимое для преобразования \\(s\\) в \\(t\\) .

        Для строки допускаются три операции редактирования: вставка одного символа, удаление одного символа и замена одного символа на произвольный другой символ.

        Как показано на рисунке 14-27, для преобразования kitten в sitting требуется 3 шага редактирования: 2 операции замены и 1 операция вставки; для преобразования hello в algo также требуется 3 шага: 2 замены и 1 удаление.

        Рисунок 14-27   Пример данных для задачи о расстоянии редактирования

        Задачу о расстоянии редактирования можно естественным образом объяснить с помощью модели дерева решений. Строки соответствуют узлам дерева, а один шаг решения, то есть одна операция редактирования, соответствует одному ребру дерева.

        Как показано на рисунке 14-28, если не ограничивать число операций, то каждый узел может порождать множество ребер, и каждое из них соответствует одному из вариантов преобразования. Это означает, что преобразовать hello в algo можно множеством разных путей.

        С точки зрения дерева решений цель этой задачи - найти кратчайший путь между узлом hello и узлом algo .

        Рисунок 14-28   Представление задачи о расстоянии редактирования через дерево решений

        ","path":["Глава 14. Динамическое программирование","14.6   Задача о расстоянии редактирования"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/#1","level":3,"title":"1.   Идея динамического программирования","text":"

        Шаг 1: продумать решения на каждом раунде, определить состояние и тем самым получить таблицу \\(dp\\)

        На каждом раунде решение состоит в выполнении одной операции редактирования над строкой \\(s\\) .

        Нам нужно, чтобы в ходе выполнения операций размер задачи постепенно уменьшался; только тогда можно строить подзадачи. Пусть длины строк \\(s\\) и \\(t\\) равны соответственно \\(n\\) и \\(m\\) ; сначала рассмотрим последние символы этих строк, то есть \\(s[n-1]\\) и \\(t[m-1]\\) .

        • Если \\(s[n-1]\\) и \\(t[m-1]\\) совпадают, их можно просто пропустить и сразу перейти к сравнению \\(s[n-2]\\) и \\(t[m-2]\\) .
        • Если \\(s[n-1]\\) и \\(t[m-1]\\) различны, нужно выполнить над \\(s\\) одну операцию редактирования (вставку, удаление или замену), чтобы последние символы стали одинаковыми, после чего можно перейти к задаче меньшего размера.

        Иначе говоря, каждый шаг решения, то есть операция редактирования над строкой \\(s\\) , меняет те символы, которые еще необходимо сопоставить в строках \\(s\\) и \\(t\\) . Поэтому состояние определяется текущими позициями рассматриваемых символов в \\(s\\) и \\(t\\) , то есть состоянием \\([i, j]\\) .

        Подзадача, соответствующая состоянию \\([i, j]\\) , такова: минимальное число операций редактирования, необходимое для преобразования первых \\(i\\) символов строки \\(s\\) в первые \\(j\\) символов строки \\(t\\).

        Отсюда получается двумерная таблица \\(dp\\) размера \\((i+1) \\times (j+1)\\) .

        Шаг 2: найти оптимальную подструктуру и на ее основе вывести уравнение перехода состояния

        Рассмотрим подзадачу \\(dp[i, j]\\) . Ее последние символы - это \\(s[i-1]\\) и \\(t[j-1]\\) . В зависимости от операции редактирования возможны три случая, показанные на рисунке 14-29.

        1. Вставить после \\(s[i-1]\\) символ \\(t[j-1]\\) ; тогда остается подзадача \\(dp[i, j-1]\\) .
        2. Удалить \\(s[i-1]\\) ; тогда остается подзадача \\(dp[i-1, j]\\) .
        3. Заменить \\(s[i-1]\\) на \\(t[j-1]\\) ; тогда остается подзадача \\(dp[i-1, j-1]\\) .

        Рисунок 14-29   Переходы состояния в задаче о расстоянии редактирования

        Согласно этому анализу оптимальная подструктура такова: минимальное число шагов редактирования для \\(dp[i, j]\\) равно минимуму из трех значений - \\(dp[i, j-1]\\) , \\(dp[i-1, j]\\) и \\(dp[i-1, j-1]\\) - плюс \\(1\\) шаг за текущее редактирование. Значит, уравнение перехода состояния имеет вид:

        \\[ dp[i, j] = \\min(dp[i, j-1], dp[i-1, j], dp[i-1, j-1]) + 1 \\]

        Заметим, что если символы \\(s[i-1]\\) и \\(t[j-1]\\) совпадают, то редактировать текущий символ не нужно. В этом случае уравнение перехода состояния имеет вид:

        \\[ dp[i, j] = dp[i-1, j-1] \\]

        Шаг 3: определить граничные условия и порядок переходов

        Когда обе строки пусты, число операций редактирования равно \\(0\\) , то есть \\(dp[0, 0] = 0\\) . Когда строка \\(s\\) пуста, а строка \\(t\\) непуста, минимальное число операций равно длине строки \\(t\\) , то есть вся первая строка инициализируется как \\(dp[0, j] = j\\) . Когда строка \\(s\\) непуста, а строка \\(t\\) пуста, минимальное число операций равно длине строки \\(s\\) , то есть весь первый столбец инициализируется как \\(dp[i, 0] = i\\) .

        Из уравнения перехода видно, что решение \\(dp[i, j]\\) зависит от значений слева, сверху и слева сверху, поэтому всю таблицу \\(dp\\) можно обходить двумя вложенными циклами в прямом порядке.

        ","path":["Глава 14. Динамическое программирование","14.6   Задача о расстоянии редактирования"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/#2","level":3,"title":"2.   Реализация кода","text":"PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby edit_distance.py
        def edit_distance_dp(s: str, t: str) -> int:\n    \"\"\"Редакционное расстояние: динамическое программирование\"\"\"\n    n, m = len(s), len(t)\n    dp = [[0] * (m + 1) for _ in range(n + 1)]\n    # Переход состояний: первая строка и первый столбец\n    for i in range(1, n + 1):\n        dp[i][0] = i\n    for j in range(1, m + 1):\n        dp[0][j] = j\n    # Переход состояний: остальные строки и столбцы\n    for i in range(1, n + 1):\n        for j in range(1, m + 1):\n            if s[i - 1] == t[j - 1]:\n                # Если два символа равны, сразу пропустить их\n                dp[i][j] = dp[i - 1][j - 1]\n            else:\n                # Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[i][j] = min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1\n    return dp[n][m]\n
        edit_distance.cpp
        /* Редакционное расстояние: динамическое программирование */\nint editDistanceDP(string s, string t) {\n    int n = s.length(), m = t.length();\n    vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));\n    // Переход состояний: первая строка и первый столбец\n    for (int i = 1; i <= n; i++) {\n        dp[i][0] = i;\n    }\n    for (int j = 1; j <= m; j++) {\n        dp[0][j] = j;\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (int i = 1; i <= n; i++) {\n        for (int j = 1; j <= m; j++) {\n            if (s[i - 1] == t[j - 1]) {\n                // Если два символа равны, сразу пропустить их\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    return dp[n][m];\n}\n
        edit_distance.java
        /* Редакционное расстояние: динамическое программирование */\nint editDistanceDP(String s, String t) {\n    int n = s.length(), m = t.length();\n    int[][] dp = new int[n + 1][m + 1];\n    // Переход состояний: первая строка и первый столбец\n    for (int i = 1; i <= n; i++) {\n        dp[i][0] = i;\n    }\n    for (int j = 1; j <= m; j++) {\n        dp[0][j] = j;\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (int i = 1; i <= n; i++) {\n        for (int j = 1; j <= m; j++) {\n            if (s.charAt(i - 1) == t.charAt(j - 1)) {\n                // Если два символа равны, сразу пропустить их\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[i][j] = Math.min(Math.min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    return dp[n][m];\n}\n
        edit_distance.cs
        /* Редакционное расстояние: динамическое программирование */\nint EditDistanceDP(string s, string t) {\n    int n = s.Length, m = t.Length;\n    int[,] dp = new int[n + 1, m + 1];\n    // Переход состояний: первая строка и первый столбец\n    for (int i = 1; i <= n; i++) {\n        dp[i, 0] = i;\n    }\n    for (int j = 1; j <= m; j++) {\n        dp[0, j] = j;\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (int i = 1; i <= n; i++) {\n        for (int j = 1; j <= m; j++) {\n            if (s[i - 1] == t[j - 1]) {\n                // Если два символа равны, сразу пропустить их\n                dp[i, j] = dp[i - 1, j - 1];\n            } else {\n                // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[i, j] = Math.Min(Math.Min(dp[i, j - 1], dp[i - 1, j]), dp[i - 1, j - 1]) + 1;\n            }\n        }\n    }\n    return dp[n, m];\n}\n
        edit_distance.go
        /* Редакционное расстояние: динамическое программирование */\nfunc editDistanceDP(s string, t string) int {\n    n := len(s)\n    m := len(t)\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, m+1)\n    }\n    // Переход состояний: первая строка и первый столбец\n    for i := 1; i <= n; i++ {\n        dp[i][0] = i\n    }\n    for j := 1; j <= m; j++ {\n        dp[0][j] = j\n    }\n    // Переход состояний: остальные строки и столбцы\n    for i := 1; i <= n; i++ {\n        for j := 1; j <= m; j++ {\n            if s[i-1] == t[j-1] {\n                // Если два символа равны, сразу пропустить их\n                dp[i][j] = dp[i-1][j-1]\n            } else {\n                // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[i][j] = MinInt(MinInt(dp[i][j-1], dp[i-1][j]), dp[i-1][j-1]) + 1\n            }\n        }\n    }\n    return dp[n][m]\n}\n
        edit_distance.swift
        /* Редакционное расстояние: динамическое программирование */\nfunc editDistanceDP(s: String, t: String) -> Int {\n    let n = s.utf8CString.count\n    let m = t.utf8CString.count\n    var dp = Array(repeating: Array(repeating: 0, count: m + 1), count: n + 1)\n    // Переход состояний: первая строка и первый столбец\n    for i in 1 ... n {\n        dp[i][0] = i\n    }\n    for j in 1 ... m {\n        dp[0][j] = j\n    }\n    // Переход состояний: остальные строки и столбцы\n    for i in 1 ... n {\n        for j in 1 ... m {\n            if s.utf8CString[i - 1] == t.utf8CString[j - 1] {\n                // Если два символа равны, сразу пропустить их\n                dp[i][j] = dp[i - 1][j - 1]\n            } else {\n                // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1\n            }\n        }\n    }\n    return dp[n][m]\n}\n
        edit_distance.js
        /* Редакционное расстояние: динамическое программирование */\nfunction editDistanceDP(s, t) {\n    const n = s.length,\n        m = t.length;\n    const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));\n    // Переход состояний: первая строка и первый столбец\n    for (let i = 1; i <= n; i++) {\n        dp[i][0] = i;\n    }\n    for (let j = 1; j <= m; j++) {\n        dp[0][j] = j;\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (let i = 1; i <= n; i++) {\n        for (let j = 1; j <= m; j++) {\n            if (s.charAt(i - 1) === t.charAt(j - 1)) {\n                // Если два символа равны, сразу пропустить их\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[i][j] =\n                    Math.min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    return dp[n][m];\n}\n
        edit_distance.ts
        /* Редакционное расстояние: динамическое программирование */\nfunction editDistanceDP(s: string, t: string): number {\n    const n = s.length,\n        m = t.length;\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: m + 1 }, () => 0)\n    );\n    // Переход состояний: первая строка и первый столбец\n    for (let i = 1; i <= n; i++) {\n        dp[i][0] = i;\n    }\n    for (let j = 1; j <= m; j++) {\n        dp[0][j] = j;\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (let i = 1; i <= n; i++) {\n        for (let j = 1; j <= m; j++) {\n            if (s.charAt(i - 1) === t.charAt(j - 1)) {\n                // Если два символа равны, сразу пропустить их\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[i][j] =\n                    Math.min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    return dp[n][m];\n}\n
        edit_distance.dart
        /* Редакционное расстояние: динамическое программирование */\nint editDistanceDP(String s, String t) {\n  int n = s.length, m = t.length;\n  List<List<int>> dp = List.generate(n + 1, (_) => List.filled(m + 1, 0));\n  // Переход состояний: первая строка и первый столбец\n  for (int i = 1; i <= n; i++) {\n    dp[i][0] = i;\n  }\n  for (int j = 1; j <= m; j++) {\n    dp[0][j] = j;\n  }\n  // Переход состояний: остальные строки и столбцы\n  for (int i = 1; i <= n; i++) {\n    for (int j = 1; j <= m; j++) {\n      if (s[i - 1] == t[j - 1]) {\n        // Если два символа равны, сразу пропустить их\n        dp[i][j] = dp[i - 1][j - 1];\n      } else {\n        // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n        dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n      }\n    }\n  }\n  return dp[n][m];\n}\n
        edit_distance.rs
        /* Редакционное расстояние: динамическое программирование */\nfn edit_distance_dp(s: &str, t: &str) -> i32 {\n    let (n, m) = (s.len(), t.len());\n    let mut dp = vec![vec![0; m + 1]; n + 1];\n    // Переход состояний: первая строка и первый столбец\n    for i in 1..=n {\n        dp[i][0] = i as i32;\n    }\n    for j in 1..m {\n        dp[0][j] = j as i32;\n    }\n    // Переход состояний: остальные строки и столбцы\n    for i in 1..=n {\n        for j in 1..=m {\n            if s.chars().nth(i - 1) == t.chars().nth(j - 1) {\n                // Если два символа равны, сразу пропустить их\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[i][j] =\n                    std::cmp::min(std::cmp::min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    dp[n][m]\n}\n
        edit_distance.c
        /* Редакционное расстояние: динамическое программирование */\nint editDistanceDP(char *s, char *t, int n, int m) {\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(m + 1, sizeof(int));\n    }\n    // Переход состояний: первая строка и первый столбец\n    for (int i = 1; i <= n; i++) {\n        dp[i][0] = i;\n    }\n    for (int j = 1; j <= m; j++) {\n        dp[0][j] = j;\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (int i = 1; i <= n; i++) {\n        for (int j = 1; j <= m; j++) {\n            if (s[i - 1] == t[j - 1]) {\n                // Если два символа равны, сразу пропустить их\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[i][j] = myMin(myMin(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    int res = dp[n][m];\n    // Освободить память\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    return res;\n}\n
        edit_distance.kt
        /* Редакционное расстояние: динамическое программирование */\nfun editDistanceDP(s: String, t: String): Int {\n    val n = s.length\n    val m = t.length\n    val dp = Array(n + 1) { IntArray(m + 1) }\n    // Переход состояний: первая строка и первый столбец\n    for (i in 1..n) {\n        dp[i][0] = i\n    }\n    for (j in 1..m) {\n        dp[0][j] = j\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (i in 1..n) {\n        for (j in 1..m) {\n            if (s[i - 1] == t[j - 1]) {\n                // Если два символа равны, сразу пропустить их\n                dp[i][j] = dp[i - 1][j - 1]\n            } else {\n                // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1\n            }\n        }\n    }\n    return dp[n][m]\n}\n
        edit_distance.rb
        ### Редакционное расстояние: динамическое программирование ###\ndef edit_distance_dp(s, t)\n  n, m = s.length, t.length\n  dp = Array.new(n + 1) { Array.new(m + 1, 0) }\n  # Переход состояний: первая строка и первый столбец\n  (1...(n + 1)).each { |i| dp[i][0] = i }\n  (1...(m + 1)).each { |j| dp[0][j] = j }\n  # Переход состояний: остальные строки и столбцы\n  for i in 1...(n + 1)\n    for j in 1...(m +1)\n      if s[i - 1] == t[j - 1]\n        # Если два символа равны, сразу пропустить их\n        dp[i][j] = dp[i - 1][j - 1]\n      else\n        # Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n        dp[i][j] = [dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]].min + 1\n      end\n    end\n  end\n  dp[n][m]\nend\n
        Визуализация кода

        Во весь экран >

        Как показано на рисунке 14-30, процесс переходов состояния в задаче о расстоянии редактирования очень похож на задачи о рюкзаке: и там и здесь его можно рассматривать как заполнение двумерной сетки.

        <1><2><3><4><5><6><7><8><9><10><11><12><13><14><15>

        Рисунок 14-30   Процесс динамического программирования для расстояния редактирования

        ","path":["Глава 14. Динамическое программирование","14.6   Задача о расстоянии редактирования"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/#3","level":3,"title":"3.   Оптимизация пространства","text":"

        Поскольку \\(dp[i,j]\\) зависит от значения сверху \\(dp[i-1, j]\\) , слева \\(dp[i, j-1]\\) и слева сверху \\(dp[i-1, j-1]\\) , прямой обход после оптимизации памяти теряет значение слева сверху, а обратный обход не позволяет заранее построить значение слева \\(dp[i, j-1]\\) . Значит, оба наивных варианта обхода здесь непригодны.

        Чтобы решить эту проблему, можно использовать переменную leftup для временного сохранения значения слева сверху \\(dp[i-1, j-1]\\) ; после этого остается учитывать только верхнее и левое значения. Тогда ситуация становится аналогичной задаче о полном рюкзаке, и можно выполнять прямой обход. Код приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby edit_distance.py
        def edit_distance_dp_comp(s: str, t: str) -> int:\n    \"\"\"Редакционное расстояние: динамическое программирование с оптимизацией памяти\"\"\"\n    n, m = len(s), len(t)\n    dp = [0] * (m + 1)\n    # Переход состояний: первая строка\n    for j in range(1, m + 1):\n        dp[j] = j\n    # Переход состояний: остальные строки\n    for i in range(1, n + 1):\n        # Переход состояний: первый столбец\n        leftup = dp[0]  # Временно сохранить dp[i-1, j-1]\n        dp[0] += 1\n        # Переход состояний: остальные столбцы\n        for j in range(1, m + 1):\n            temp = dp[j]\n            if s[i - 1] == t[j - 1]:\n                # Если два символа равны, сразу пропустить их\n                dp[j] = leftup\n            else:\n                # Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[j] = min(dp[j - 1], dp[j], leftup) + 1\n            leftup = temp  # Обновить до значения dp[i-1, j-1] для следующей итерации\n    return dp[m]\n
        edit_distance.cpp
        /* Редакционное расстояние: динамическое программирование с оптимизацией памяти */\nint editDistanceDPComp(string s, string t) {\n    int n = s.length(), m = t.length();\n    vector<int> dp(m + 1, 0);\n    // Переход состояний: первая строка\n    for (int j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // Переход состояний: остальные строки\n    for (int i = 1; i <= n; i++) {\n        // Переход состояний: первый столбец\n        int leftup = dp[0]; // Временно сохранить dp[i-1, j-1]\n        dp[0] = i;\n        // Переход состояний: остальные столбцы\n        for (int j = 1; j <= m; j++) {\n            int temp = dp[j];\n            if (s[i - 1] == t[j - 1]) {\n                // Если два символа равны, сразу пропустить их\n                dp[j] = leftup;\n            } else {\n                // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // Обновить до значения dp[i-1, j-1] для следующей итерации\n        }\n    }\n    return dp[m];\n}\n
        edit_distance.java
        /* Редакционное расстояние: динамическое программирование с оптимизацией памяти */\nint editDistanceDPComp(String s, String t) {\n    int n = s.length(), m = t.length();\n    int[] dp = new int[m + 1];\n    // Переход состояний: первая строка\n    for (int j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // Переход состояний: остальные строки\n    for (int i = 1; i <= n; i++) {\n        // Переход состояний: первый столбец\n        int leftup = dp[0]; // Временно сохранить dp[i-1, j-1]\n        dp[0] = i;\n        // Переход состояний: остальные столбцы\n        for (int j = 1; j <= m; j++) {\n            int temp = dp[j];\n            if (s.charAt(i - 1) == t.charAt(j - 1)) {\n                // Если два символа равны, сразу пропустить их\n                dp[j] = leftup;\n            } else {\n                // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[j] = Math.min(Math.min(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // Обновить до значения dp[i-1, j-1] для следующей итерации\n        }\n    }\n    return dp[m];\n}\n
        edit_distance.cs
        /* Редакционное расстояние: динамическое программирование с оптимизацией памяти */\nint EditDistanceDPComp(string s, string t) {\n    int n = s.Length, m = t.Length;\n    int[] dp = new int[m + 1];\n    // Переход состояний: первая строка\n    for (int j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // Переход состояний: остальные строки\n    for (int i = 1; i <= n; i++) {\n        // Переход состояний: первый столбец\n        int leftup = dp[0]; // Временно сохранить dp[i-1, j-1]\n        dp[0] = i;\n        // Переход состояний: остальные столбцы\n        for (int j = 1; j <= m; j++) {\n            int temp = dp[j];\n            if (s[i - 1] == t[j - 1]) {\n                // Если два символа равны, сразу пропустить их\n                dp[j] = leftup;\n            } else {\n                // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[j] = Math.Min(Math.Min(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // Обновить до значения dp[i-1, j-1] для следующей итерации\n        }\n    }\n    return dp[m];\n}\n
        edit_distance.go
        /* Редакционное расстояние: динамическое программирование с оптимизацией памяти */\nfunc editDistanceDPComp(s string, t string) int {\n    n := len(s)\n    m := len(t)\n    dp := make([]int, m+1)\n    // Переход состояний: первая строка\n    for j := 1; j <= m; j++ {\n        dp[j] = j\n    }\n    // Переход состояний: остальные строки\n    for i := 1; i <= n; i++ {\n        // Переход состояний: первый столбец\n        leftUp := dp[0] // Временно сохранить dp[i-1, j-1]\n        dp[0] = i\n        // Переход состояний: остальные столбцы\n        for j := 1; j <= m; j++ {\n            temp := dp[j]\n            if s[i-1] == t[j-1] {\n                // Если два символа равны, сразу пропустить их\n                dp[j] = leftUp\n            } else {\n                // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[j] = MinInt(MinInt(dp[j-1], dp[j]), leftUp) + 1\n            }\n            leftUp = temp // Обновить до значения dp[i-1, j-1] для следующей итерации\n        }\n    }\n    return dp[m]\n}\n
        edit_distance.swift
        /* Редакционное расстояние: динамическое программирование с оптимизацией памяти */\nfunc editDistanceDPComp(s: String, t: String) -> Int {\n    let n = s.utf8CString.count\n    let m = t.utf8CString.count\n    var dp = Array(repeating: 0, count: m + 1)\n    // Переход состояний: первая строка\n    for j in 1 ... m {\n        dp[j] = j\n    }\n    // Переход состояний: остальные строки\n    for i in 1 ... n {\n        // Переход состояний: первый столбец\n        var leftup = dp[0] // Временно сохранить dp[i-1, j-1]\n        dp[0] = i\n        // Переход состояний: остальные столбцы\n        for j in 1 ... m {\n            let temp = dp[j]\n            if s.utf8CString[i - 1] == t.utf8CString[j - 1] {\n                // Если два символа равны, сразу пропустить их\n                dp[j] = leftup\n            } else {\n                // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1\n            }\n            leftup = temp // Обновить до значения dp[i-1, j-1] для следующей итерации\n        }\n    }\n    return dp[m]\n}\n
        edit_distance.js
        /* Редакционное расстояние: динамическое программирование с оптимизацией памяти */\nfunction editDistanceDPComp(s, t) {\n    const n = s.length,\n        m = t.length;\n    const dp = new Array(m + 1).fill(0);\n    // Переход состояний: первая строка\n    for (let j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // Переход состояний: остальные строки\n    for (let i = 1; i <= n; i++) {\n        // Переход состояний: первый столбец\n        let leftup = dp[0]; // Временно сохранить dp[i-1, j-1]\n        dp[0] = i;\n        // Переход состояний: остальные столбцы\n        for (let j = 1; j <= m; j++) {\n            const temp = dp[j];\n            if (s.charAt(i - 1) === t.charAt(j - 1)) {\n                // Если два символа равны, сразу пропустить их\n                dp[j] = leftup;\n            } else {\n                // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[j] = Math.min(dp[j - 1], dp[j], leftup) + 1;\n            }\n            leftup = temp; // Обновить до значения dp[i-1, j-1] для следующей итерации\n        }\n    }\n    return dp[m];\n}\n
        edit_distance.ts
        /* Редакционное расстояние: динамическое программирование с оптимизацией памяти */\nfunction editDistanceDPComp(s: string, t: string): number {\n    const n = s.length,\n        m = t.length;\n    const dp = new Array(m + 1).fill(0);\n    // Переход состояний: первая строка\n    for (let j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // Переход состояний: остальные строки\n    for (let i = 1; i <= n; i++) {\n        // Переход состояний: первый столбец\n        let leftup = dp[0]; // Временно сохранить dp[i-1, j-1]\n        dp[0] = i;\n        // Переход состояний: остальные столбцы\n        for (let j = 1; j <= m; j++) {\n            const temp = dp[j];\n            if (s.charAt(i - 1) === t.charAt(j - 1)) {\n                // Если два символа равны, сразу пропустить их\n                dp[j] = leftup;\n            } else {\n                // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[j] = Math.min(dp[j - 1], dp[j], leftup) + 1;\n            }\n            leftup = temp; // Обновить до значения dp[i-1, j-1] для следующей итерации\n        }\n    }\n    return dp[m];\n}\n
        edit_distance.dart
        /* Редакционное расстояние: динамическое программирование с оптимизацией памяти */\nint editDistanceDPComp(String s, String t) {\n  int n = s.length, m = t.length;\n  List<int> dp = List.filled(m + 1, 0);\n  // Переход состояний: первая строка\n  for (int j = 1; j <= m; j++) {\n    dp[j] = j;\n  }\n  // Переход состояний: остальные строки\n  for (int i = 1; i <= n; i++) {\n    // Переход состояний: первый столбец\n    int leftup = dp[0]; // Временно сохранить dp[i-1, j-1]\n    dp[0] = i;\n    // Переход состояний: остальные столбцы\n    for (int j = 1; j <= m; j++) {\n      int temp = dp[j];\n      if (s[i - 1] == t[j - 1]) {\n        // Если два символа равны, сразу пропустить их\n        dp[j] = leftup;\n      } else {\n        // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n        dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1;\n      }\n      leftup = temp; // Обновить до значения dp[i-1, j-1] для следующей итерации\n    }\n  }\n  return dp[m];\n}\n
        edit_distance.rs
        /* Редакционное расстояние: динамическое программирование с оптимизацией памяти */\nfn edit_distance_dp_comp(s: &str, t: &str) -> i32 {\n    let (n, m) = (s.len(), t.len());\n    let mut dp = vec![0; m + 1];\n    // Переход состояний: первая строка\n    for j in 1..m {\n        dp[j] = j as i32;\n    }\n    // Переход состояний: остальные строки\n    for i in 1..=n {\n        // Переход состояний: первый столбец\n        let mut leftup = dp[0]; // Временно сохранить dp[i-1, j-1]\n        dp[0] = i as i32;\n        // Переход состояний: остальные столбцы\n        for j in 1..=m {\n            let temp = dp[j];\n            if s.chars().nth(i - 1) == t.chars().nth(j - 1) {\n                // Если два символа равны, сразу пропустить их\n                dp[j] = leftup;\n            } else {\n                // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[j] = std::cmp::min(std::cmp::min(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // Обновить до значения dp[i-1, j-1] для следующей итерации\n        }\n    }\n    dp[m]\n}\n
        edit_distance.c
        /* Редакционное расстояние: динамическое программирование с оптимизацией памяти */\nint editDistanceDPComp(char *s, char *t, int n, int m) {\n    int *dp = calloc(m + 1, sizeof(int));\n    // Переход состояний: первая строка\n    for (int j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // Переход состояний: остальные строки\n    for (int i = 1; i <= n; i++) {\n        // Переход состояний: первый столбец\n        int leftup = dp[0]; // Временно сохранить dp[i-1, j-1]\n        dp[0] = i;\n        // Переход состояний: остальные столбцы\n        for (int j = 1; j <= m; j++) {\n            int temp = dp[j];\n            if (s[i - 1] == t[j - 1]) {\n                // Если два символа равны, сразу пропустить их\n                dp[j] = leftup;\n            } else {\n                // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[j] = myMin(myMin(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // Обновить до значения dp[i-1, j-1] для следующей итерации\n        }\n    }\n    int res = dp[m];\n    // Освободить память\n    free(dp);\n    return res;\n}\n
        edit_distance.kt
        /* Редакционное расстояние: динамическое программирование с оптимизацией памяти */\nfun editDistanceDPComp(s: String, t: String): Int {\n    val n = s.length\n    val m = t.length\n    val dp = IntArray(m + 1)\n    // Переход состояний: первая строка\n    for (j in 1..m) {\n        dp[j] = j\n    }\n    // Переход состояний: остальные строки\n    for (i in 1..n) {\n        // Переход состояний: первый столбец\n        var leftup = dp[0] // Временно сохранить dp[i-1, j-1]\n        dp[0] = i\n        // Переход состояний: остальные столбцы\n        for (j in 1..m) {\n            val temp = dp[j]\n            if (s[i - 1] == t[j - 1]) {\n                // Если два символа равны, сразу пропустить их\n                dp[j] = leftup\n            } else {\n                // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1\n            }\n            leftup = temp // Обновить до значения dp[i-1, j-1] для следующей итерации\n        }\n    }\n    return dp[m]\n}\n
        edit_distance.rb
        ### Редакционное расстояние: динамическое программирование с оптимизацией памяти ###\ndef edit_distance_dp_comp(s, t)\n  n, m = s.length, t.length\n  dp = Array.new(m + 1, 0)\n  # Переход состояний: первая строка\n  (1...(m + 1)).each { |j| dp[j] = j }\n  # Переход состояний: остальные строки\n  for i in 1...(n + 1)\n    # Переход состояний: первый столбец\n    leftup = dp.first # Временно сохранить dp[i-1, j-1]\n    dp[0] += 1\n    # Переход состояний: остальные столбцы\n    for j in 1...(m + 1)\n      temp = dp[j]\n      if s[i - 1] == t[j - 1]\n        # Если два символа равны, сразу пропустить их\n        dp[j] = leftup\n      else\n        # Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n        dp[j] = [dp[j - 1], dp[j], leftup].min + 1\n      end\n      leftup = temp # Обновить до значения dp[i-1, j-1] для следующей итерации\n    end\n  end\n  dp[m]\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 14. Динамическое программирование","14.6   Задача о расстоянии редактирования"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/","level":1,"title":"14.1   Первое знакомство с динамическим программированием","text":"

        Динамическое программирование (dynamic programming) - это важная алгоритмическая парадигма, которая разбивает задачу на последовательность более мелких подзадач и за счет хранения их решений избегает повторных вычислений, тем самым резко повышая эффективность по времени.

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

        Подъем по лестнице

        Дана лестница из \\(n\\) ступеней. За один шаг можно подняться на \\(1\\) или на \\(2\\) ступени. Сколькими способами можно добраться до вершины?

        Как показано на рисунке 14-1, для лестницы из \\(3\\) ступеней существует \\(3\\) способа добраться до вершины.

        Рисунок 14-1   Число способов подняться на 3-ю ступень

        Цель этой задачи - вычислить количество способов. Поэтому можно попробовать использовать для ее решения метод поиска с возвратом. Если представить подъем по лестнице как последовательность решений, то мы начинаем от земли и на каждом раунде выбираем прыжок на \\(1\\) или на \\(2\\) ступени; всякий раз, когда достигаем вершины, увеличиваем число способов на \\(1\\) , а если перескакиваем вершину, обрезаем эту ветвь. Код выглядит так:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_backtrack.py
        def backtrack(choices: list[int], state: int, n: int, res: list[int]) -> int:\n    \"\"\"Бэктрекинг\"\"\"\n    # Когда подъем достигает n-й ступени, число вариантов увеличивается на 1\n    if state == n:\n        res[0] += 1\n    # Перебор всех вариантов выбора\n    for choice in choices:\n        # Отсечение: нельзя выходить за n-ю ступень\n        if state + choice > n:\n            continue\n        # Попытка: сделать выбор и обновить состояние\n        backtrack(choices, state + choice, n, res)\n        # Откат\n\ndef climbing_stairs_backtrack(n: int) -> int:\n    \"\"\"Подъем по лестнице: бэктрекинг\"\"\"\n    choices = [1, 2]  # Можно подняться на 1 или 2 ступени\n    state = 0  # Начать подъем с 0-й ступени\n    res = [0]  # Использовать res[0] для хранения числа решений\n    backtrack(choices, state, n, res)\n    return res[0]\n
        climbing_stairs_backtrack.cpp
        /* Бэктрекинг */\nvoid backtrack(vector<int> &choices, int state, int n, vector<int> &res) {\n    // Когда подъем достигает n-й ступени, число вариантов увеличивается на 1\n    if (state == n)\n        res[0]++;\n    // Перебор всех вариантов выбора\n    for (auto &choice : choices) {\n        // Отсечение: нельзя выходить за n-ю ступень\n        if (state + choice > n)\n            continue;\n        // Попытка: сделать выбор и обновить состояние\n        backtrack(choices, state + choice, n, res);\n        // Откат\n    }\n}\n\n/* Подъем по лестнице: бэктрекинг */\nint climbingStairsBacktrack(int n) {\n    vector<int> choices = {1, 2}; // Можно подняться на 1 или 2 ступени\n    int state = 0;                // Начать подъем с 0-й ступени\n    vector<int> res = {0};        // Использовать res[0] для хранения числа решений\n    backtrack(choices, state, n, res);\n    return res[0];\n}\n
        climbing_stairs_backtrack.java
        /* Бэктрекинг */\nvoid backtrack(List<Integer> choices, int state, int n, List<Integer> res) {\n    // Когда подъем достигает n-й ступени, число вариантов увеличивается на 1\n    if (state == n)\n        res.set(0, res.get(0) + 1);\n    // Перебор всех вариантов выбора\n    for (Integer choice : choices) {\n        // Отсечение: нельзя выходить за n-ю ступень\n        if (state + choice > n)\n            continue;\n        // Попытка: сделать выбор и обновить состояние\n        backtrack(choices, state + choice, n, res);\n        // Откат\n    }\n}\n\n/* Подъем по лестнице: бэктрекинг */\nint climbingStairsBacktrack(int n) {\n    List<Integer> choices = Arrays.asList(1, 2); // Можно подняться на 1 или 2 ступени\n    int state = 0; // Начать подъем с 0-й ступени\n    List<Integer> res = new ArrayList<>();\n    res.add(0); // Использовать res[0] для хранения числа решений\n    backtrack(choices, state, n, res);\n    return res.get(0);\n}\n
        climbing_stairs_backtrack.cs
        /* Бэктрекинг */\nvoid Backtrack(List<int> choices, int state, int n, List<int> res) {\n    // Когда подъем достигает n-й ступени, число вариантов увеличивается на 1\n    if (state == n)\n        res[0]++;\n    // Перебор всех вариантов выбора\n    foreach (int choice in choices) {\n        // Отсечение: нельзя выходить за n-ю ступень\n        if (state + choice > n)\n            continue;\n        // Попытка: сделать выбор и обновить состояние\n        Backtrack(choices, state + choice, n, res);\n        // Откат\n    }\n}\n\n/* Подъем по лестнице: бэктрекинг */\nint ClimbingStairsBacktrack(int n) {\n    List<int> choices = [1, 2]; // Можно подняться на 1 или 2 ступени\n    int state = 0; // Начать подъем с 0-й ступени\n    List<int> res = [0]; // Использовать res[0] для хранения числа решений\n    Backtrack(choices, state, n, res);\n    return res[0];\n}\n
        climbing_stairs_backtrack.go
        /* Бэктрекинг */\nfunc backtrack(choices []int, state, n int, res []int) {\n    // Когда подъем достигает n-й ступени, число вариантов увеличивается на 1\n    if state == n {\n        res[0] = res[0] + 1\n    }\n    // Перебор всех вариантов выбора\n    for _, choice := range choices {\n        // Отсечение: нельзя выходить за n-ю ступень\n        if state+choice > n {\n            continue\n        }\n        // Попытка: сделать выбор и обновить состояние\n        backtrack(choices, state+choice, n, res)\n        // Откат\n    }\n}\n\n/* Подъем по лестнице: бэктрекинг */\nfunc climbingStairsBacktrack(n int) int {\n    // Можно подняться на 1 или 2 ступени\n    choices := []int{1, 2}\n    // Начать подъем с 0-й ступени\n    state := 0\n    res := make([]int, 1)\n    // Использовать res[0] для хранения числа решений\n    res[0] = 0\n    backtrack(choices, state, n, res)\n    return res[0]\n}\n
        climbing_stairs_backtrack.swift
        /* Бэктрекинг */\nfunc backtrack(choices: [Int], state: Int, n: Int, res: inout [Int]) {\n    // Когда подъем достигает n-й ступени, число вариантов увеличивается на 1\n    if state == n {\n        res[0] += 1\n    }\n    // Перебор всех вариантов выбора\n    for choice in choices {\n        // Отсечение: нельзя выходить за n-ю ступень\n        if state + choice > n {\n            continue\n        }\n        // Попытка: сделать выбор и обновить состояние\n        backtrack(choices: choices, state: state + choice, n: n, res: &res)\n        // Откат\n    }\n}\n\n/* Подъем по лестнице: бэктрекинг */\nfunc climbingStairsBacktrack(n: Int) -> Int {\n    let choices = [1, 2] // Можно подняться на 1 или 2 ступени\n    let state = 0 // Начать подъем с 0-й ступени\n    var res: [Int] = []\n    res.append(0) // Использовать res[0] для хранения числа решений\n    backtrack(choices: choices, state: state, n: n, res: &res)\n    return res[0]\n}\n
        climbing_stairs_backtrack.js
        /* Бэктрекинг */\nfunction backtrack(choices, state, n, res) {\n    // Когда подъем достигает n-й ступени, число вариантов увеличивается на 1\n    if (state === n) res.set(0, res.get(0) + 1);\n    // Перебор всех вариантов выбора\n    for (const choice of choices) {\n        // Отсечение: нельзя выходить за n-ю ступень\n        if (state + choice > n) continue;\n        // Попытка: сделать выбор и обновить состояние\n        backtrack(choices, state + choice, n, res);\n        // Откат\n    }\n}\n\n/* Подъем по лестнице: бэктрекинг */\nfunction climbingStairsBacktrack(n) {\n    const choices = [1, 2]; // Можно подняться на 1 или 2 ступени\n    const state = 0; // Начать подъем с 0-й ступени\n    const res = new Map();\n    res.set(0, 0); // Использовать res[0] для хранения числа решений\n    backtrack(choices, state, n, res);\n    return res.get(0);\n}\n
        climbing_stairs_backtrack.ts
        /* Бэктрекинг */\nfunction backtrack(\n    choices: number[],\n    state: number,\n    n: number,\n    res: Map<0, any>\n): void {\n    // Когда подъем достигает n-й ступени, число вариантов увеличивается на 1\n    if (state === n) res.set(0, res.get(0) + 1);\n    // Перебор всех вариантов выбора\n    for (const choice of choices) {\n        // Отсечение: нельзя выходить за n-ю ступень\n        if (state + choice > n) continue;\n        // Попытка: сделать выбор и обновить состояние\n        backtrack(choices, state + choice, n, res);\n        // Откат\n    }\n}\n\n/* Подъем по лестнице: бэктрекинг */\nfunction climbingStairsBacktrack(n: number): number {\n    const choices = [1, 2]; // Можно подняться на 1 или 2 ступени\n    const state = 0; // Начать подъем с 0-й ступени\n    const res = new Map();\n    res.set(0, 0); // Использовать res[0] для хранения числа решений\n    backtrack(choices, state, n, res);\n    return res.get(0);\n}\n
        climbing_stairs_backtrack.dart
        /* Бэктрекинг */\nvoid backtrack(List<int> choices, int state, int n, List<int> res) {\n  // Когда подъем достигает n-й ступени, число вариантов увеличивается на 1\n  if (state == n) {\n    res[0]++;\n  }\n  // Перебор всех вариантов выбора\n  for (int choice in choices) {\n    // Отсечение: нельзя выходить за n-ю ступень\n    if (state + choice > n) continue;\n    // Попытка: сделать выбор и обновить состояние\n    backtrack(choices, state + choice, n, res);\n    // Откат\n  }\n}\n\n/* Подъем по лестнице: бэктрекинг */\nint climbingStairsBacktrack(int n) {\n  List<int> choices = [1, 2]; // Можно подняться на 1 или 2 ступени\n  int state = 0; // Начать подъем с 0-й ступени\n  List<int> res = [];\n  res.add(0); // Использовать res[0] для хранения числа решений\n  backtrack(choices, state, n, res);\n  return res[0];\n}\n
        climbing_stairs_backtrack.rs
        /* Бэктрекинг */\nfn backtrack(choices: &[i32], state: i32, n: i32, res: &mut [i32]) {\n    // Когда подъем достигает n-й ступени, число вариантов увеличивается на 1\n    if state == n {\n        res[0] = res[0] + 1;\n    }\n    // Перебор всех вариантов выбора\n    for &choice in choices {\n        // Отсечение: нельзя выходить за n-ю ступень\n        if state + choice > n {\n            continue;\n        }\n        // Попытка: сделать выбор и обновить состояние\n        backtrack(choices, state + choice, n, res);\n        // Откат\n    }\n}\n\n/* Подъем по лестнице: бэктрекинг */\nfn climbing_stairs_backtrack(n: usize) -> i32 {\n    let choices = vec![1, 2]; // Можно подняться на 1 или 2 ступени\n    let state = 0; // Начать подъем с 0-й ступени\n    let mut res = Vec::new();\n    res.push(0); // Использовать res[0] для хранения числа решений\n    backtrack(&choices, state, n as i32, &mut res);\n    res[0]\n}\n
        climbing_stairs_backtrack.c
        /* Бэктрекинг */\nvoid backtrack(int *choices, int state, int n, int *res, int len) {\n    // Когда подъем достигает n-й ступени, число вариантов увеличивается на 1\n    if (state == n)\n        res[0]++;\n    // Перебор всех вариантов выбора\n    for (int i = 0; i < len; i++) {\n        int choice = choices[i];\n        // Отсечение: нельзя выходить за n-ю ступень\n        if (state + choice > n)\n            continue;\n        // Попытка: сделать выбор и обновить состояние\n        backtrack(choices, state + choice, n, res, len);\n        // Откат\n    }\n}\n\n/* Подъем по лестнице: бэктрекинг */\nint climbingStairsBacktrack(int n) {\n    int choices[2] = {1, 2}; // Можно подняться на 1 или 2 ступени\n    int state = 0;           // Начать подъем с 0-й ступени\n    int *res = (int *)malloc(sizeof(int));\n    *res = 0; // Использовать res[0] для хранения числа решений\n    int len = sizeof(choices) / sizeof(int);\n    backtrack(choices, state, n, res, len);\n    int result = *res;\n    free(res);\n    return result;\n}\n
        climbing_stairs_backtrack.kt
        /* Бэктрекинг */\nfun backtrack(\n    choices: MutableList<Int>,\n    state: Int,\n    n: Int,\n    res: MutableList<Int>\n) {\n    // Когда подъем достигает n-й ступени, число вариантов увеличивается на 1\n    if (state == n)\n        res[0] = res[0] + 1\n    // Перебор всех вариантов выбора\n    for (choice in choices) {\n        // Отсечение: нельзя выходить за n-ю ступень\n        if (state + choice > n) continue\n        // Попытка: сделать выбор и обновить состояние\n        backtrack(choices, state + choice, n, res)\n        // Откат\n    }\n}\n\n/* Подъем по лестнице: бэктрекинг */\nfun climbingStairsBacktrack(n: Int): Int {\n    val choices = mutableListOf(1, 2) // Можно подняться на 1 или 2 ступени\n    val state = 0 // Начать подъем с 0-й ступени\n    val res = mutableListOf<Int>()\n    res.add(0) // Использовать res[0] для хранения числа решений\n    backtrack(choices, state, n, res)\n    return res[0]\n}\n
        climbing_stairs_backtrack.rb
        ### Бэктрекинг ###\ndef backtrack(choices, state, n, res)\n  # Когда подъем достигает n-й ступени, число вариантов увеличивается на 1\n  res[0] += 1 if state == n\n  # Перебор всех вариантов выбора\n  for choice in choices\n    # Отсечение: нельзя выходить за n-ю ступень\n    next if state + choice > n\n\n    # Попытка: сделать выбор и обновить состояние\n    backtrack(choices, state + choice, n, res)\n  end\n  # Откат\nend\n\n### Подъем по лестнице: бэктрекинг ###\ndef climbing_stairs_backtrack(n)\n  choices = [1, 2] # Можно подняться на 1 или 2 ступени\n  state = 0 # Начать подъем с 0-й ступени\n  res = [0] # Использовать res[0] для хранения числа решений\n  backtrack(choices, state, n, res)\n  res.first\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 14. Динамическое программирование","14.1   Первое знакомство с динамическим программированием"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1411-1","level":2,"title":"14.1.1   Метод 1: полный перебор","text":"

        Алгоритм поиска с возвратом обычно не раскладывает задачу явно на подзадачи; вместо этого он рассматривает решение как последовательность решений, используя попытки и обрезку для поиска всех возможных ответов.

        Попробуем посмотреть на задачу именно как на разложение подзадач. Пусть число способов добраться до ступени \\(i\\) равно \\(dp[i]\\) ; тогда \\(dp[i]\\) - это исходная задача, а ее подзадачи включают:

        \\[ dp[i-1], dp[i-2], \\dots, dp[2], dp[1] \\]

        Поскольку за один раунд можно подняться только на \\(1\\) или на \\(2\\) ступени, стоя на ступени \\(i\\) , в предыдущий раунд мы могли находиться только на ступени \\(i - 1\\) или на ступени \\(i - 2\\) . Иначе говоря, на ступень \\(i\\) можно попасть только со ступени \\(i -1\\) или со ступени \\(i - 2\\) .

        Отсюда получается важный вывод: число способов добраться до ступени \\(i - 1\\) плюс число способов добраться до ступени \\(i - 2\\) равно числу способов добраться до ступени \\(i\\). Формула имеет вид:

        \\[ dp[i] = dp[i-1] + dp[i-2] \\]

        Это означает, что в задаче о подъеме по лестнице между подзадачами существует рекуррентная зависимость, и решение исходной задачи может быть построено на основе решений подзадач. Эта связь показана на рисунке 14-2.

        Рисунок 14-2   Рекуррентная связь числа способов

        По рекуррентной формуле можно получить решение полного перебора. Начиная с \\(dp[n]\\) , мы рекурсивно разлагаем большую задачу в сумму двух меньших задач , пока не дойдем до наименьших подзадач \\(dp[1]\\) и \\(dp[2]\\) . Их решения уже известны: \\(dp[1] = 1\\) и \\(dp[2] = 2\\) , что означает \\(1\\) и \\(2\\) способа подняться соответственно на \\(1\\)-ю и \\(2\\)-ю ступени.

        Посмотрите на следующий код: как и стандартный поиск с возвратом, он относится к поиску в глубину, но выглядит более компактно:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dfs.py
        def dfs(i: int) -> int:\n    \"\"\"Поиск\"\"\"\n    # dp[1] и dp[2] уже известны, вернуть их\n    if i == 1 or i == 2:\n        return i\n    # dp[i] = dp[i-1] + dp[i-2]\n    count = dfs(i - 1) + dfs(i - 2)\n    return count\n\ndef climbing_stairs_dfs(n: int) -> int:\n    \"\"\"Подъем по лестнице: поиск\"\"\"\n    return dfs(n)\n
        climbing_stairs_dfs.cpp
        /* Поиск */\nint dfs(int i) {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if (i == 1 || i == 2)\n        return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1) + dfs(i - 2);\n    return count;\n}\n\n/* Подъем по лестнице: поиск */\nint climbingStairsDFS(int n) {\n    return dfs(n);\n}\n
        climbing_stairs_dfs.java
        /* Поиск */\nint dfs(int i) {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if (i == 1 || i == 2)\n        return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1) + dfs(i - 2);\n    return count;\n}\n\n/* Подъем по лестнице: поиск */\nint climbingStairsDFS(int n) {\n    return dfs(n);\n}\n
        climbing_stairs_dfs.cs
        /* Поиск */\nint DFS(int i) {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if (i == 1 || i == 2)\n        return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = DFS(i - 1) + DFS(i - 2);\n    return count;\n}\n\n/* Подъем по лестнице: поиск */\nint ClimbingStairsDFS(int n) {\n    return DFS(n);\n}\n
        climbing_stairs_dfs.go
        /* Поиск */\nfunc dfs(i int) int {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if i == 1 || i == 2 {\n        return i\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    count := dfs(i-1) + dfs(i-2)\n    return count\n}\n\n/* Подъем по лестнице: поиск */\nfunc climbingStairsDFS(n int) int {\n    return dfs(n)\n}\n
        climbing_stairs_dfs.swift
        /* Поиск */\nfunc dfs(i: Int) -> Int {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if i == 1 || i == 2 {\n        return i\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    let count = dfs(i: i - 1) + dfs(i: i - 2)\n    return count\n}\n\n/* Подъем по лестнице: поиск */\nfunc climbingStairsDFS(n: Int) -> Int {\n    dfs(i: n)\n}\n
        climbing_stairs_dfs.js
        /* Поиск */\nfunction dfs(i) {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if (i === 1 || i === 2) return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    const count = dfs(i - 1) + dfs(i - 2);\n    return count;\n}\n\n/* Подъем по лестнице: поиск */\nfunction climbingStairsDFS(n) {\n    return dfs(n);\n}\n
        climbing_stairs_dfs.ts
        /* Поиск */\nfunction dfs(i: number): number {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if (i === 1 || i === 2) return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    const count = dfs(i - 1) + dfs(i - 2);\n    return count;\n}\n\n/* Подъем по лестнице: поиск */\nfunction climbingStairsDFS(n: number): number {\n    return dfs(n);\n}\n
        climbing_stairs_dfs.dart
        /* Поиск */\nint dfs(int i) {\n  // dp[1] и dp[2] уже известны, вернуть их\n  if (i == 1 || i == 2) return i;\n  // dp[i] = dp[i-1] + dp[i-2]\n  int count = dfs(i - 1) + dfs(i - 2);\n  return count;\n}\n\n/* Подъем по лестнице: поиск */\nint climbingStairsDFS(int n) {\n  return dfs(n);\n}\n
        climbing_stairs_dfs.rs
        /* Поиск */\nfn dfs(i: usize) -> i32 {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if i == 1 || i == 2 {\n        return i as i32;\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    let count = dfs(i - 1) + dfs(i - 2);\n    count\n}\n\n/* Подъем по лестнице: поиск */\nfn climbing_stairs_dfs(n: usize) -> i32 {\n    dfs(n)\n}\n
        climbing_stairs_dfs.c
        /* Поиск */\nint dfs(int i) {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if (i == 1 || i == 2)\n        return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1) + dfs(i - 2);\n    return count;\n}\n\n/* Подъем по лестнице: поиск */\nint climbingStairsDFS(int n) {\n    return dfs(n);\n}\n
        climbing_stairs_dfs.kt
        /* Поиск */\nfun dfs(i: Int): Int {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if (i == 1 || i == 2) return i\n    // dp[i] = dp[i-1] + dp[i-2]\n    val count = dfs(i - 1) + dfs(i - 2)\n    return count\n}\n\n/* Подъем по лестнице: поиск */\nfun climbingStairsDFS(n: Int): Int {\n    return dfs(n)\n}\n
        climbing_stairs_dfs.rb
        ### Поиск ###\ndef dfs(i)\n  # dp[1] и dp[2] уже известны, вернуть их\n  return i if i == 1 || i == 2\n  # dp[i] = dp[i-1] + dp[i-2]\n  dfs(i - 1) + dfs(i - 2)\nend\n\n### Подъем по лестнице: поиск ###\ndef climbing_stairs_dfs(n)\n  dfs(n)\nend\n
        Визуализация кода

        Во весь экран >

        На рисунке 14-3 показано дерево рекурсии, возникающее при полном переборе. Для задачи \\(dp[n]\\) глубина дерева рекурсии равна \\(n\\) , а временная сложность равна \\(O(2^n)\\) . Экспоненциальный рост взрывообразен: если подать на вход достаточно большое значение \\(n\\) , ожидание станет очень долгим.

        Рисунок 14-3   Дерево рекурсии для подъема по лестнице

        Если посмотреть на рисунок 14-3, то видно, что экспоненциальная временная сложность порождается \"перекрывающимися подзадачами\". Например, \\(dp[9]\\) раскладывается в \\(dp[8]\\) и \\(dp[7]\\) , а \\(dp[8]\\) - в \\(dp[7]\\) и \\(dp[6]\\) ; обе ветви содержат подзадачу \\(dp[7]\\) .

        Продолжая это рассуждение, мы видим, что подзадачи порождают все более мелкие перекрывающиеся подзадачи без конца. Подавляющая часть вычислительных ресурсов уходит именно на них.

        ","path":["Глава 14. Динамическое программирование","14.1   Первое знакомство с динамическим программированием"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1412-2","level":2,"title":"14.1.2   Метод 2: поиск с мемоизацией","text":"

        Чтобы ускорить алгоритм, мы хотим, чтобы каждая перекрывающаяся подзадача вычислялась только один раз. Для этого объявим массив mem для хранения решения каждой подзадачи и будем обрезать повторные вычисления в процессе поиска.

        1. Когда \\(dp[i]\\) вычисляется впервые, мы сохраняем его в mem[i] для последующего использования.
        2. Когда значение \\(dp[i]\\) требуется снова, мы просто берем его напрямую из mem[i] и тем самым избегаем повторного вычисления подзадачи.

        Код приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dfs_mem.py
        def dfs(i: int, mem: list[int]) -> int:\n    \"\"\"Поиск с мемоизацией\"\"\"\n    # dp[1] и dp[2] уже известны, вернуть их\n    if i == 1 or i == 2:\n        return i\n    # Если запись dp[i] существует, сразу вернуть ее\n    if mem[i] != -1:\n        return mem[i]\n    # dp[i] = dp[i-1] + dp[i-2]\n    count = dfs(i - 1, mem) + dfs(i - 2, mem)\n    # Сохранить dp[i]\n    mem[i] = count\n    return count\n\ndef climbing_stairs_dfs_mem(n: int) -> int:\n    \"\"\"Подъем по лестнице: поиск с мемоизацией\"\"\"\n    # mem[i] хранит число способов подняться на i-ю ступень, -1 означает отсутствие записи\n    mem = [-1] * (n + 1)\n    return dfs(n, mem)\n
        climbing_stairs_dfs_mem.cpp
        /* Поиск с мемоизацией */\nint dfs(int i, vector<int> &mem) {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if (i == 1 || i == 2)\n        return i;\n    // Если запись dp[i] существует, сразу вернуть ее\n    if (mem[i] != -1)\n        return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // Сохранить dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* Подъем по лестнице: поиск с мемоизацией */\nint climbingStairsDFSMem(int n) {\n    // mem[i] хранит число способов подняться на i-ю ступень, -1 означает отсутствие записи\n    vector<int> mem(n + 1, -1);\n    return dfs(n, mem);\n}\n
        climbing_stairs_dfs_mem.java
        /* Поиск с мемоизацией */\nint dfs(int i, int[] mem) {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if (i == 1 || i == 2)\n        return i;\n    // Если запись dp[i] существует, сразу вернуть ее\n    if (mem[i] != -1)\n        return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // Сохранить dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* Подъем по лестнице: поиск с мемоизацией */\nint climbingStairsDFSMem(int n) {\n    // mem[i] хранит число способов подняться на i-ю ступень, -1 означает отсутствие записи\n    int[] mem = new int[n + 1];\n    Arrays.fill(mem, -1);\n    return dfs(n, mem);\n}\n
        climbing_stairs_dfs_mem.cs
        /* Поиск с мемоизацией */\nint DFS(int i, int[] mem) {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if (i == 1 || i == 2)\n        return i;\n    // Если запись dp[i] существует, сразу вернуть ее\n    if (mem[i] != -1)\n        return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = DFS(i - 1, mem) + DFS(i - 2, mem);\n    // Сохранить dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* Подъем по лестнице: поиск с мемоизацией */\nint ClimbingStairsDFSMem(int n) {\n    // mem[i] хранит число способов подняться на i-ю ступень, -1 означает отсутствие записи\n    int[] mem = new int[n + 1];\n    Array.Fill(mem, -1);\n    return DFS(n, mem);\n}\n
        climbing_stairs_dfs_mem.go
        /* Поиск с мемоизацией */\nfunc dfsMem(i int, mem []int) int {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if i == 1 || i == 2 {\n        return i\n    }\n    // Если запись dp[i] существует, сразу вернуть ее\n    if mem[i] != -1 {\n        return mem[i]\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    count := dfsMem(i-1, mem) + dfsMem(i-2, mem)\n    // Сохранить dp[i]\n    mem[i] = count\n    return count\n}\n\n/* Подъем по лестнице: поиск с мемоизацией */\nfunc climbingStairsDFSMem(n int) int {\n    // mem[i] хранит число способов подняться на i-ю ступень, -1 означает отсутствие записи\n    mem := make([]int, n+1)\n    for i := range mem {\n        mem[i] = -1\n    }\n    return dfsMem(n, mem)\n}\n
        climbing_stairs_dfs_mem.swift
        /* Поиск с мемоизацией */\nfunc dfs(i: Int, mem: inout [Int]) -> Int {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if i == 1 || i == 2 {\n        return i\n    }\n    // Если запись dp[i] существует, сразу вернуть ее\n    if mem[i] != -1 {\n        return mem[i]\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    let count = dfs(i: i - 1, mem: &mem) + dfs(i: i - 2, mem: &mem)\n    // Сохранить dp[i]\n    mem[i] = count\n    return count\n}\n\n/* Подъем по лестнице: поиск с мемоизацией */\nfunc climbingStairsDFSMem(n: Int) -> Int {\n    // mem[i] хранит число способов подняться на i-ю ступень, -1 означает отсутствие записи\n    var mem = Array(repeating: -1, count: n + 1)\n    return dfs(i: n, mem: &mem)\n}\n
        climbing_stairs_dfs_mem.js
        /* Поиск с мемоизацией */\nfunction dfs(i, mem) {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if (i === 1 || i === 2) return i;\n    // Если запись dp[i] существует, сразу вернуть ее\n    if (mem[i] != -1) return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    const count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // Сохранить dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* Подъем по лестнице: поиск с мемоизацией */\nfunction climbingStairsDFSMem(n) {\n    // mem[i] хранит число способов подняться на i-ю ступень, -1 означает отсутствие записи\n    const mem = new Array(n + 1).fill(-1);\n    return dfs(n, mem);\n}\n
        climbing_stairs_dfs_mem.ts
        /* Поиск с мемоизацией */\nfunction dfs(i: number, mem: number[]): number {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if (i === 1 || i === 2) return i;\n    // Если запись dp[i] существует, сразу вернуть ее\n    if (mem[i] != -1) return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    const count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // Сохранить dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* Подъем по лестнице: поиск с мемоизацией */\nfunction climbingStairsDFSMem(n: number): number {\n    // mem[i] хранит число способов подняться на i-ю ступень, -1 означает отсутствие записи\n    const mem = new Array(n + 1).fill(-1);\n    return dfs(n, mem);\n}\n
        climbing_stairs_dfs_mem.dart
        /* Поиск с мемоизацией */\nint dfs(int i, List<int> mem) {\n  // dp[1] и dp[2] уже известны, вернуть их\n  if (i == 1 || i == 2) return i;\n  // Если запись dp[i] существует, сразу вернуть ее\n  if (mem[i] != -1) return mem[i];\n  // dp[i] = dp[i-1] + dp[i-2]\n  int count = dfs(i - 1, mem) + dfs(i - 2, mem);\n  // Сохранить dp[i]\n  mem[i] = count;\n  return count;\n}\n\n/* Подъем по лестнице: поиск с мемоизацией */\nint climbingStairsDFSMem(int n) {\n  // mem[i] хранит число способов подняться на i-ю ступень, -1 означает отсутствие записи\n  List<int> mem = List.filled(n + 1, -1);\n  return dfs(n, mem);\n}\n
        climbing_stairs_dfs_mem.rs
        /* Поиск с мемоизацией */\nfn dfs(i: usize, mem: &mut [i32]) -> i32 {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if i == 1 || i == 2 {\n        return i as i32;\n    }\n    // Если запись dp[i] существует, сразу вернуть ее\n    if mem[i] != -1 {\n        return mem[i];\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    let count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // Сохранить dp[i]\n    mem[i] = count;\n    count\n}\n\n/* Подъем по лестнице: поиск с мемоизацией */\nfn climbing_stairs_dfs_mem(n: usize) -> i32 {\n    // mem[i] хранит число способов подняться на i-ю ступень, -1 означает отсутствие записи\n    let mut mem = vec![-1; n + 1];\n    dfs(n, &mut mem)\n}\n
        climbing_stairs_dfs_mem.c
        /* Поиск с мемоизацией */\nint dfs(int i, int *mem) {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if (i == 1 || i == 2)\n        return i;\n    // Если запись dp[i] существует, сразу вернуть ее\n    if (mem[i] != -1)\n        return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // Сохранить dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* Подъем по лестнице: поиск с мемоизацией */\nint climbingStairsDFSMem(int n) {\n    // mem[i] хранит число способов подняться на i-ю ступень, -1 означает отсутствие записи\n    int *mem = (int *)malloc((n + 1) * sizeof(int));\n    for (int i = 0; i <= n; i++) {\n        mem[i] = -1;\n    }\n    int result = dfs(n, mem);\n    free(mem);\n    return result;\n}\n
        climbing_stairs_dfs_mem.kt
        /* Поиск с мемоизацией */\nfun dfs(i: Int, mem: IntArray): Int {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if (i == 1 || i == 2) return i\n    // Если запись dp[i] существует, сразу вернуть ее\n    if (mem[i] != -1) return mem[i]\n    // dp[i] = dp[i-1] + dp[i-2]\n    val count = dfs(i - 1, mem) + dfs(i - 2, mem)\n    // Сохранить dp[i]\n    mem[i] = count\n    return count\n}\n\n/* Подъем по лестнице: поиск с мемоизацией */\nfun climbingStairsDFSMem(n: Int): Int {\n    // mem[i] хранит число способов подняться на i-ю ступень, -1 означает отсутствие записи\n    val mem = IntArray(n + 1)\n    mem.fill(-1)\n    return dfs(n, mem)\n}\n
        climbing_stairs_dfs_mem.rb
        ### Поиск с мемоизацией ###\ndef dfs(i, mem)\n  # dp[1] и dp[2] уже известны, вернуть их\n  return i if i == 1 || i == 2\n  # Если запись dp[i] существует, сразу вернуть ее\n  return mem[i] if mem[i] != -1\n\n  # dp[i] = dp[i-1] + dp[i-2]\n  count = dfs(i - 1, mem) + dfs(i - 2, mem)\n  # Сохранить dp[i]\n  mem[i] = count\nend\n\n### Подъем по лестнице: поиск с мемоизацией ###\ndef climbing_stairs_dfs_mem(n)\n  # mem[i] хранит число способов подняться на i-ю ступень, -1 означает отсутствие записи\n  mem = Array.new(n + 1, -1)\n  dfs(n, mem)\nend\n
        Визуализация кода

        Во весь экран >

        Как показано на рисунке 14-4, после введения мемоизации каждая перекрывающаяся подзадача вычисляется только один раз, и временная сложность оптимизируется до \\(O(n)\\) . Это огромный скачок в эффективности.

        Рисунок 14-4   Дерево рекурсии для поиска с мемоизацией

        ","path":["Глава 14. Динамическое программирование","14.1   Первое знакомство с динамическим программированием"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1413-3","level":2,"title":"14.1.3   Метод 3: динамическое программирование","text":"

        Поиск с мемоизацией - это метод \"сверху вниз\" : мы начинаем с исходной задачи (корня), рекурсивно раскладываем более крупные подзадачи на меньшие, пока не достигнем наименьших подзадач с уже известным ответом (листьев). Затем в процессе возврата постепенно собираем решения подзадач и тем самым получаем решение исходной задачи.

        Напротив, динамическое программирование - это метод \"снизу вверх\" : начиная с решений наименьших подзадач, мы итеративно строим решения для более крупных подзадач, пока не получим ответ на исходную задачу.

        Поскольку в динамическом программировании нет этапа возврата, для его реализации достаточно обычных циклов, без рекурсии. В приведенном ниже коде мы инициализируем массив dp для хранения решений подзадач; он выполняет ту же роль, что и массив mem в мемоизированном поиске:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dp.py
        def climbing_stairs_dp(n: int) -> int:\n    \"\"\"Подъем по лестнице: динамическое программирование\"\"\"\n    if n == 1 or n == 2:\n        return n\n    # Инициализация таблицы dp для хранения решений подзадач\n    dp = [0] * (n + 1)\n    # Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1], dp[2] = 1, 2\n    # Переход состояний: постепенное решение больших подзадач через меньшие\n    for i in range(3, n + 1):\n        dp[i] = dp[i - 1] + dp[i - 2]\n    return dp[n]\n
        climbing_stairs_dp.cpp
        /* Подъем по лестнице: динамическое программирование */\nint climbingStairsDP(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    // Инициализация таблицы dp для хранения решений подзадач\n    vector<int> dp(n + 1);\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1] = 1;\n    dp[2] = 2;\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (int i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    return dp[n];\n}\n
        climbing_stairs_dp.java
        /* Подъем по лестнице: динамическое программирование */\nint climbingStairsDP(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    // Инициализация таблицы dp для хранения решений подзадач\n    int[] dp = new int[n + 1];\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1] = 1;\n    dp[2] = 2;\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (int i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    return dp[n];\n}\n
        climbing_stairs_dp.cs
        /* Подъем по лестнице: динамическое программирование */\nint ClimbingStairsDP(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    // Инициализация таблицы dp для хранения решений подзадач\n    int[] dp = new int[n + 1];\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1] = 1;\n    dp[2] = 2;\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (int i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    return dp[n];\n}\n
        climbing_stairs_dp.go
        /* Подъем по лестнице: динамическое программирование */\nfunc climbingStairsDP(n int) int {\n    if n == 1 || n == 2 {\n        return n\n    }\n    // Инициализация таблицы dp для хранения решений подзадач\n    dp := make([]int, n+1)\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1] = 1\n    dp[2] = 2\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for i := 3; i <= n; i++ {\n        dp[i] = dp[i-1] + dp[i-2]\n    }\n    return dp[n]\n}\n
        climbing_stairs_dp.swift
        /* Подъем по лестнице: динамическое программирование */\nfunc climbingStairsDP(n: Int) -> Int {\n    if n == 1 || n == 2 {\n        return n\n    }\n    // Инициализация таблицы dp для хранения решений подзадач\n    var dp = Array(repeating: 0, count: n + 1)\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1] = 1\n    dp[2] = 2\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for i in 3 ... n {\n        dp[i] = dp[i - 1] + dp[i - 2]\n    }\n    return dp[n]\n}\n
        climbing_stairs_dp.js
        /* Подъем по лестнице: динамическое программирование */\nfunction climbingStairsDP(n) {\n    if (n === 1 || n === 2) return n;\n    // Инициализация таблицы dp для хранения решений подзадач\n    const dp = new Array(n + 1).fill(-1);\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1] = 1;\n    dp[2] = 2;\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (let i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    return dp[n];\n}\n
        climbing_stairs_dp.ts
        /* Подъем по лестнице: динамическое программирование */\nfunction climbingStairsDP(n: number): number {\n    if (n === 1 || n === 2) return n;\n    // Инициализация таблицы dp для хранения решений подзадач\n    const dp = new Array(n + 1).fill(-1);\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1] = 1;\n    dp[2] = 2;\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (let i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    return dp[n];\n}\n
        climbing_stairs_dp.dart
        /* Подъем по лестнице: динамическое программирование */\nint climbingStairsDP(int n) {\n  if (n == 1 || n == 2) return n;\n  // Инициализация таблицы dp для хранения решений подзадач\n  List<int> dp = List.filled(n + 1, 0);\n  // Начальное состояние: заранее задать решения наименьших подзадач\n  dp[1] = 1;\n  dp[2] = 2;\n  // Переход состояний: постепенное решение больших подзадач через меньшие\n  for (int i = 3; i <= n; i++) {\n    dp[i] = dp[i - 1] + dp[i - 2];\n  }\n  return dp[n];\n}\n
        climbing_stairs_dp.rs
        /* Подъем по лестнице: динамическое программирование */\nfn climbing_stairs_dp(n: usize) -> i32 {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if n == 1 || n == 2 {\n        return n as i32;\n    }\n    // Инициализация таблицы dp для хранения решений подзадач\n    let mut dp = vec![-1; n + 1];\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1] = 1;\n    dp[2] = 2;\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for i in 3..=n {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    dp[n]\n}\n
        climbing_stairs_dp.c
        /* Подъем по лестнице: динамическое программирование */\nint climbingStairsDP(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    // Инициализация таблицы dp для хранения решений подзадач\n    int *dp = (int *)malloc((n + 1) * sizeof(int));\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1] = 1;\n    dp[2] = 2;\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (int i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    int result = dp[n];\n    free(dp);\n    return result;\n}\n
        climbing_stairs_dp.kt
        /* Подъем по лестнице: динамическое программирование */\nfun climbingStairsDP(n: Int): Int {\n    if (n == 1 || n == 2) return n\n    // Инициализация таблицы dp для хранения решений подзадач\n    val dp = IntArray(n + 1)\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1] = 1\n    dp[2] = 2\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (i in 3..n) {\n        dp[i] = dp[i - 1] + dp[i - 2]\n    }\n    return dp[n]\n}\n
        climbing_stairs_dp.rb
        ### Подъем по лестнице: динамическое программирование ###\ndef climbing_stairs_dp(n)\n  return n  if n == 1 || n == 2\n\n  # Инициализация таблицы dp для хранения решений подзадач\n  dp = Array.new(n + 1, 0)\n  # Начальное состояние: заранее задать решения наименьших подзадач\n  dp[1], dp[2] = 1, 2\n  # Переход состояний: постепенное решение больших подзадач через меньшие\n  (3...(n + 1)).each { |i| dp[i] = dp[i - 1] + dp[i - 2] }\n\n  dp[n]\nend\n
        Визуализация кода

        Во весь экран >

        На рисунке 14-5 смоделирован процесс выполнения этого кода.

        Рисунок 14-5   Процесс динамического программирования для подъема по лестнице

        Как и в поиске с возвратом, в динамическом программировании используется понятие \"состояние\" для обозначения некоторого этапа решения задачи; каждое состояние соответствует одной подзадаче и ее локально оптимальному решению. Например, в задаче о лестнице состояние определяется текущим номером ступени \\(i\\) .

        На основе сказанного можно подвести несколько часто используемых терминов динамического программирования.

        • Массив dp называют таблицей dp, а \\(dp[i]\\) обозначает решение подзадачи, соответствующей состоянию \\(i\\) .
        • Состояния, соответствующие наименьшим подзадачам (первая и вторая ступени), называют начальными состояниями.
        • Рекуррентную формулу \\(dp[i] = dp[i-1] + dp[i-2]\\) называют уравнением перехода состояния.
        ","path":["Глава 14. Динамическое программирование","14.1   Первое знакомство с динамическим программированием"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1414","level":2,"title":"14.1.4   Оптимизация пространства","text":"

        Внимательный читатель мог заметить, что поскольку \\(dp[i]\\) зависит только от \\(dp[i-1]\\) и \\(dp[i-2]\\) , нам не нужен весь массив dp для хранения ответов всех подзадач ; достаточно двух переменных, которые будут \"перекатываться\" вперед. Код имеет вид:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dp.py
        def climbing_stairs_dp_comp(n: int) -> int:\n    \"\"\"Подъем по лестнице: динамическое программирование с оптимизацией памяти\"\"\"\n    if n == 1 or n == 2:\n        return n\n    a, b = 1, 2\n    for _ in range(3, n + 1):\n        a, b = b, a + b\n    return b\n
        climbing_stairs_dp.cpp
        /* Подъем по лестнице: динамическое программирование с оптимизацией памяти */\nint climbingStairsDPComp(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    int a = 1, b = 2;\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
        climbing_stairs_dp.java
        /* Подъем по лестнице: динамическое программирование с оптимизацией памяти */\nint climbingStairsDPComp(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    int a = 1, b = 2;\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
        climbing_stairs_dp.cs
        /* Подъем по лестнице: динамическое программирование с оптимизацией памяти */\nint ClimbingStairsDPComp(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    int a = 1, b = 2;\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
        climbing_stairs_dp.go
        /* Подъем по лестнице: динамическое программирование с оптимизацией памяти */\nfunc climbingStairsDPComp(n int) int {\n    if n == 1 || n == 2 {\n        return n\n    }\n    a, b := 1, 2\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for i := 3; i <= n; i++ {\n        a, b = b, a+b\n    }\n    return b\n}\n
        climbing_stairs_dp.swift
        /* Подъем по лестнице: динамическое программирование с оптимизацией памяти */\nfunc climbingStairsDPComp(n: Int) -> Int {\n    if n == 1 || n == 2 {\n        return n\n    }\n    var a = 1\n    var b = 2\n    for _ in 3 ... n {\n        (a, b) = (b, a + b)\n    }\n    return b\n}\n
        climbing_stairs_dp.js
        /* Подъем по лестнице: динамическое программирование с оптимизацией памяти */\nfunction climbingStairsDPComp(n) {\n    if (n === 1 || n === 2) return n;\n    let a = 1,\n        b = 2;\n    for (let i = 3; i <= n; i++) {\n        const tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
        climbing_stairs_dp.ts
        /* Подъем по лестнице: динамическое программирование с оптимизацией памяти */\nfunction climbingStairsDPComp(n: number): number {\n    if (n === 1 || n === 2) return n;\n    let a = 1,\n        b = 2;\n    for (let i = 3; i <= n; i++) {\n        const tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
        climbing_stairs_dp.dart
        /* Подъем по лестнице: динамическое программирование с оптимизацией памяти */\nint climbingStairsDPComp(int n) {\n  if (n == 1 || n == 2) return n;\n  int a = 1, b = 2;\n  for (int i = 3; i <= n; i++) {\n    int tmp = b;\n    b = a + b;\n    a = tmp;\n  }\n  return b;\n}\n
        climbing_stairs_dp.rs
        /* Подъем по лестнице: динамическое программирование с оптимизацией памяти */\nfn climbing_stairs_dp_comp(n: usize) -> i32 {\n    if n == 1 || n == 2 {\n        return n as i32;\n    }\n    let (mut a, mut b) = (1, 2);\n    for _ in 3..=n {\n        let tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    b\n}\n
        climbing_stairs_dp.c
        /* Подъем по лестнице: динамическое программирование с оптимизацией памяти */\nint climbingStairsDPComp(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    int a = 1, b = 2;\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
        climbing_stairs_dp.kt
        /* Подъем по лестнице: динамическое программирование с оптимизацией памяти */\nfun climbingStairsDPComp(n: Int): Int {\n    if (n == 1 || n == 2) return n\n    var a = 1\n    var b = 2\n    for (i in 3..n) {\n        val temp = b\n        b += a\n        a = temp\n    }\n    return b\n}\n
        climbing_stairs_dp.rb
        ### Подъем по лестнице: динамическое программирование с оптимизацией памяти ###\ndef climbing_stairs_dp_comp(n)\n  return n if n == 1 || n == 2\n\n  a, b = 1, 2\n  (3...(n + 1)).each { a, b = b, a + b }\n\n  b\nend\n
        Визуализация кода

        Во весь экран >

        Из кода видно, что после отказа от массива dp пространственная сложность уменьшается с \\(O(n)\\) до \\(O(1)\\) .

        Во многих задачах динамического программирования текущее состояние зависит лишь от ограниченного числа предыдущих состояний. Тогда можно сохранять только действительно нужные состояния и за счет \"уменьшения размерности\" экономить память. Этот прием оптимизации памяти называют \"скользящими переменными\" или \"скользящим массивом\".

        ","path":["Глава 14. Динамическое программирование","14.1   Первое знакомство с динамическим программированием"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/","level":1,"title":"14.4   Задача о рюкзаке 0-1","text":"

        Задача о рюкзаке является отличным примером для начала изучения динамического программирования и представляет собой одну из наиболее распространенных форм этой задачи. У нее существует множество вариантов, например задача о рюкзаке 0-1, задача о полном рюкзаке, задача о многократном рюкзаке и т.д.

        В этом разделе сначала разберем самый распространенный вариант - задачу о рюкзаке 0-1.

        Question

        Даны \\(n\\) предметов. Вес \\(i\\)-го предмета равен \\(wgt[i-1]\\) , стоимость равна \\(val[i-1]\\) . Также дан рюкзак вместимости \\(cap\\) . Каждый предмет можно выбрать только один раз. Найдите максимальную суммарную стоимость, которую можно поместить в рюкзак при заданной вместимости.

        Как видно на рисунке 14-17, поскольку номер предмета \\(i\\) начинается с \\(1\\) , а индексы массива начинаются с \\(0\\) , предмету \\(i\\) соответствуют вес \\(wgt[i-1]\\) и стоимость \\(val[i-1]\\) .

        Рисунок 14-17   Пример данных для задачи о рюкзаке 0-1

        Задачу о рюкзаке 0-1 можно рассматривать как процесс из \\(n\\) раундов принятия решений: для каждого предмета есть два решения - не класть его в рюкзак или положить в рюкзак. Поэтому задача удовлетворяет модели дерева решений.

        Цель задачи - найти \"максимальную суммарную стоимость при ограниченной вместимости рюкзака\", а это с большой вероятностью указывает на задачу динамического программирования.

        Шаг 1: продумать решения на каждом раунде, определить состояние и тем самым получить таблицу \\(dp\\)

        Для каждого предмета возможны два случая: не класть его в рюкзак, тогда вместимость не меняется; или положить его в рюкзак, тогда оставшаяся вместимость уменьшается. Отсюда получается определение состояния: текущий номер предмета \\(i\\) и текущая вместимость рюкзака \\(c\\) , то есть состояние обозначается как \\([i, c]\\) .

        Подзадача, соответствующая состоянию \\([i, c]\\) , такова: максимальная стоимость, которую можно получить, используя первые \\(i\\) предметов и рюкзак вместимости \\(c\\). Ее решение обозначается через \\(dp[i, c]\\) .

        Искомым значением является \\(dp[n, cap]\\) , значит, нам нужна двумерная таблица \\(dp\\) размера \\((n+1) \\times (cap+1)\\) .

        Шаг 2: найти оптимальную подструктуру и на ее основе вывести уравнение перехода состояния

        После того как мы принимаем решение по предмету \\(i\\) , остается подзадача, связанная с первыми \\(i-1\\) предметами. Здесь возможны два случая.

        • Не класть предмет \\(i\\) : вместимость рюкзака не меняется, и состояние переходит в \\([i-1, c]\\) .
        • Положить предмет \\(i\\) : вместимость рюкзака уменьшается на \\(wgt[i-1]\\) , а стоимость увеличивается на \\(val[i-1]\\) , и состояние переходит в \\([i-1, c-wgt[i-1]]\\) .

        Этот анализ и раскрывает оптимальную подструктуру задачи: максимальная стоимость \\(dp[i, c]\\) равна лучшему из двух вариантов - не брать предмет \\(i\\) или взять предмет \\(i\\). Отсюда получается уравнение перехода состояния:

        \\[ dp[i, c] = \\max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1]) \\]

        Нужно учитывать, что если вес текущего предмета \\(wgt[i - 1]\\) превышает оставшуюся вместимость \\(c\\) , то предмет можно только не брать.

        Шаг 3: определить граничные условия и порядок переходов

        Когда предметов нет или вместимость рюкзака равна \\(0\\) , максимальная стоимость равна \\(0\\) ; то есть весь первый столбец \\(dp[i, 0]\\) и вся первая строка \\(dp[0, c]\\) заполняются нулями.

        Текущее состояние \\([i, c]\\) зависит от состояния сверху \\([i-1, c]\\) и состояния слева сверху \\([i-1, c-wgt[i-1]]\\) , поэтому достаточно двумя вложенными циклами пройти по всей таблице \\(dp\\) в прямом порядке.

        После этого анализа реализуем по порядку: полный перебор, поиск с мемоизацией и динамическое программирование.

        ","path":["Глава 14. Динамическое программирование","14.4   Задача о рюкзаке 0-1"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#1-1","level":3,"title":"1.   Метод 1: полный перебор","text":"

        Код поиска содержит следующие элементы.

        • Параметры рекурсии: состояние \\([i, c]\\) .
        • Возвращаемое значение: решение подзадачи \\(dp[i, c]\\) .
        • Условие завершения: когда номер предмета выходит за границу, то есть \\(i = 0\\) , или оставшаяся вместимость равна \\(0\\) , рекурсия завершается и возвращается стоимость \\(0\\) .
        • Обрезка: если вес текущего предмета превышает оставшуюся вместимость рюкзака, то можно только не класть этот предмет.
        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py
        def knapsack_dfs(wgt: list[int], val: list[int], i: int, c: int) -> int:\n    \"\"\"Рюкзак 0-1: полный перебор\"\"\"\n    # Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if i == 0 or c == 0:\n        return 0\n    # Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if wgt[i - 1] > c:\n        return knapsack_dfs(wgt, val, i - 1, c)\n    # Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    no = knapsack_dfs(wgt, val, i - 1, c)\n    yes = knapsack_dfs(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]\n    # Вернуть вариант с большей стоимостью из двух возможных\n    return max(no, yes)\n
        knapsack.cpp
        /* Рюкзак 0-1: полный перебор */\nint knapsackDFS(vector<int> &wgt, vector<int> &val, int i, int c) {\n    // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    int no = knapsackDFS(wgt, val, i - 1, c);\n    int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // Вернуть вариант с большей стоимостью из двух возможных\n    return max(no, yes);\n}\n
        knapsack.java
        /* Рюкзак 0-1: полный перебор */\nint knapsackDFS(int[] wgt, int[] val, int i, int c) {\n    // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    int no = knapsackDFS(wgt, val, i - 1, c);\n    int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // Вернуть вариант с большей стоимостью из двух возможных\n    return Math.max(no, yes);\n}\n
        knapsack.cs
        /* Рюкзак 0-1: полный перебор */\nint KnapsackDFS(int[] weight, int[] val, int i, int c) {\n    // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if (weight[i - 1] > c) {\n        return KnapsackDFS(weight, val, i - 1, c);\n    }\n    // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    int no = KnapsackDFS(weight, val, i - 1, c);\n    int yes = KnapsackDFS(weight, val, i - 1, c - weight[i - 1]) + val[i - 1];\n    // Вернуть вариант с большей стоимостью из двух возможных\n    return Math.Max(no, yes);\n}\n
        knapsack.go
        /* Рюкзак 0-1: полный перебор */\nfunc knapsackDFS(wgt, val []int, i, c int) int {\n    // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if i == 0 || c == 0 {\n        return 0\n    }\n    // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if wgt[i-1] > c {\n        return knapsackDFS(wgt, val, i-1, c)\n    }\n    // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    no := knapsackDFS(wgt, val, i-1, c)\n    yes := knapsackDFS(wgt, val, i-1, c-wgt[i-1]) + val[i-1]\n    // Вернуть вариант с большей стоимостью из двух возможных\n    return int(math.Max(float64(no), float64(yes)))\n}\n
        knapsack.swift
        /* Рюкзак 0-1: полный перебор */\nfunc knapsackDFS(wgt: [Int], val: [Int], i: Int, c: Int) -> Int {\n    // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if i == 0 || c == 0 {\n        return 0\n    }\n    // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if wgt[i - 1] > c {\n        return knapsackDFS(wgt: wgt, val: val, i: i - 1, c: c)\n    }\n    // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    let no = knapsackDFS(wgt: wgt, val: val, i: i - 1, c: c)\n    let yes = knapsackDFS(wgt: wgt, val: val, i: i - 1, c: c - wgt[i - 1]) + val[i - 1]\n    // Вернуть вариант с большей стоимостью из двух возможных\n    return max(no, yes)\n}\n
        knapsack.js
        /* Рюкзак 0-1: полный перебор */\nfunction knapsackDFS(wgt, val, i, c) {\n    // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if (i === 0 || c === 0) {\n        return 0;\n    }\n    // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    const no = knapsackDFS(wgt, val, i - 1, c);\n    const yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // Вернуть вариант с большей стоимостью из двух возможных\n    return Math.max(no, yes);\n}\n
        knapsack.ts
        /* Рюкзак 0-1: полный перебор */\nfunction knapsackDFS(\n    wgt: Array<number>,\n    val: Array<number>,\n    i: number,\n    c: number\n): number {\n    // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if (i === 0 || c === 0) {\n        return 0;\n    }\n    // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    const no = knapsackDFS(wgt, val, i - 1, c);\n    const yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // Вернуть вариант с большей стоимостью из двух возможных\n    return Math.max(no, yes);\n}\n
        knapsack.dart
        /* Рюкзак 0-1: полный перебор */\nint knapsackDFS(List<int> wgt, List<int> val, int i, int c) {\n  // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n  if (i == 0 || c == 0) {\n    return 0;\n  }\n  // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n  if (wgt[i - 1] > c) {\n    return knapsackDFS(wgt, val, i - 1, c);\n  }\n  // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n  int no = knapsackDFS(wgt, val, i - 1, c);\n  int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n  // Вернуть вариант с большей стоимостью из двух возможных\n  return max(no, yes);\n}\n
        knapsack.rs
        /* Рюкзак 0-1: полный перебор */\nfn knapsack_dfs(wgt: &[i32], val: &[i32], i: usize, c: usize) -> i32 {\n    // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if i == 0 || c == 0 {\n        return 0;\n    }\n    // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if wgt[i - 1] > c as i32 {\n        return knapsack_dfs(wgt, val, i - 1, c);\n    }\n    // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    let no = knapsack_dfs(wgt, val, i - 1, c);\n    let yes = knapsack_dfs(wgt, val, i - 1, c - wgt[i - 1] as usize) + val[i - 1];\n    // Вернуть вариант с большей стоимостью из двух возможных\n    std::cmp::max(no, yes)\n}\n
        knapsack.c
        /* Рюкзак 0-1: полный перебор */\nint knapsackDFS(int wgt[], int val[], int i, int c) {\n    // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    int no = knapsackDFS(wgt, val, i - 1, c);\n    int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // Вернуть вариант с большей стоимостью из двух возможных\n    return myMax(no, yes);\n}\n
        knapsack.kt
        /* Рюкзак 0-1: полный перебор */\nfun knapsackDFS(\n    wgt: IntArray,\n    _val: IntArray,\n    i: Int,\n    c: Int\n): Int {\n    // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if (i == 0 || c == 0) {\n        return 0\n    }\n    // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, _val, i - 1, c)\n    }\n    // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    val no = knapsackDFS(wgt, _val, i - 1, c)\n    val yes = knapsackDFS(wgt, _val, i - 1, c - wgt[i - 1]) + _val[i - 1]\n    // Вернуть вариант с большей стоимостью из двух возможных\n    return max(no, yes)\n}\n
        knapsack.rb
        ### Рюкзак 0-1: полный перебор ###\ndef knapsack_dfs(wgt, val, i, c)\n  # Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n  return 0 if i == 0 || c == 0\n  # Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n  return knapsack_dfs(wgt, val, i - 1, c) if wgt[i - 1] > c\n  # Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n  no = knapsack_dfs(wgt, val, i - 1, c)\n  yes = knapsack_dfs(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]\n  # Вернуть вариант с большей стоимостью из двух возможных\n  [no, yes].max\nend\n
        Визуализация кода

        Во весь экран >

        Как показано на рисунке 14-18, поскольку каждый предмет создает две ветви поиска - \"не брать\" и \"брать\", временная сложность равна \\(O(2^n)\\) .

        Посмотрев на дерево рекурсии, легко заметить наличие перекрывающихся подзадач, например \\(dp[1, 10]\\) и подобных. Когда число предметов растет, вместимость рюкзака велика, а особенно когда много предметов с одинаковым весом, количество перекрывающихся подзадач быстро увеличивается.

        Рисунок 14-18   Дерево полного перебора для задачи о рюкзаке 0-1

        ","path":["Глава 14. Динамическое программирование","14.4   Задача о рюкзаке 0-1"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#2-2","level":3,"title":"2.   Метод 2: мемоизация","text":"

        Чтобы каждая перекрывающаяся подзадача вычислялась только один раз, используем таблицу памяти mem для хранения решений подзадач, где mem[i][c] соответствует \\(dp[i, c]\\) .

        После введения мемоизации временная сложность определяется числом подзадач , то есть равна \\(O(n \\times cap)\\) . Код выглядит так:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py
        def knapsack_dfs_mem(\n    wgt: list[int], val: list[int], mem: list[list[int]], i: int, c: int\n) -> int:\n    \"\"\"Рюкзак 0-1: поиск с мемоизацией\"\"\"\n    # Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if i == 0 or c == 0:\n        return 0\n    # Если запись уже есть, вернуть сразу\n    if mem[i][c] != -1:\n        return mem[i][c]\n    # Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if wgt[i - 1] > c:\n        return knapsack_dfs_mem(wgt, val, mem, i - 1, c)\n    # Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    no = knapsack_dfs_mem(wgt, val, mem, i - 1, c)\n    yes = knapsack_dfs_mem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1]\n    # Сохранить и вернуть вариант с большей стоимостью из двух решений\n    mem[i][c] = max(no, yes)\n    return mem[i][c]\n
        knapsack.cpp
        /* Рюкзак 0-1: поиск с мемоизацией */\nint knapsackDFSMem(vector<int> &wgt, vector<int> &val, vector<vector<int>> &mem, int i, int c) {\n    // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // Если запись уже есть, вернуть сразу\n    if (mem[i][c] != -1) {\n        return mem[i][c];\n    }\n    // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, mem, i - 1, c);\n    }\n    // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    int no = knapsackDFSMem(wgt, val, mem, i - 1, c);\n    int yes = knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // Сохранить и вернуть вариант с большей стоимостью из двух решений\n    mem[i][c] = max(no, yes);\n    return mem[i][c];\n}\n
        knapsack.java
        /* Рюкзак 0-1: поиск с мемоизацией */\nint knapsackDFSMem(int[] wgt, int[] val, int[][] mem, int i, int c) {\n    // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // Если запись уже есть, вернуть сразу\n    if (mem[i][c] != -1) {\n        return mem[i][c];\n    }\n    // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, mem, i - 1, c);\n    }\n    // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    int no = knapsackDFSMem(wgt, val, mem, i - 1, c);\n    int yes = knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // Сохранить и вернуть вариант с большей стоимостью из двух решений\n    mem[i][c] = Math.max(no, yes);\n    return mem[i][c];\n}\n
        knapsack.cs
        /* Рюкзак 0-1: поиск с мемоизацией */\nint KnapsackDFSMem(int[] weight, int[] val, int[][] mem, int i, int c) {\n    // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // Если запись уже есть, вернуть сразу\n    if (mem[i][c] != -1) {\n        return mem[i][c];\n    }\n    // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if (weight[i - 1] > c) {\n        return KnapsackDFSMem(weight, val, mem, i - 1, c);\n    }\n    // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    int no = KnapsackDFSMem(weight, val, mem, i - 1, c);\n    int yes = KnapsackDFSMem(weight, val, mem, i - 1, c - weight[i - 1]) + val[i - 1];\n    // Сохранить и вернуть вариант с большей стоимостью из двух решений\n    mem[i][c] = Math.Max(no, yes);\n    return mem[i][c];\n}\n
        knapsack.go
        /* Рюкзак 0-1: поиск с мемоизацией */\nfunc knapsackDFSMem(wgt, val []int, mem [][]int, i, c int) int {\n    // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if i == 0 || c == 0 {\n        return 0\n    }\n    // Если запись уже есть, вернуть сразу\n    if mem[i][c] != -1 {\n        return mem[i][c]\n    }\n    // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if wgt[i-1] > c {\n        return knapsackDFSMem(wgt, val, mem, i-1, c)\n    }\n    // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    no := knapsackDFSMem(wgt, val, mem, i-1, c)\n    yes := knapsackDFSMem(wgt, val, mem, i-1, c-wgt[i-1]) + val[i-1]\n    // Вернуть вариант с большей стоимостью из двух возможных\n    mem[i][c] = int(math.Max(float64(no), float64(yes)))\n    return mem[i][c]\n}\n
        knapsack.swift
        /* Рюкзак 0-1: поиск с мемоизацией */\nfunc knapsackDFSMem(wgt: [Int], val: [Int], mem: inout [[Int]], i: Int, c: Int) -> Int {\n    // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if i == 0 || c == 0 {\n        return 0\n    }\n    // Если запись уже есть, вернуть сразу\n    if mem[i][c] != -1 {\n        return mem[i][c]\n    }\n    // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if wgt[i - 1] > c {\n        return knapsackDFSMem(wgt: wgt, val: val, mem: &mem, i: i - 1, c: c)\n    }\n    // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    let no = knapsackDFSMem(wgt: wgt, val: val, mem: &mem, i: i - 1, c: c)\n    let yes = knapsackDFSMem(wgt: wgt, val: val, mem: &mem, i: i - 1, c: c - wgt[i - 1]) + val[i - 1]\n    // Сохранить и вернуть вариант с большей стоимостью из двух решений\n    mem[i][c] = max(no, yes)\n    return mem[i][c]\n}\n
        knapsack.js
        /* Рюкзак 0-1: поиск с мемоизацией */\nfunction knapsackDFSMem(wgt, val, mem, i, c) {\n    // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if (i === 0 || c === 0) {\n        return 0;\n    }\n    // Если запись уже есть, вернуть сразу\n    if (mem[i][c] !== -1) {\n        return mem[i][c];\n    }\n    // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, mem, i - 1, c);\n    }\n    // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    const no = knapsackDFSMem(wgt, val, mem, i - 1, c);\n    const yes =\n        knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // Сохранить и вернуть вариант с большей стоимостью из двух решений\n    mem[i][c] = Math.max(no, yes);\n    return mem[i][c];\n}\n
        knapsack.ts
        /* Рюкзак 0-1: поиск с мемоизацией */\nfunction knapsackDFSMem(\n    wgt: Array<number>,\n    val: Array<number>,\n    mem: Array<Array<number>>,\n    i: number,\n    c: number\n): number {\n    // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if (i === 0 || c === 0) {\n        return 0;\n    }\n    // Если запись уже есть, вернуть сразу\n    if (mem[i][c] !== -1) {\n        return mem[i][c];\n    }\n    // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, mem, i - 1, c);\n    }\n    // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    const no = knapsackDFSMem(wgt, val, mem, i - 1, c);\n    const yes =\n        knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // Сохранить и вернуть вариант с большей стоимостью из двух решений\n    mem[i][c] = Math.max(no, yes);\n    return mem[i][c];\n}\n
        knapsack.dart
        /* Рюкзак 0-1: поиск с мемоизацией */\nint knapsackDFSMem(\n  List<int> wgt,\n  List<int> val,\n  List<List<int>> mem,\n  int i,\n  int c,\n) {\n  // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n  if (i == 0 || c == 0) {\n    return 0;\n  }\n  // Если запись уже есть, вернуть сразу\n  if (mem[i][c] != -1) {\n    return mem[i][c];\n  }\n  // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n  if (wgt[i - 1] > c) {\n    return knapsackDFSMem(wgt, val, mem, i - 1, c);\n  }\n  // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n  int no = knapsackDFSMem(wgt, val, mem, i - 1, c);\n  int yes = knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n  // Сохранить и вернуть вариант с большей стоимостью из двух решений\n  mem[i][c] = max(no, yes);\n  return mem[i][c];\n}\n
        knapsack.rs
        /* Рюкзак 0-1: поиск с мемоизацией */\nfn knapsack_dfs_mem(wgt: &[i32], val: &[i32], mem: &mut Vec<Vec<i32>>, i: usize, c: usize) -> i32 {\n    // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if i == 0 || c == 0 {\n        return 0;\n    }\n    // Если запись уже есть, вернуть сразу\n    if mem[i][c] != -1 {\n        return mem[i][c];\n    }\n    // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if wgt[i - 1] > c as i32 {\n        return knapsack_dfs_mem(wgt, val, mem, i - 1, c);\n    }\n    // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    let no = knapsack_dfs_mem(wgt, val, mem, i - 1, c);\n    let yes = knapsack_dfs_mem(wgt, val, mem, i - 1, c - wgt[i - 1] as usize) + val[i - 1];\n    // Сохранить и вернуть вариант с большей стоимостью из двух решений\n    mem[i][c] = std::cmp::max(no, yes);\n    mem[i][c]\n}\n
        knapsack.c
        /* Рюкзак 0-1: поиск с мемоизацией */\nint knapsackDFSMem(int wgt[], int val[], int memCols, int **mem, int i, int c) {\n    // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // Если запись уже есть, вернуть сразу\n    if (mem[i][c] != -1) {\n        return mem[i][c];\n    }\n    // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, memCols, mem, i - 1, c);\n    }\n    // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    int no = knapsackDFSMem(wgt, val, memCols, mem, i - 1, c);\n    int yes = knapsackDFSMem(wgt, val, memCols, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // Сохранить и вернуть вариант с большей стоимостью из двух решений\n    mem[i][c] = myMax(no, yes);\n    return mem[i][c];\n}\n
        knapsack.kt
        /* Рюкзак 0-1: поиск с мемоизацией */\nfun knapsackDFSMem(\n    wgt: IntArray,\n    _val: IntArray,\n    mem: Array<IntArray>,\n    i: Int,\n    c: Int\n): Int {\n    // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if (i == 0 || c == 0) {\n        return 0\n    }\n    // Если запись уже есть, вернуть сразу\n    if (mem[i][c] != -1) {\n        return mem[i][c]\n    }\n    // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, _val, mem, i - 1, c)\n    }\n    // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    val no = knapsackDFSMem(wgt, _val, mem, i - 1, c)\n    val yes = knapsackDFSMem(wgt, _val, mem, i - 1, c - wgt[i - 1]) + _val[i - 1]\n    // Сохранить и вернуть вариант с большей стоимостью из двух решений\n    mem[i][c] = max(no, yes)\n    return mem[i][c]\n}\n
        knapsack.rb
        ### Рюкзак 0-1: поиск с мемоизацией ###\ndef knapsack_dfs_mem(wgt, val, mem, i, c)\n  # Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n  return 0 if i == 0 || c == 0\n  # Если запись уже есть, вернуть сразу\n  return mem[i][c] if mem[i][c] != -1\n  # Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n  return knapsack_dfs_mem(wgt, val, mem, i - 1, c) if wgt[i - 1] > c\n  # Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n  no = knapsack_dfs_mem(wgt, val, mem, i - 1, c)\n  yes = knapsack_dfs_mem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1]\n  # Сохранить и вернуть вариант с большей стоимостью из двух решений\n  mem[i][c] = [no, yes].max\nend\n
        Визуализация кода

        Во весь экран >

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

        Рисунок 14-19   Дерево поиска с мемоизацией для задачи о рюкзаке 0-1

        ","path":["Глава 14. Динамическое программирование","14.4   Задача о рюкзаке 0-1"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#3-3","level":3,"title":"3.   Метод 3: динамическое программирование","text":"

        По своей сути динамическое программирование здесь - это процесс последовательного заполнения таблицы \\(dp\\) в соответствии с переходами состояний. Код приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py
        def knapsack_dp(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"Рюкзак 0-1: динамическое программирование\"\"\"\n    n = len(wgt)\n    # Инициализация таблицы dp\n    dp = [[0] * (cap + 1) for _ in range(n + 1)]\n    # Переход состояний\n    for i in range(1, n + 1):\n        for c in range(1, cap + 1):\n            if wgt[i - 1] > c:\n                # Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i][c] = dp[i - 1][c]\n            else:\n                # Большее из двух решений: не брать или взять предмет i\n                dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1])\n    return dp[n][cap]\n
        knapsack.cpp
        /* Рюкзак 0-1: динамическое программирование */\nint knapsackDP(vector<int> &wgt, vector<int> &val, int cap) {\n    int n = wgt.size();\n    // Инициализация таблицы dp\n    vector<vector<int>> dp(n + 1, vector<int>(cap + 1, 0));\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
        knapsack.java
        /* Рюкзак 0-1: динамическое программирование */\nint knapsackDP(int[] wgt, int[] val, int cap) {\n    int n = wgt.length;\n    // Инициализация таблицы dp\n    int[][] dp = new int[n + 1][cap + 1];\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[i][c] = Math.max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
        knapsack.cs
        /* Рюкзак 0-1: динамическое программирование */\nint KnapsackDP(int[] weight, int[] val, int cap) {\n    int n = weight.Length;\n    // Инициализация таблицы dp\n    int[,] dp = new int[n + 1, cap + 1];\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (weight[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i, c] = dp[i - 1, c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[i, c] = Math.Max(dp[i - 1, c - weight[i - 1]] + val[i - 1], dp[i - 1, c]);\n            }\n        }\n    }\n    return dp[n, cap];\n}\n
        knapsack.go
        /* Рюкзак 0-1: динамическое программирование */\nfunc knapsackDP(wgt, val []int, cap int) int {\n    n := len(wgt)\n    // Инициализация таблицы dp\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, cap+1)\n    }\n    // Переход состояний\n    for i := 1; i <= n; i++ {\n        for c := 1; c <= cap; c++ {\n            if wgt[i-1] > c {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i][c] = dp[i-1][c]\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[i][c] = int(math.Max(float64(dp[i-1][c]), float64(dp[i-1][c-wgt[i-1]]+val[i-1])))\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
        knapsack.swift
        /* Рюкзак 0-1: динамическое программирование */\nfunc knapsackDP(wgt: [Int], val: [Int], cap: Int) -> Int {\n    let n = wgt.count\n    // Инициализация таблицы dp\n    var dp = Array(repeating: Array(repeating: 0, count: cap + 1), count: n + 1)\n    // Переход состояний\n    for i in 1 ... n {\n        for c in 1 ... cap {\n            if wgt[i - 1] > c {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i][c] = dp[i - 1][c]\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1])\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
        knapsack.js
        /* Рюкзак 0-1: динамическое программирование */\nfunction knapsackDP(wgt, val, cap) {\n    const n = wgt.length;\n    // Инициализация таблицы dp\n    const dp = Array(n + 1)\n        .fill(0)\n        .map(() => Array(cap + 1).fill(0));\n    // Переход состояний\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[i][c] = Math.max(\n                    dp[i - 1][c],\n                    dp[i - 1][c - wgt[i - 1]] + val[i - 1]\n                );\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
        knapsack.ts
        /* Рюкзак 0-1: динамическое программирование */\nfunction knapsackDP(\n    wgt: Array<number>,\n    val: Array<number>,\n    cap: number\n): number {\n    const n = wgt.length;\n    // Инициализация таблицы dp\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: cap + 1 }, () => 0)\n    );\n    // Переход состояний\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[i][c] = Math.max(\n                    dp[i - 1][c],\n                    dp[i - 1][c - wgt[i - 1]] + val[i - 1]\n                );\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
        knapsack.dart
        /* Рюкзак 0-1: динамическое программирование */\nint knapsackDP(List<int> wgt, List<int> val, int cap) {\n  int n = wgt.length;\n  // Инициализация таблицы dp\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(cap + 1, 0));\n  // Переход состояний\n  for (int i = 1; i <= n; i++) {\n    for (int c = 1; c <= cap; c++) {\n      if (wgt[i - 1] > c) {\n        // Если вместимость рюкзака превышена, предмет i не выбирать\n        dp[i][c] = dp[i - 1][c];\n      } else {\n        // Большее из двух решений: не брать или взять предмет i\n        dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);\n      }\n    }\n  }\n  return dp[n][cap];\n}\n
        knapsack.rs
        /* Рюкзак 0-1: динамическое программирование */\nfn knapsack_dp(wgt: &[i32], val: &[i32], cap: usize) -> i32 {\n    let n = wgt.len();\n    // Инициализация таблицы dp\n    let mut dp = vec![vec![0; cap + 1]; n + 1];\n    // Переход состояний\n    for i in 1..=n {\n        for c in 1..=cap {\n            if wgt[i - 1] > c as i32 {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[i][c] = std::cmp::max(\n                    dp[i - 1][c],\n                    dp[i - 1][c - wgt[i - 1] as usize] + val[i - 1],\n                );\n            }\n        }\n    }\n    dp[n][cap]\n}\n
        knapsack.c
        /* Рюкзак 0-1: динамическое программирование */\nint knapsackDP(int wgt[], int val[], int cap, int wgtSize) {\n    int n = wgtSize;\n    // Инициализация таблицы dp\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(cap + 1, sizeof(int));\n    }\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[i][c] = myMax(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    int res = dp[n][cap];\n    // Освободить память\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    return res;\n}\n
        knapsack.kt
        /* Рюкзак 0-1: динамическое программирование */\nfun knapsackDP(wgt: IntArray, _val: IntArray, cap: Int): Int {\n    val n = wgt.size\n    // Инициализация таблицы dp\n    val dp = Array(n + 1) { IntArray(cap + 1) }\n    // Переход состояний\n    for (i in 1..n) {\n        for (c in 1..cap) {\n            if (wgt[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i][c] = dp[i - 1][c]\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + _val[i - 1])\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
        knapsack.rb
        ### Рюкзак 0-1: динамическое программирование ###\ndef knapsack_dp(wgt, val, cap)\n  n = wgt.length\n  # Инициализация таблицы dp\n  dp = Array.new(n + 1) { Array.new(cap + 1, 0) }\n  # Переход состояний\n  for i in 1...(n + 1)\n    for c in 1...(cap + 1)\n      if wgt[i - 1] > c\n        # Если вместимость рюкзака превышена, предмет i не выбирать\n        dp[i][c] = dp[i - 1][c]\n      else\n        # Большее из двух решений: не брать или взять предмет i\n        dp[i][c] = [dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]].max\n      end\n    end\n  end\n  dp[n][cap]\nend\n
        Визуализация кода

        Во весь экран >

        Как показано на рисунке 14-20, и временная сложность, и пространственная сложность определяются размером массива dp , то есть равны \\(O(n \\times cap)\\) .

        <1><2><3><4><5><6><7><8><9><10><11><12><13><14>

        Рисунок 14-20   Процесс динамического программирования для задачи о рюкзаке 0-1

        ","path":["Глава 14. Динамическое программирование","14.4   Задача о рюкзаке 0-1"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#4","level":3,"title":"4.   Оптимизация пространства","text":"

        Поскольку каждое состояние зависит только от состояния в предыдущей строке, можно использовать два массива, которые будут продвигаться вперед по очереди, и тем самым уменьшить пространственную сложность с \\(O(n^2)\\) до \\(O(n)\\) .

        Если пойти дальше, можно спросить: можно ли оптимизировать память так, чтобы использовать только один массив? Наблюдение показывает, что каждое состояние зависит от клетки прямо сверху и клетки слева сверху. Предположим, что у нас есть только один массив, и в момент начала обхода строки \\(i\\) он еще хранит состояния строки \\(i-1\\) .

        • Если обходить массив слева направо, то к моменту вычисления \\(dp[i, j]\\) значения слева сверху \\(dp[i-1, 1]\\) ~ \\(dp[i-1, j-1]\\) могут уже быть перезаписаны, и правильный результат перехода состояния получить не удастся.
        • Если же обходить массив справа налево, проблема перезаписи не возникает, и переход состояния вычисляется корректно.

        На рисунке 14-21 показан процесс перехода от строки \\(i = 1\\) к строке \\(i = 2\\) при использовании одного массива. С его помощью удобно понять различие между прямым и обратным обходом.

        <1><2><3><4><5><6>

        Рисунок 14-21   Процесс динамического программирования после оптимизации памяти для рюкзака 0-1

        В коде для этого достаточно удалить первое измерение массива dp , а внутренний цикл заменить на обратный обход:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py
        def knapsack_dp_comp(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"Рюкзак 0-1: динамическое программирование с оптимизацией памяти\"\"\"\n    n = len(wgt)\n    # Инициализация таблицы dp\n    dp = [0] * (cap + 1)\n    # Переход состояний\n    for i in range(1, n + 1):\n        # Обход в обратном порядке\n        for c in range(cap, 0, -1):\n            if wgt[i - 1] > c:\n                # Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[c] = dp[c]\n            else:\n                # Большее из двух решений: не брать или взять предмет i\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])\n    return dp[cap]\n
        knapsack.cpp
        /* Рюкзак 0-1: динамическое программирование с оптимизацией памяти */\nint knapsackDPComp(vector<int> &wgt, vector<int> &val, int cap) {\n    int n = wgt.size();\n    // Инициализация таблицы dp\n    vector<int> dp(cap + 1, 0);\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        // Обход в обратном порядке\n        for (int c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
        knapsack.java
        /* Рюкзак 0-1: динамическое программирование с оптимизацией памяти */\nint knapsackDPComp(int[] wgt, int[] val, int cap) {\n    int n = wgt.length;\n    // Инициализация таблицы dp\n    int[] dp = new int[cap + 1];\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        // Обход в обратном порядке\n        for (int c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
        knapsack.cs
        /* Рюкзак 0-1: динамическое программирование с оптимизацией памяти */\nint KnapsackDPComp(int[] weight, int[] val, int cap) {\n    int n = weight.Length;\n    // Инициализация таблицы dp\n    int[] dp = new int[cap + 1];\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        // Обход в обратном порядке\n        for (int c = cap; c > 0; c--) {\n            if (weight[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[c] = dp[c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[c] = Math.Max(dp[c], dp[c - weight[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
        knapsack.go
        /* Рюкзак 0-1: динамическое программирование с оптимизацией памяти */\nfunc knapsackDPComp(wgt, val []int, cap int) int {\n    n := len(wgt)\n    // Инициализация таблицы dp\n    dp := make([]int, cap+1)\n    // Переход состояний\n    for i := 1; i <= n; i++ {\n        // Обход в обратном порядке\n        for c := cap; c >= 1; c-- {\n            if wgt[i-1] <= c {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[c] = int(math.Max(float64(dp[c]), float64(dp[c-wgt[i-1]]+val[i-1])))\n            }\n        }\n    }\n    return dp[cap]\n}\n
        knapsack.swift
        /* Рюкзак 0-1: динамическое программирование с оптимизацией памяти */\nfunc knapsackDPComp(wgt: [Int], val: [Int], cap: Int) -> Int {\n    let n = wgt.count\n    // Инициализация таблицы dp\n    var dp = Array(repeating: 0, count: cap + 1)\n    // Переход состояний\n    for i in 1 ... n {\n        // Обход в обратном порядке\n        for c in (1 ... cap).reversed() {\n            if wgt[i - 1] <= c {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])\n            }\n        }\n    }\n    return dp[cap]\n}\n
        knapsack.js
        /* Рюкзак 0-1: динамическое программирование с оптимизацией памяти */\nfunction knapsackDPComp(wgt, val, cap) {\n    const n = wgt.length;\n    // Инициализация таблицы dp\n    const dp = Array(cap + 1).fill(0);\n    // Переход состояний\n    for (let i = 1; i <= n; i++) {\n        // Обход в обратном порядке\n        for (let c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
        knapsack.ts
        /* Рюкзак 0-1: динамическое программирование с оптимизацией памяти */\nfunction knapsackDPComp(\n    wgt: Array<number>,\n    val: Array<number>,\n    cap: number\n): number {\n    const n = wgt.length;\n    // Инициализация таблицы dp\n    const dp = Array(cap + 1).fill(0);\n    // Переход состояний\n    for (let i = 1; i <= n; i++) {\n        // Обход в обратном порядке\n        for (let c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
        knapsack.dart
        /* Рюкзак 0-1: динамическое программирование с оптимизацией памяти */\nint knapsackDPComp(List<int> wgt, List<int> val, int cap) {\n  int n = wgt.length;\n  // Инициализация таблицы dp\n  List<int> dp = List.filled(cap + 1, 0);\n  // Переход состояний\n  for (int i = 1; i <= n; i++) {\n    // Обход в обратном порядке\n    for (int c = cap; c >= 1; c--) {\n      if (wgt[i - 1] <= c) {\n        // Большее из двух решений: не брать или взять предмет i\n        dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n      }\n    }\n  }\n  return dp[cap];\n}\n
        knapsack.rs
        /* Рюкзак 0-1: динамическое программирование с оптимизацией памяти */\nfn knapsack_dp_comp(wgt: &[i32], val: &[i32], cap: usize) -> i32 {\n    let n = wgt.len();\n    // Инициализация таблицы dp\n    let mut dp = vec![0; cap + 1];\n    // Переход состояний\n    for i in 1..=n {\n        // Обход в обратном порядке\n        for c in (1..=cap).rev() {\n            if wgt[i - 1] <= c as i32 {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[c] = std::cmp::max(dp[c], dp[c - wgt[i - 1] as usize] + val[i - 1]);\n            }\n        }\n    }\n    dp[cap]\n}\n
        knapsack.c
        /* Рюкзак 0-1: динамическое программирование с оптимизацией памяти */\nint knapsackDPComp(int wgt[], int val[], int cap, int wgtSize) {\n    int n = wgtSize;\n    // Инициализация таблицы dp\n    int *dp = calloc(cap + 1, sizeof(int));\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        // Обход в обратном порядке\n        for (int c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[c] = myMax(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    int res = dp[cap];\n    // Освободить память\n    free(dp);\n    return res;\n}\n
        knapsack.kt
        /* Рюкзак 0-1: динамическое программирование с оптимизацией памяти */\nfun knapsackDPComp(wgt: IntArray, _val: IntArray, cap: Int): Int {\n    val n = wgt.size\n    // Инициализация таблицы dp\n    val dp = IntArray(cap + 1)\n    // Переход состояний\n    for (i in 1..n) {\n        // Обход в обратном порядке\n        for (c in cap downTo 1) {\n            if (wgt[i - 1] <= c) {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + _val[i - 1])\n            }\n        }\n    }\n    return dp[cap]\n}\n
        knapsack.rb
        ### Рюкзак 0-1: динамическое программирование с оптимизацией памяти ###\ndef knapsack_dp_comp(wgt, val, cap)\n  n = wgt.length\n  # Инициализация таблицы dp\n  dp = Array.new(cap + 1, 0)\n  # Переход состояний\n  for i in 1...(n + 1)\n    # Обход в обратном порядке\n    for c in cap.downto(1)\n      if wgt[i - 1] > c\n        # Если вместимость рюкзака превышена, предмет i не выбирать\n        dp[c] = dp[c]\n      else\n        # Большее из двух решений: не брать или взять предмет i\n        dp[c] = [dp[c], dp[c - wgt[i - 1]] + val[i - 1]].max\n      end\n    end\n  end\n  dp[cap]\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 14. Динамическое программирование","14.4   Задача о рюкзаке 0-1"],"tags":[]},{"location":"chapter_dynamic_programming/summary/","level":1,"title":"14.7   Резюме","text":"","path":["Глава 14. Динамическое программирование","14.7   Резюме"],"tags":[]},{"location":"chapter_dynamic_programming/summary/#1","level":3,"title":"1.   Ключевые выводы","text":"
        • Динамическое программирование раскладывает задачу на подзадачи и повышает вычислительную эффективность за счет хранения решений этих подзадач и устранения повторных вычислений.
        • Если не учитывать затраты времени, то любую задачу динамического программирования можно решить с помощью поиска с возвратом (полного перебора), однако в дереве рекурсии возникает множество перекрывающихся подзадач, из-за чего эффективность крайне низка. После введения таблицы памяти можно хранить решения всех уже вычисленных подзадач и гарантировать, что каждая перекрывающаяся подзадача будет вычисляться только один раз.
        • Поиск с мемоизацией - это рекурсивный метод \"сверху вниз\", а соответствующее ему динамическое программирование - это итеративный метод \"снизу вверх\", похожий на заполнение таблицы. Поскольку текущее состояние обычно зависит только от части локальных состояний, можно убрать одно измерение таблицы \\(dp\\) и тем самым снизить пространственную сложность.
        • Разложение на подзадачи - это общий алгоритмический подход, но в методе \"разделяй и властвуй\", динамическом программировании и поиске с возвратом он имеет разные свойства.
        • Для задач динамического программирования характерны три главных свойства: перекрывающиеся подзадачи, оптимальная подструктура и отсутствие последствий.
        • Если оптимальное решение исходной задачи можно построить из оптимальных решений подзадач, то задача обладает оптимальной подструктурой.
        • Отсутствие последствий означает, что для данного состояния его дальнейшее развитие определяется только этим состоянием и не зависит от всех прошлых состояний. Многие задачи комбинаторной оптимизации этим свойством не обладают и потому не могут эффективно решаться с помощью динамического программирования.

        Задачи о рюкзаке

        • Задача о рюкзаке - один из самых типичных классов задач динамического программирования; она включает варианты 0-1 рюкзака, полного рюкзака, многократного рюкзака и другие.
        • В задаче о рюкзаке 0-1 состояние определяется как максимальная стоимость первых \\(i\\) предметов в рюкзаке вместимости \\(c\\) . Рассматривая два решения - не брать предмет и брать предмет, - можно получить оптимальную подструктуру и вывести уравнение перехода состояния. При оптимизации памяти, поскольку каждое состояние зависит от значения сверху и слева сверху, внутренний цикл нужно выполнять в обратном порядке, чтобы не перезаписать нужное значение.
        • В задаче о полном рюкзаке число экземпляров каждого предмета не ограничено, поэтому при выборе предмета переход состояния отличается от варианта 0-1. Поскольку состояние зависит от значения сверху и слева, после оптимизации памяти внутренний цикл следует выполнять в прямом порядке.
        • Задача о размене монет - это вариант задачи о полном рюкзаке. Здесь вместо \"максимальной стоимости\" ищется \"минимальное число монет\", поэтому в уравнении перехода \\(\\max()\\) заменяется на \\(\\min()\\) . Кроме того, вместо условия \"не превышать вместимость рюкзака\" нужно ровно набрать целевую сумму, поэтому значение \\(amt + 1\\) используется как обозначение недопустимого решения \"сумму набрать нельзя\".
        • В задаче о размене монет II вместо \"минимального числа монет\" требуется найти \"число комбинаций монет\", поэтому в уравнении перехода оператор \\(\\min()\\) заменяется на суммирование.

        Задача о расстоянии редактирования

        • Расстояние редактирования (расстояние Левенштейна) используется для измерения сходства двух строк и определяется как минимальное число операций редактирования, необходимых для преобразования одной строки в другую; допустимые операции - вставка, удаление и замена.
        • В задаче о расстоянии редактирования состояние определяется как минимальное число шагов редактирования, необходимых для преобразования первых \\(i\\) символов строки \\(s\\) в первые \\(j\\) символов строки \\(t\\) . Если \\(s[i] \\ne t[j]\\) , то существуют три решения: вставка, удаление и замена, и каждому из них соответствует своя остаточная подзадача. На этой основе выводятся оптимальная подструктура и уравнение перехода состояния. Если же \\(s[i] = t[j]\\) , то редактировать текущий символ не нужно.
        • В задаче о расстоянии редактирования состояние зависит от значений сверху, слева и слева сверху. Поэтому после оптимизации памяти ни прямой, ни обратный обход сам по себе не дает корректного перехода состояния. Для решения этой проблемы значение слева сверху временно сохраняется в отдельной переменной, что делает ситуацию эквивалентной задаче о полном рюкзаке и позволяет использовать прямой обход.
        ","path":["Глава 14. Динамическое программирование","14.7   Резюме"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/","level":1,"title":"14.5   Задача о полном рюкзаке","text":"

        В этом разделе сначала решим еще одну распространенную задачу о рюкзаке - задачу о полном рюкзаке, а затем рассмотрим один из ее типичных частных случаев: задачу о размене монет.

        ","path":["Глава 14. Динамическое программирование","14.5   Задача о полном рюкзаке"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1451","level":2,"title":"14.5.1   Задача о полном рюкзаке","text":"

        Question

        Даны \\(n\\) предметов. Вес \\(i\\)-го предмета равен \\(wgt[i-1]\\) , стоимость равна \\(val[i-1]\\) . Также дан рюкзак вместимости \\(cap\\) . Каждый предмет можно выбирать многократно. Найдите максимальную суммарную стоимость, которую можно поместить в рюкзак при заданной вместимости. Пример показан на рисунке 14-22.

        Рисунок 14-22   Пример данных для задачи о полном рюкзаке

        ","path":["Глава 14. Динамическое программирование","14.5   Задача о полном рюкзаке"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1","level":3,"title":"1.   Идея динамического программирования","text":"

        Задача о полном рюкзаке очень похожа на задачу о рюкзаке 0-1; разница состоит только в том, что количество выборов каждого предмета не ограничено.

        • В задаче о рюкзаке 0-1 каждого предмета существует только один экземпляр, поэтому после того как предмет \\(i\\) помещен в рюкзак, выбирать можно только из первых \\(i-1\\) предметов.
        • В задаче о полном рюкзаке количество предметов не ограничено, поэтому после того как предмет \\(i\\) помещен в рюкзак, можно продолжать выбирать из первых \\(i\\) предметов.

        При этом состояние \\([i, c]\\) в задаче о полном рюкзаке может изменяться двумя способами.

        • Не брать предмет \\(i\\) : как и в задаче о рюкзаке 0-1, переход осуществляется в \\([i-1, c]\\) .
        • Взять предмет \\(i\\) : в отличие от рюкзака 0-1 переход происходит в \\([i, c-wgt[i-1]]\\) .

        Следовательно, уравнение перехода состояния принимает вид:

        \\[ dp[i, c] = \\max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1]) \\]","path":["Глава 14. Динамическое программирование","14.5   Задача о полном рюкзаке"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#2","level":3,"title":"2.   Реализация кода","text":"

        Если сравнить код этой задачи с кодом задачи о рюкзаке 0-1, то окажется, что в переходе состояний меняется только одна деталь: вместо \\(i-1\\) появляется \\(i\\) ; все остальное остается таким же:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby unbounded_knapsack.py
        def unbounded_knapsack_dp(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"Полный рюкзак: динамическое программирование\"\"\"\n    n = len(wgt)\n    # Инициализация таблицы dp\n    dp = [[0] * (cap + 1) for _ in range(n + 1)]\n    # Переход состояний\n    for i in range(1, n + 1):\n        for c in range(1, cap + 1):\n            if wgt[i - 1] > c:\n                # Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i][c] = dp[i - 1][c]\n            else:\n                # Большее из двух решений: не брать или взять предмет i\n                dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1])\n    return dp[n][cap]\n
        unbounded_knapsack.cpp
        /* Полный рюкзак: динамическое программирование */\nint unboundedKnapsackDP(vector<int> &wgt, vector<int> &val, int cap) {\n    int n = wgt.size();\n    // Инициализация таблицы dp\n    vector<vector<int>> dp(n + 1, vector<int>(cap + 1, 0));\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
        unbounded_knapsack.java
        /* Полный рюкзак: динамическое программирование */\nint unboundedKnapsackDP(int[] wgt, int[] val, int cap) {\n    int n = wgt.length;\n    // Инициализация таблицы dp\n    int[][] dp = new int[n + 1][cap + 1];\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[i][c] = Math.max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
        unbounded_knapsack.cs
        /* Полный рюкзак: динамическое программирование */\nint UnboundedKnapsackDP(int[] wgt, int[] val, int cap) {\n    int n = wgt.Length;\n    // Инициализация таблицы dp\n    int[,] dp = new int[n + 1, cap + 1];\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i, c] = dp[i - 1, c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[i, c] = Math.Max(dp[i - 1, c], dp[i, c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n, cap];\n}\n
        unbounded_knapsack.go
        /* Полный рюкзак: динамическое программирование */\nfunc unboundedKnapsackDP(wgt, val []int, cap int) int {\n    n := len(wgt)\n    // Инициализация таблицы dp\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, cap+1)\n    }\n    // Переход состояний\n    for i := 1; i <= n; i++ {\n        for c := 1; c <= cap; c++ {\n            if wgt[i-1] > c {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i][c] = dp[i-1][c]\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[i][c] = int(math.Max(float64(dp[i-1][c]), float64(dp[i][c-wgt[i-1]]+val[i-1])))\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
        unbounded_knapsack.swift
        /* Полный рюкзак: динамическое программирование */\nfunc unboundedKnapsackDP(wgt: [Int], val: [Int], cap: Int) -> Int {\n    let n = wgt.count\n    // Инициализация таблицы dp\n    var dp = Array(repeating: Array(repeating: 0, count: cap + 1), count: n + 1)\n    // Переход состояний\n    for i in 1 ... n {\n        for c in 1 ... cap {\n            if wgt[i - 1] > c {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i][c] = dp[i - 1][c]\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1])\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
        unbounded_knapsack.js
        /* Полный рюкзак: динамическое программирование */\nfunction unboundedKnapsackDP(wgt, val, cap) {\n    const n = wgt.length;\n    // Инициализация таблицы dp\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: cap + 1 }, () => 0)\n    );\n    // Переход состояний\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[i][c] = Math.max(\n                    dp[i - 1][c],\n                    dp[i][c - wgt[i - 1]] + val[i - 1]\n                );\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
        unbounded_knapsack.ts
        /* Полный рюкзак: динамическое программирование */\nfunction unboundedKnapsackDP(\n    wgt: Array<number>,\n    val: Array<number>,\n    cap: number\n): number {\n    const n = wgt.length;\n    // Инициализация таблицы dp\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: cap + 1 }, () => 0)\n    );\n    // Переход состояний\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[i][c] = Math.max(\n                    dp[i - 1][c],\n                    dp[i][c - wgt[i - 1]] + val[i - 1]\n                );\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
        unbounded_knapsack.dart
        /* Полный рюкзак: динамическое программирование */\nint unboundedKnapsackDP(List<int> wgt, List<int> val, int cap) {\n  int n = wgt.length;\n  // Инициализация таблицы dp\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(cap + 1, 0));\n  // Переход состояний\n  for (int i = 1; i <= n; i++) {\n    for (int c = 1; c <= cap; c++) {\n      if (wgt[i - 1] > c) {\n        // Если вместимость рюкзака превышена, предмет i не выбирать\n        dp[i][c] = dp[i - 1][c];\n      } else {\n        // Большее из двух решений: не брать или взять предмет i\n        dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]);\n      }\n    }\n  }\n  return dp[n][cap];\n}\n
        unbounded_knapsack.rs
        /* Полный рюкзак: динамическое программирование */\nfn unbounded_knapsack_dp(wgt: &[i32], val: &[i32], cap: usize) -> i32 {\n    let n = wgt.len();\n    // Инициализация таблицы dp\n    let mut dp = vec![vec![0; cap + 1]; n + 1];\n    // Переход состояний\n    for i in 1..=n {\n        for c in 1..=cap {\n            if wgt[i - 1] > c as i32 {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[i][c] = std::cmp::max(dp[i - 1][c], dp[i][c - wgt[i - 1] as usize] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
        unbounded_knapsack.c
        /* Полный рюкзак: динамическое программирование */\nint unboundedKnapsackDP(int wgt[], int val[], int cap, int wgtSize) {\n    int n = wgtSize;\n    // Инициализация таблицы dp\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(cap + 1, sizeof(int));\n    }\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[i][c] = myMax(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    int res = dp[n][cap];\n    // Освободить память\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    return res;\n}\n
        unbounded_knapsack.kt
        /* Полный рюкзак: динамическое программирование */\nfun unboundedKnapsackDP(wgt: IntArray, _val: IntArray, cap: Int): Int {\n    val n = wgt.size\n    // Инициализация таблицы dp\n    val dp = Array(n + 1) { IntArray(cap + 1) }\n    // Переход состояний\n    for (i in 1..n) {\n        for (c in 1..cap) {\n            if (wgt[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i][c] = dp[i - 1][c]\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + _val[i - 1])\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
        unbounded_knapsack.rb
        ### Полный рюкзак: динамическое программирование ###\ndef unbounded_knapsack_dp(wgt, val, cap)\n  n = wgt.length\n  # Инициализация таблицы dp\n  dp = Array.new(n + 1) { Array.new(cap + 1, 0) }\n  # Переход состояний\n  for i in 1...(n + 1)\n    for c in 1...(cap + 1)\n      if wgt[i - 1] > c\n        # Если вместимость рюкзака превышена, предмет i не выбирать\n        dp[i][c] = dp[i - 1][c]\n      else\n        # Большее из двух решений: не брать или взять предмет i\n        dp[i][c] = [dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]].max\n      end\n    end\n  end\n  dp[n][cap]\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 14. Динамическое программирование","14.5   Задача о полном рюкзаке"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#3","level":3,"title":"3.   Оптимизация пространства","text":"

        Поскольку текущее состояние переходит из состояния слева и состояния сверху, после оптимизации памяти каждую строку таблицы \\(dp\\) нужно обходить слева направо.

        Этот порядок обхода как раз противоположен задаче о рюкзаке 0-1. Разницу удобно понять по рисунку ниже.

        <1><2><3><4><5><6>

        Рисунок 14-23   Процесс динамического программирования после оптимизации памяти для полного рюкзака

        Код реализации здесь довольно прост: достаточно просто убрать первое измерение массива dp :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby unbounded_knapsack.py
        def unbounded_knapsack_dp_comp(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"Полный рюкзак: динамическое программирование с оптимизацией памяти\"\"\"\n    n = len(wgt)\n    # Инициализация таблицы dp\n    dp = [0] * (cap + 1)\n    # Переход состояний\n    for i in range(1, n + 1):\n        # Прямой обход\n        for c in range(1, cap + 1):\n            if wgt[i - 1] > c:\n                # Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[c] = dp[c]\n            else:\n                # Большее из двух решений: не брать или взять предмет i\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])\n    return dp[cap]\n
        unbounded_knapsack.cpp
        /* Полный рюкзак: динамическое программирование с оптимизацией памяти */\nint unboundedKnapsackDPComp(vector<int> &wgt, vector<int> &val, int cap) {\n    int n = wgt.size();\n    // Инициализация таблицы dp\n    vector<int> dp(cap + 1, 0);\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[c] = dp[c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
        unbounded_knapsack.java
        /* Полный рюкзак: динамическое программирование с оптимизацией памяти */\nint unboundedKnapsackDPComp(int[] wgt, int[] val, int cap) {\n    int n = wgt.length;\n    // Инициализация таблицы dp\n    int[] dp = new int[cap + 1];\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[c] = dp[c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
        unbounded_knapsack.cs
        /* Полный рюкзак: динамическое программирование с оптимизацией памяти */\nint UnboundedKnapsackDPComp(int[] wgt, int[] val, int cap) {\n    int n = wgt.Length;\n    // Инициализация таблицы dp\n    int[] dp = new int[cap + 1];\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[c] = dp[c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[c] = Math.Max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
        unbounded_knapsack.go
        /* Полный рюкзак: динамическое программирование с оптимизацией памяти */\nfunc unboundedKnapsackDPComp(wgt, val []int, cap int) int {\n    n := len(wgt)\n    // Инициализация таблицы dp\n    dp := make([]int, cap+1)\n    // Переход состояний\n    for i := 1; i <= n; i++ {\n        for c := 1; c <= cap; c++ {\n            if wgt[i-1] > c {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[c] = dp[c]\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[c] = int(math.Max(float64(dp[c]), float64(dp[c-wgt[i-1]]+val[i-1])))\n            }\n        }\n    }\n    return dp[cap]\n}\n
        unbounded_knapsack.swift
        /* Полный рюкзак: динамическое программирование с оптимизацией памяти */\nfunc unboundedKnapsackDPComp(wgt: [Int], val: [Int], cap: Int) -> Int {\n    let n = wgt.count\n    // Инициализация таблицы dp\n    var dp = Array(repeating: 0, count: cap + 1)\n    // Переход состояний\n    for i in 1 ... n {\n        for c in 1 ... cap {\n            if wgt[i - 1] > c {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[c] = dp[c]\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])\n            }\n        }\n    }\n    return dp[cap]\n}\n
        unbounded_knapsack.js
        /* Полный рюкзак: динамическое программирование с оптимизацией памяти */\nfunction unboundedKnapsackDPComp(wgt, val, cap) {\n    const n = wgt.length;\n    // Инициализация таблицы dp\n    const dp = Array.from({ length: cap + 1 }, () => 0);\n    // Переход состояний\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[c] = dp[c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
        unbounded_knapsack.ts
        /* Полный рюкзак: динамическое программирование с оптимизацией памяти */\nfunction unboundedKnapsackDPComp(\n    wgt: Array<number>,\n    val: Array<number>,\n    cap: number\n): number {\n    const n = wgt.length;\n    // Инициализация таблицы dp\n    const dp = Array.from({ length: cap + 1 }, () => 0);\n    // Переход состояний\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[c] = dp[c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
        unbounded_knapsack.dart
        /* Полный рюкзак: динамическое программирование с оптимизацией памяти */\nint unboundedKnapsackDPComp(List<int> wgt, List<int> val, int cap) {\n  int n = wgt.length;\n  // Инициализация таблицы dp\n  List<int> dp = List.filled(cap + 1, 0);\n  // Переход состояний\n  for (int i = 1; i <= n; i++) {\n    for (int c = 1; c <= cap; c++) {\n      if (wgt[i - 1] > c) {\n        // Если вместимость рюкзака превышена, предмет i не выбирать\n        dp[c] = dp[c];\n      } else {\n        // Большее из двух решений: не брать или взять предмет i\n        dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n      }\n    }\n  }\n  return dp[cap];\n}\n
        unbounded_knapsack.rs
        /* Полный рюкзак: динамическое программирование с оптимизацией памяти */\nfn unbounded_knapsack_dp_comp(wgt: &[i32], val: &[i32], cap: usize) -> i32 {\n    let n = wgt.len();\n    // Инициализация таблицы dp\n    let mut dp = vec![0; cap + 1];\n    // Переход состояний\n    for i in 1..=n {\n        for c in 1..=cap {\n            if wgt[i - 1] > c as i32 {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[c] = dp[c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[c] = std::cmp::max(dp[c], dp[c - wgt[i - 1] as usize] + val[i - 1]);\n            }\n        }\n    }\n    dp[cap]\n}\n
        unbounded_knapsack.c
        /* Полный рюкзак: динамическое программирование с оптимизацией памяти */\nint unboundedKnapsackDPComp(int wgt[], int val[], int cap, int wgtSize) {\n    int n = wgtSize;\n    // Инициализация таблицы dp\n    int *dp = calloc(cap + 1, sizeof(int));\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[c] = dp[c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[c] = myMax(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    int res = dp[cap];\n    // Освободить память\n    free(dp);\n    return res;\n}\n
        unbounded_knapsack.kt
        /* Полный рюкзак: динамическое программирование с оптимизацией памяти */\nfun unboundedKnapsackDPComp(\n    wgt: IntArray,\n    _val: IntArray,\n    cap: Int\n): Int {\n    val n = wgt.size\n    // Инициализация таблицы dp\n    val dp = IntArray(cap + 1)\n    // Переход состояний\n    for (i in 1..n) {\n        for (c in 1..cap) {\n            if (wgt[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[c] = dp[c]\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + _val[i - 1])\n            }\n        }\n    }\n    return dp[cap]\n}\n
        unbounded_knapsack.rb
        ### Полный рюкзак: динамическое программирование ###\ndef unbounded_knapsack_dp(wgt, val, cap)\n  n = wgt.length\n  # Инициализация таблицы dp\n  dp = Array.new(n + 1) { Array.new(cap + 1, 0) }\n  # Переход состояний\n  for i in 1...(n + 1)\n    for c in 1...(cap + 1)\n      if wgt[i - 1] > c\n        # Если вместимость рюкзака превышена, предмет i не выбирать\n        dp[i][c] = dp[i - 1][c]\n      else\n        # Большее из двух решений: не брать или взять предмет i\n        dp[i][c] = [dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]].max\n      end\n    end\n  end\n  dp[n][cap]\nend\n\n# ## Полный рюкзак: динамическое программирование с оптимизацией памяти ##3\ndef unbounded_knapsack_dp_comp(wgt, val, cap)\n  n = wgt.length\n  # Инициализация таблицы dp\n  dp = Array.new(cap + 1, 0)\n  # Переход состояний\n  for i in 1...(n + 1)\n    # Прямой обход\n    for c in 1...(cap + 1)\n      if wgt[i -1] > c\n        # Если вместимость рюкзака превышена, предмет i не выбирать\n        dp[c] = dp[c]\n      else\n        # Большее из двух решений: не брать или взять предмет i\n        dp[c] = [dp[c], dp[c - wgt[i - 1]] + val[i - 1]].max\n      end\n    end\n  end\n  dp[cap]\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 14. Динамическое программирование","14.5   Задача о полном рюкзаке"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1452","level":2,"title":"14.5.2   Задача о размене монет","text":"

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

        Question

        Даны \\(n\\) видов монет, номинал монеты \\(i\\) равен \\(coins[i - 1]\\) , а целевая сумма равна \\(amt\\) . Монеты каждого вида можно брать многократно. Требуется найти минимальное число монет, которыми можно набрать целевую сумму. Если набрать сумму невозможно, верните \\(-1\\) . Пример показан на рисунке 14-24.

        Рисунок 14-24   Пример данных для задачи о размене монет

        ","path":["Глава 14. Динамическое программирование","14.5   Задача о полном рюкзаке"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1_1","level":3,"title":"1.   Идея динамического программирования","text":"

        Задачу о размене монет можно рассматривать как частный случай задачи о полном рюкзаке ; между ними существуют следующие соответствия и различия.

        • Эти две задачи можно взаимно преобразовать: \"предмет\" соответствует \"монете\", \"вес предмета\" соответствует \"номиналу монеты\", а \"вместимость рюкзака\" соответствует \"целевой сумме\".
        • Цель оптимизации противоположна: в задаче о полном рюкзаке нужно максимизировать стоимость предметов, а в задаче о размене монет - минимизировать число монет.
        • В задаче о полном рюкзаке ищется решение, не превышающее вместимость, а в задаче о размене монет требуется ровно набрать целевую сумму.

        Шаг 1: продумать решения на каждом раунде, определить состояние и тем самым получить таблицу \\(dp\\)

        Подзадача, соответствующая состоянию \\([i, a]\\) , выглядит так: минимальное число монет из первых \\(i\\) видов, которыми можно набрать сумму \\(a\\). Решение этой подзадачи обозначается как \\(dp[i, a]\\) .

        Размер двумерной таблицы \\(dp\\) равен \\((n+1) \\times (amt+1)\\) .

        Шаг 2: найти оптимальную подструктуру и на ее основе вывести уравнение перехода состояния

        По сравнению с задачей о полном рюкзаке здесь есть два отличия в уравнении перехода состояния.

        • Нужно искать минимум, а не максимум, поэтому оператор \\(\\max()\\) заменяется на \\(\\min()\\) .
        • Оптимизируемое значение - это число монет, а не суммарная стоимость, поэтому при выборе монеты нужно просто прибавить \\(1\\) .
        \\[ dp[i, a] = \\min(dp[i-1, a], dp[i, a - coins[i-1]] + 1) \\]

        Шаг 3: определить граничные условия и порядок переходов

        Когда целевая сумма равна \\(0\\) , минимальное число монет для ее набора равно \\(0\\) , то есть весь первый столбец \\(dp[i, 0]\\) заполняется нулями.

        Когда монет нет, невозможно набрать никакую целевую сумму \\(> 0\\) ; это и есть недопустимое решение. Чтобы функция \\(\\min()\\) в уравнении перехода состояния могла распознавать и отбрасывать такие недопустимые решения, удобно использовать значение \\(+ \\infty\\) ; то есть всю первую строку \\(dp[0, a]\\) нужно инициализировать значением \\(+ \\infty\\) .

        ","path":["Глава 14. Динамическое программирование","14.5   Задача о полном рюкзаке"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#2_1","level":3,"title":"2.   Реализация кода","text":"

        Большинство языков программирования не предоставляет представление для \\(+ \\infty\\) в целочисленном виде, поэтому обычно приходится заменять его на максимальное значение типа int . Но тогда возникает риск переполнения: операция \\(+ 1\\) в уравнении перехода может переполнить большое число.

        Поэтому здесь мы используем число \\(amt + 1\\) как обозначение недопустимого решения, потому что для набора суммы \\(amt\\) максимум нужно не больше чем \\(amt\\) монет. Перед возвратом результата проверяем, равно ли \\(dp[n, amt]\\) значению \\(amt + 1\\) ; если да, то возвращаем \\(-1\\) , что означает невозможность набрать целевую сумму. Код приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change.py
        def coin_change_dp(coins: list[int], amt: int) -> int:\n    \"\"\"Размен монет: динамическое программирование\"\"\"\n    n = len(coins)\n    MAX = amt + 1\n    # Инициализация таблицы dp\n    dp = [[0] * (amt + 1) for _ in range(n + 1)]\n    # Переход состояний: первая строка и первый столбец\n    for a in range(1, amt + 1):\n        dp[0][a] = MAX\n    # Переход состояний: остальные строки и столбцы\n    for i in range(1, n + 1):\n        for a in range(1, amt + 1):\n            if coins[i - 1] > a:\n                # Если целевая сумма превышена, монету i не выбирать\n                dp[i][a] = dp[i - 1][a]\n            else:\n                # Меньшее из двух решений: не брать или взять монету i\n                dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1)\n    return dp[n][amt] if dp[n][amt] != MAX else -1\n
        coin_change.cpp
        /* Размен монет: динамическое программирование */\nint coinChangeDP(vector<int> &coins, int amt) {\n    int n = coins.size();\n    int MAX = amt + 1;\n    // Инициализация таблицы dp\n    vector<vector<int>> dp(n + 1, vector<int>(amt + 1, 0));\n    // Переход состояний: первая строка и первый столбец\n    for (int a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Меньшее из двух решений: не брать или взять монету i\n                dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[n][amt] != MAX ? dp[n][amt] : -1;\n}\n
        coin_change.java
        /* Размен монет: динамическое программирование */\nint coinChangeDP(int[] coins, int amt) {\n    int n = coins.length;\n    int MAX = amt + 1;\n    // Инициализация таблицы dp\n    int[][] dp = new int[n + 1][amt + 1];\n    // Переход состояний: первая строка и первый столбец\n    for (int a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Меньшее из двух решений: не брать или взять монету i\n                dp[i][a] = Math.min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[n][amt] != MAX ? dp[n][amt] : -1;\n}\n
        coin_change.cs
        /* Размен монет: динамическое программирование */\nint CoinChangeDP(int[] coins, int amt) {\n    int n = coins.Length;\n    int MAX = amt + 1;\n    // Инициализация таблицы dp\n    int[,] dp = new int[n + 1, amt + 1];\n    // Переход состояний: первая строка и первый столбец\n    for (int a = 1; a <= amt; a++) {\n        dp[0, a] = MAX;\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[i, a] = dp[i - 1, a];\n            } else {\n                // Меньшее из двух решений: не брать или взять монету i\n                dp[i, a] = Math.Min(dp[i - 1, a], dp[i, a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[n, amt] != MAX ? dp[n, amt] : -1;\n}\n
        coin_change.go
        /* Размен монет: динамическое программирование */\nfunc coinChangeDP(coins []int, amt int) int {\n    n := len(coins)\n    max := amt + 1\n    // Инициализация таблицы dp\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, amt+1)\n    }\n    // Переход состояний: первая строка и первый столбец\n    for a := 1; a <= amt; a++ {\n        dp[0][a] = max\n    }\n    // Переход состояний: остальные строки и столбцы\n    for i := 1; i <= n; i++ {\n        for a := 1; a <= amt; a++ {\n            if coins[i-1] > a {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[i][a] = dp[i-1][a]\n            } else {\n                // Меньшее из двух решений: не брать или взять монету i\n                dp[i][a] = int(math.Min(float64(dp[i-1][a]), float64(dp[i][a-coins[i-1]]+1)))\n            }\n        }\n    }\n    if dp[n][amt] != max {\n        return dp[n][amt]\n    }\n    return -1\n}\n
        coin_change.swift
        /* Размен монет: динамическое программирование */\nfunc coinChangeDP(coins: [Int], amt: Int) -> Int {\n    let n = coins.count\n    let MAX = amt + 1\n    // Инициализация таблицы dp\n    var dp = Array(repeating: Array(repeating: 0, count: amt + 1), count: n + 1)\n    // Переход состояний: первая строка и первый столбец\n    for a in 1 ... amt {\n        dp[0][a] = MAX\n    }\n    // Переход состояний: остальные строки и столбцы\n    for i in 1 ... n {\n        for a in 1 ... amt {\n            if coins[i - 1] > a {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[i][a] = dp[i - 1][a]\n            } else {\n                // Меньшее из двух решений: не брать или взять монету i\n                dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1)\n            }\n        }\n    }\n    return dp[n][amt] != MAX ? dp[n][amt] : -1\n}\n
        coin_change.js
        /* Размен монет: динамическое программирование */\nfunction coinChangeDP(coins, amt) {\n    const n = coins.length;\n    const MAX = amt + 1;\n    // Инициализация таблицы dp\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: amt + 1 }, () => 0)\n    );\n    // Переход состояний: первая строка и первый столбец\n    for (let a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Меньшее из двух решений: не брать или взять монету i\n                dp[i][a] = Math.min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[n][amt] !== MAX ? dp[n][amt] : -1;\n}\n
        coin_change.ts
        /* Размен монет: динамическое программирование */\nfunction coinChangeDP(coins: Array<number>, amt: number): number {\n    const n = coins.length;\n    const MAX = amt + 1;\n    // Инициализация таблицы dp\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: amt + 1 }, () => 0)\n    );\n    // Переход состояний: первая строка и первый столбец\n    for (let a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Меньшее из двух решений: не брать или взять монету i\n                dp[i][a] = Math.min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[n][amt] !== MAX ? dp[n][amt] : -1;\n}\n
        coin_change.dart
        /* Размен монет: динамическое программирование */\nint coinChangeDP(List<int> coins, int amt) {\n  int n = coins.length;\n  int MAX = amt + 1;\n  // Инициализация таблицы dp\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(amt + 1, 0));\n  // Переход состояний: первая строка и первый столбец\n  for (int a = 1; a <= amt; a++) {\n    dp[0][a] = MAX;\n  }\n  // Переход состояний: остальные строки и столбцы\n  for (int i = 1; i <= n; i++) {\n    for (int a = 1; a <= amt; a++) {\n      if (coins[i - 1] > a) {\n        // Если целевая сумма превышена, монету i не выбирать\n        dp[i][a] = dp[i - 1][a];\n      } else {\n        // Меньшее из двух решений: не брать или взять монету i\n        dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n      }\n    }\n  }\n  return dp[n][amt] != MAX ? dp[n][amt] : -1;\n}\n
        coin_change.rs
        /* Размен монет: динамическое программирование */\nfn coin_change_dp(coins: &[i32], amt: usize) -> i32 {\n    let n = coins.len();\n    let max = amt + 1;\n    // Инициализация таблицы dp\n    let mut dp = vec![vec![0; amt + 1]; n + 1];\n    // Переход состояний: первая строка и первый столбец\n    for a in 1..=amt {\n        dp[0][a] = max;\n    }\n    // Переход состояний: остальные строки и столбцы\n    for i in 1..=n {\n        for a in 1..=amt {\n            if coins[i - 1] > a as i32 {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Меньшее из двух решений: не брать или взять монету i\n                dp[i][a] = std::cmp::min(dp[i - 1][a], dp[i][a - coins[i - 1] as usize] + 1);\n            }\n        }\n    }\n    if dp[n][amt] != max {\n        return dp[n][amt] as i32;\n    } else {\n        -1\n    }\n}\n
        coin_change.c
        /* Размен монет: динамическое программирование */\nint coinChangeDP(int coins[], int amt, int coinsSize) {\n    int n = coinsSize;\n    int MAX = amt + 1;\n    // Инициализация таблицы dp\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(amt + 1, sizeof(int));\n    }\n    // Переход состояний: первая строка и первый столбец\n    for (int a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Меньшее из двух решений: не брать или взять монету i\n                dp[i][a] = myMin(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    int res = dp[n][amt] != MAX ? dp[n][amt] : -1;\n    // Освободить память\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    free(dp);\n    return res;\n}\n
        coin_change.kt
        /* Размен монет: динамическое программирование */\nfun coinChangeDP(coins: IntArray, amt: Int): Int {\n    val n = coins.size\n    val MAX = amt + 1\n    // Инициализация таблицы dp\n    val dp = Array(n + 1) { IntArray(amt + 1) }\n    // Переход состояний: первая строка и первый столбец\n    for (a in 1..amt) {\n        dp[0][a] = MAX\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (i in 1..n) {\n        for (a in 1..amt) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[i][a] = dp[i - 1][a]\n            } else {\n                // Меньшее из двух решений: не брать или взять монету i\n                dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1)\n            }\n        }\n    }\n    return if (dp[n][amt] != MAX) dp[n][amt] else -1\n}\n
        coin_change.rb
        ### Размен монет: динамическое программирование ###\ndef coin_change_dp(coins, amt)\n  n = coins.length\n  _MAX = amt + 1\n  # Инициализация таблицы dp\n  dp = Array.new(n + 1) { Array.new(amt + 1, 0) }\n  # Переход состояний: первая строка и первый столбец\n  (1...(amt + 1)).each { |a| dp[0][a] = _MAX }\n  # Переход состояний: остальные строки и столбцы\n  for i in 1...(n + 1)\n    for a in 1...(amt + 1)\n      if coins[i - 1] > a\n        # Если целевая сумма превышена, монету i не выбирать\n        dp[i][a] = dp[i - 1][a]\n      else\n        # Меньшее из двух решений: не брать или взять монету i\n        dp[i][a] = [dp[i - 1][a], dp[i][a - coins[i - 1]] + 1].min\n      end\n    end\n  end\n  dp[n][amt] != _MAX ? dp[n][amt] : -1\nend\n
        Визуализация кода

        Во весь экран >

        Как показано на рисунке 14-25, процесс динамического программирования для задачи о размене монет очень похож на задачу о полном рюкзаке.

        <1><2><3><4><5><6><7><8><9><10><11><12><13><14><15>

        Рисунок 14-25   Процесс динамического программирования для задачи о размене монет

        ","path":["Глава 14. Динамическое программирование","14.5   Задача о полном рюкзаке"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#3_1","level":3,"title":"3.   Оптимизация пространства","text":"

        Оптимизация пространства для задачи о размене монет выполняется так же, как и для полного рюкзака:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change.py
        def coin_change_dp_comp(coins: list[int], amt: int) -> int:\n    \"\"\"Размен монет: динамическое программирование с оптимизацией памяти\"\"\"\n    n = len(coins)\n    MAX = amt + 1\n    # Инициализация таблицы dp\n    dp = [MAX] * (amt + 1)\n    dp[0] = 0\n    # Переход состояний\n    for i in range(1, n + 1):\n        # Прямой обход\n        for a in range(1, amt + 1):\n            if coins[i - 1] > a:\n                # Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a]\n            else:\n                # Меньшее из двух решений: не брать или взять монету i\n                dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1)\n    return dp[amt] if dp[amt] != MAX else -1\n
        coin_change.cpp
        /* Размен монет: динамическое программирование с оптимизацией памяти */\nint coinChangeDPComp(vector<int> &coins, int amt) {\n    int n = coins.size();\n    int MAX = amt + 1;\n    // Инициализация таблицы dp\n    vector<int> dp(amt + 1, MAX);\n    dp[0] = 0;\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a];\n            } else {\n                // Меньшее из двух решений: не брать или взять монету i\n                dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[amt] != MAX ? dp[amt] : -1;\n}\n
        coin_change.java
        /* Размен монет: динамическое программирование с оптимизацией памяти */\nint coinChangeDPComp(int[] coins, int amt) {\n    int n = coins.length;\n    int MAX = amt + 1;\n    // Инициализация таблицы dp\n    int[] dp = new int[amt + 1];\n    Arrays.fill(dp, MAX);\n    dp[0] = 0;\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a];\n            } else {\n                // Меньшее из двух решений: не брать или взять монету i\n                dp[a] = Math.min(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[amt] != MAX ? dp[amt] : -1;\n}\n
        coin_change.cs
        /* Размен монет: динамическое программирование с оптимизацией памяти */\nint CoinChangeDPComp(int[] coins, int amt) {\n    int n = coins.Length;\n    int MAX = amt + 1;\n    // Инициализация таблицы dp\n    int[] dp = new int[amt + 1];\n    Array.Fill(dp, MAX);\n    dp[0] = 0;\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a];\n            } else {\n                // Меньшее из двух решений: не брать или взять монету i\n                dp[a] = Math.Min(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[amt] != MAX ? dp[amt] : -1;\n}\n
        coin_change.go
        /* Размен монет: динамическое программирование */\nfunc coinChangeDPComp(coins []int, amt int) int {\n    n := len(coins)\n    max := amt + 1\n    // Инициализация таблицы dp\n    dp := make([]int, amt+1)\n    for i := 1; i <= amt; i++ {\n        dp[i] = max\n    }\n    // Переход состояний\n    for i := 1; i <= n; i++ {\n        // Прямой обход\n        for a := 1; a <= amt; a++ {\n            if coins[i-1] > a {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a]\n            } else {\n                // Меньшее из двух решений: не брать или взять монету i\n                dp[a] = int(math.Min(float64(dp[a]), float64(dp[a-coins[i-1]]+1)))\n            }\n        }\n    }\n    if dp[amt] != max {\n        return dp[amt]\n    }\n    return -1\n}\n
        coin_change.swift
        /* Размен монет: динамическое программирование с оптимизацией памяти */\nfunc coinChangeDPComp(coins: [Int], amt: Int) -> Int {\n    let n = coins.count\n    let MAX = amt + 1\n    // Инициализация таблицы dp\n    var dp = Array(repeating: MAX, count: amt + 1)\n    dp[0] = 0\n    // Переход состояний\n    for i in 1 ... n {\n        for a in 1 ... amt {\n            if coins[i - 1] > a {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a]\n            } else {\n                // Меньшее из двух решений: не брать или взять монету i\n                dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1)\n            }\n        }\n    }\n    return dp[amt] != MAX ? dp[amt] : -1\n}\n
        coin_change.js
        /* Размен монет: динамическое программирование с оптимизацией памяти */\nfunction coinChangeDPComp(coins, amt) {\n    const n = coins.length;\n    const MAX = amt + 1;\n    // Инициализация таблицы dp\n    const dp = Array.from({ length: amt + 1 }, () => MAX);\n    dp[0] = 0;\n    // Переход состояний\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a];\n            } else {\n                // Меньшее из двух решений: не брать или взять монету i\n                dp[a] = Math.min(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[amt] !== MAX ? dp[amt] : -1;\n}\n
        coin_change.ts
        /* Размен монет: динамическое программирование с оптимизацией памяти */\nfunction coinChangeDPComp(coins: Array<number>, amt: number): number {\n    const n = coins.length;\n    const MAX = amt + 1;\n    // Инициализация таблицы dp\n    const dp = Array.from({ length: amt + 1 }, () => MAX);\n    dp[0] = 0;\n    // Переход состояний\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a];\n            } else {\n                // Меньшее из двух решений: не брать или взять монету i\n                dp[a] = Math.min(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[amt] !== MAX ? dp[amt] : -1;\n}\n
        coin_change.dart
        /* Размен монет: динамическое программирование с оптимизацией памяти */\nint coinChangeDPComp(List<int> coins, int amt) {\n  int n = coins.length;\n  int MAX = amt + 1;\n  // Инициализация таблицы dp\n  List<int> dp = List.filled(amt + 1, MAX);\n  dp[0] = 0;\n  // Переход состояний\n  for (int i = 1; i <= n; i++) {\n    for (int a = 1; a <= amt; a++) {\n      if (coins[i - 1] > a) {\n        // Если целевая сумма превышена, монету i не выбирать\n        dp[a] = dp[a];\n      } else {\n        // Меньшее из двух решений: не брать или взять монету i\n        dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1);\n      }\n    }\n  }\n  return dp[amt] != MAX ? dp[amt] : -1;\n}\n
        coin_change.rs
        /* Размен монет: динамическое программирование с оптимизацией памяти */\nfn coin_change_dp_comp(coins: &[i32], amt: usize) -> i32 {\n    let n = coins.len();\n    let max = amt + 1;\n    // Инициализация таблицы dp\n    let mut dp = vec![0; amt + 1];\n    dp.fill(max);\n    dp[0] = 0;\n    // Переход состояний\n    for i in 1..=n {\n        for a in 1..=amt {\n            if coins[i - 1] > a as i32 {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a];\n            } else {\n                // Меньшее из двух решений: не брать или взять монету i\n                dp[a] = std::cmp::min(dp[a], dp[a - coins[i - 1] as usize] + 1);\n            }\n        }\n    }\n    if dp[amt] != max {\n        return dp[amt] as i32;\n    } else {\n        -1\n    }\n}\n
        coin_change.c
        /* Размен монет: динамическое программирование с оптимизацией памяти */\nint coinChangeDPComp(int coins[], int amt, int coinsSize) {\n    int n = coinsSize;\n    int MAX = amt + 1;\n    // Инициализация таблицы dp\n    int *dp = malloc((amt + 1) * sizeof(int));\n    for (int j = 1; j <= amt; j++) {\n        dp[j] = MAX;\n    } \n    dp[0] = 0;\n\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a];\n            } else {\n                // Меньшее из двух решений: не брать или взять монету i\n                dp[a] = myMin(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    int res = dp[amt] != MAX ? dp[amt] : -1;\n    // Освободить память\n    free(dp);\n    return res;\n}\n
        coin_change.kt
        /* Размен монет: динамическое программирование с оптимизацией памяти */\nfun coinChangeDPComp(coins: IntArray, amt: Int): Int {\n    val n = coins.size\n    val MAX = amt + 1\n    // Инициализация таблицы dp\n    val dp = IntArray(amt + 1)\n    dp.fill(MAX)\n    dp[0] = 0\n    // Переход состояний\n    for (i in 1..n) {\n        for (a in 1..amt) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a]\n            } else {\n                // Меньшее из двух решений: не брать или взять монету i\n                dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1)\n            }\n        }\n    }\n    return if (dp[amt] != MAX) dp[amt] else -1\n}\n
        coin_change.rb
        ### Размен монет: динамическое программирование с оптимизацией памяти ###\ndef coin_change_dp_comp(coins, amt)\n  n = coins.length\n  _MAX = amt + 1\n  # Инициализация таблицы dp\n  dp = Array.new(amt + 1, _MAX)\n  dp[0] = 0\n  # Переход состояний\n  for i in 1...(n + 1)\n    # Прямой обход\n    for a in 1...(amt + 1)\n      if coins[i - 1] > a\n        # Если целевая сумма превышена, монету i не выбирать\n        dp[a] = dp[a]\n      else\n        # Меньшее из двух решений: не брать или взять монету i\n        dp[a] = [dp[a], dp[a - coins[i - 1]] + 1].min\n      end\n    end\n  end\n  dp[amt] != _MAX ? dp[amt] : -1\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 14. Динамическое программирование","14.5   Задача о полном рюкзаке"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1453-ii","level":2,"title":"14.5.3   Задача о размене монет II","text":"

        Question

        Даны \\(n\\) видов монет, номинал монеты \\(i\\) равен \\(coins[i - 1]\\) , а целевая сумма равна \\(amt\\) . Монеты каждого вида можно брать многократно. Найдите число различных комбинаций монет, которыми можно набрать целевую сумму. Пример показан на рисунке 14-26.

        Рисунок 14-26   Пример данных для задачи о размене монет II

        ","path":["Глава 14. Динамическое программирование","14.5   Задача о полном рюкзаке"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1_2","level":3,"title":"1.   Идея динамического программирования","text":"

        По сравнению с предыдущей задачей здесь целью является число комбинаций. Поэтому подзадача меняется на следующую: число комбинаций из первых \\(i\\) видов монет, которыми можно набрать сумму \\(a\\). При этом таблица \\(dp\\) по-прежнему остается двумерной матрицей размера \\((n+1) \\times (amt + 1)\\) .

        Число комбинаций для текущего состояния равно сумме числа комбинаций для двух решений: не брать текущую монету и брать текущую монету. Поэтому уравнение перехода состояния принимает вид:

        \\[ dp[i, a] = dp[i-1, a] + dp[i, a - coins[i-1]] \\]

        Когда целевая сумма равна \\(0\\) , ее можно набрать, не выбирая ни одной монеты, поэтому весь первый столбец \\(dp[i, 0]\\) нужно инициализировать единицами. Когда монет нет, невозможно набрать никакую сумму \\(>0\\) , поэтому вся первая строка \\(dp[0, a]\\) должна быть заполнена нулями.

        ","path":["Глава 14. Динамическое программирование","14.5   Задача о полном рюкзаке"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#2_2","level":3,"title":"2.   Реализация кода","text":"PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change_ii.py
        def coin_change_ii_dp(coins: list[int], amt: int) -> int:\n    \"\"\"Размен монет II: динамическое программирование\"\"\"\n    n = len(coins)\n    # Инициализация таблицы dp\n    dp = [[0] * (amt + 1) for _ in range(n + 1)]\n    # Инициализация первого столбца\n    for i in range(n + 1):\n        dp[i][0] = 1\n    # Переход состояний\n    for i in range(1, n + 1):\n        for a in range(1, amt + 1):\n            if coins[i - 1] > a:\n                # Если целевая сумма превышена, монету i не выбирать\n                dp[i][a] = dp[i - 1][a]\n            else:\n                # Сумма двух решений: не брать или взять монету i\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]\n    return dp[n][amt]\n
        coin_change_ii.cpp
        /* Размен монет II: динамическое программирование */\nint coinChangeIIDP(vector<int> &coins, int amt) {\n    int n = coins.size();\n    // Инициализация таблицы dp\n    vector<vector<int>> dp(n + 1, vector<int>(amt + 1, 0));\n    // Инициализация первого столбца\n    for (int i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Сумма двух решений: не брать или взять монету i\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[n][amt];\n}\n
        coin_change_ii.java
        /* Размен монет II: динамическое программирование */\nint coinChangeIIDP(int[] coins, int amt) {\n    int n = coins.length;\n    // Инициализация таблицы dp\n    int[][] dp = new int[n + 1][amt + 1];\n    // Инициализация первого столбца\n    for (int i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Сумма двух решений: не брать или взять монету i\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[n][amt];\n}\n
        coin_change_ii.cs
        /* Размен монет II: динамическое программирование */\nint CoinChangeIIDP(int[] coins, int amt) {\n    int n = coins.Length;\n    // Инициализация таблицы dp\n    int[,] dp = new int[n + 1, amt + 1];\n    // Инициализация первого столбца\n    for (int i = 0; i <= n; i++) {\n        dp[i, 0] = 1;\n    }\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[i, a] = dp[i - 1, a];\n            } else {\n                // Сумма двух решений: не брать или взять монету i\n                dp[i, a] = dp[i - 1, a] + dp[i, a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[n, amt];\n}\n
        coin_change_ii.go
        /* Размен монет II: динамическое программирование */\nfunc coinChangeIIDP(coins []int, amt int) int {\n    n := len(coins)\n    // Инициализация таблицы dp\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, amt+1)\n    }\n    // Инициализация первого столбца\n    for i := 0; i <= n; i++ {\n        dp[i][0] = 1\n    }\n    // Переход состояний: остальные строки и столбцы\n    for i := 1; i <= n; i++ {\n        for a := 1; a <= amt; a++ {\n            if coins[i-1] > a {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[i][a] = dp[i-1][a]\n            } else {\n                // Сумма двух решений: не брать или взять монету i\n                dp[i][a] = dp[i-1][a] + dp[i][a-coins[i-1]]\n            }\n        }\n    }\n    return dp[n][amt]\n}\n
        coin_change_ii.swift
        /* Размен монет II: динамическое программирование */\nfunc coinChangeIIDP(coins: [Int], amt: Int) -> Int {\n    let n = coins.count\n    // Инициализация таблицы dp\n    var dp = Array(repeating: Array(repeating: 0, count: amt + 1), count: n + 1)\n    // Инициализация первого столбца\n    for i in 0 ... n {\n        dp[i][0] = 1\n    }\n    // Переход состояний\n    for i in 1 ... n {\n        for a in 1 ... amt {\n            if coins[i - 1] > a {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[i][a] = dp[i - 1][a]\n            } else {\n                // Сумма двух решений: не брать или взять монету i\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]\n            }\n        }\n    }\n    return dp[n][amt]\n}\n
        coin_change_ii.js
        /* Размен монет II: динамическое программирование */\nfunction coinChangeIIDP(coins, amt) {\n    const n = coins.length;\n    // Инициализация таблицы dp\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: amt + 1 }, () => 0)\n    );\n    // Инициализация первого столбца\n    for (let i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // Переход состояний\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Сумма двух решений: не брать или взять монету i\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[n][amt];\n}\n
        coin_change_ii.ts
        /* Размен монет II: динамическое программирование */\nfunction coinChangeIIDP(coins: Array<number>, amt: number): number {\n    const n = coins.length;\n    // Инициализация таблицы dp\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: amt + 1 }, () => 0)\n    );\n    // Инициализация первого столбца\n    for (let i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // Переход состояний\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Сумма двух решений: не брать или взять монету i\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[n][amt];\n}\n
        coin_change_ii.dart
        /* Размен монет II: динамическое программирование */\nint coinChangeIIDP(List<int> coins, int amt) {\n  int n = coins.length;\n  // Инициализация таблицы dp\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(amt + 1, 0));\n  // Инициализация первого столбца\n  for (int i = 0; i <= n; i++) {\n    dp[i][0] = 1;\n  }\n  // Переход состояний\n  for (int i = 1; i <= n; i++) {\n    for (int a = 1; a <= amt; a++) {\n      if (coins[i - 1] > a) {\n        // Если целевая сумма превышена, монету i не выбирать\n        dp[i][a] = dp[i - 1][a];\n      } else {\n        // Сумма двух решений: не брать или взять монету i\n        dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n      }\n    }\n  }\n  return dp[n][amt];\n}\n
        coin_change_ii.rs
        /* Размен монет II: динамическое программирование */\nfn coin_change_ii_dp(coins: &[i32], amt: usize) -> i32 {\n    let n = coins.len();\n    // Инициализация таблицы dp\n    let mut dp = vec![vec![0; amt + 1]; n + 1];\n    // Инициализация первого столбца\n    for i in 0..=n {\n        dp[i][0] = 1;\n    }\n    // Переход состояний\n    for i in 1..=n {\n        for a in 1..=amt {\n            if coins[i - 1] > a as i32 {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Сумма двух решений: не брать или взять монету i\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1] as usize];\n            }\n        }\n    }\n    dp[n][amt]\n}\n
        coin_change_ii.c
        /* Размен монет II: динамическое программирование */\nint coinChangeIIDP(int coins[], int amt, int coinsSize) {\n    int n = coinsSize;\n    // Инициализация таблицы dp\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(amt + 1, sizeof(int));\n    }\n    // Инициализация первого столбца\n    for (int i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Сумма двух решений: не брать или взять монету i\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n            }\n        }\n    }\n    int res = dp[n][amt];\n    // Освободить память\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    free(dp);\n    return res;\n}\n
        coin_change_ii.kt
        /* Размен монет II: динамическое программирование */\nfun coinChangeIIDP(coins: IntArray, amt: Int): Int {\n    val n = coins.size\n    // Инициализация таблицы dp\n    val dp = Array(n + 1) { IntArray(amt + 1) }\n    // Инициализация первого столбца\n    for (i in 0..n) {\n        dp[i][0] = 1\n    }\n    // Переход состояний\n    for (i in 1..n) {\n        for (a in 1..amt) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[i][a] = dp[i - 1][a]\n            } else {\n                // Сумма двух решений: не брать или взять монету i\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]\n            }\n        }\n    }\n    return dp[n][amt]\n}\n
        coin_change_ii.rb
        ### Размен монет II: динамическое программирование ###\ndef coin_change_ii_dp(coins, amt)\n  n = coins.length\n  # Инициализация таблицы dp\n  dp = Array.new(n + 1) { Array.new(amt + 1, 0) }\n  # Инициализация первого столбца\n  (0...(n + 1)).each { |i| dp[i][0] = 1 }\n  # Переход состояний\n  for i in 1...(n + 1)\n    for a in 1...(amt + 1)\n      if coins[i - 1] > a\n        # Если целевая сумма превышена, монету i не выбирать\n        dp[i][a] = dp[i - 1][a]\n      else\n        # Сумма двух решений: не брать или взять монету i\n        dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]\n      end\n    end\n  end\n  dp[n][amt]\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 14. Динамическое программирование","14.5   Задача о полном рюкзаке"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#3_2","level":3,"title":"3.   Оптимизация пространства","text":"

        При оптимизации памяти способ остается тем же самым: достаточно убрать измерение, отвечающее за виды монет:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change_ii.py
        def coin_change_ii_dp_comp(coins: list[int], amt: int) -> int:\n    \"\"\"Размен монет II: динамическое программирование с оптимизацией памяти\"\"\"\n    n = len(coins)\n    # Инициализация таблицы dp\n    dp = [0] * (amt + 1)\n    dp[0] = 1\n    # Переход состояний\n    for i in range(1, n + 1):\n        # Прямой обход\n        for a in range(1, amt + 1):\n            if coins[i - 1] > a:\n                # Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a]\n            else:\n                # Сумма двух решений: не брать или взять монету i\n                dp[a] = dp[a] + dp[a - coins[i - 1]]\n    return dp[amt]\n
        coin_change_ii.cpp
        /* Размен монет II: динамическое программирование с оптимизацией памяти */\nint coinChangeIIDPComp(vector<int> &coins, int amt) {\n    int n = coins.size();\n    // Инициализация таблицы dp\n    vector<int> dp(amt + 1, 0);\n    dp[0] = 1;\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a];\n            } else {\n                // Сумма двух решений: не брать или взять монету i\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
        coin_change_ii.java
        /* Размен монет II: динамическое программирование с оптимизацией памяти */\nint coinChangeIIDPComp(int[] coins, int amt) {\n    int n = coins.length;\n    // Инициализация таблицы dp\n    int[] dp = new int[amt + 1];\n    dp[0] = 1;\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a];\n            } else {\n                // Сумма двух решений: не брать или взять монету i\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
        coin_change_ii.cs
        /* Размен монет II: динамическое программирование с оптимизацией памяти */\nint CoinChangeIIDPComp(int[] coins, int amt) {\n    int n = coins.Length;\n    // Инициализация таблицы dp\n    int[] dp = new int[amt + 1];\n    dp[0] = 1;\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a];\n            } else {\n                // Сумма двух решений: не брать или взять монету i\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
        coin_change_ii.go
        /* Размен монет II: динамическое программирование с оптимизацией памяти */\nfunc coinChangeIIDPComp(coins []int, amt int) int {\n    n := len(coins)\n    // Инициализация таблицы dp\n    dp := make([]int, amt+1)\n    dp[0] = 1\n    // Переход состояний\n    for i := 1; i <= n; i++ {\n        // Прямой обход\n        for a := 1; a <= amt; a++ {\n            if coins[i-1] > a {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a]\n            } else {\n                // Сумма двух решений: не брать или взять монету i\n                dp[a] = dp[a] + dp[a-coins[i-1]]\n            }\n        }\n    }\n    return dp[amt]\n}\n
        coin_change_ii.swift
        /* Размен монет II: динамическое программирование с оптимизацией памяти */\nfunc coinChangeIIDPComp(coins: [Int], amt: Int) -> Int {\n    let n = coins.count\n    // Инициализация таблицы dp\n    var dp = Array(repeating: 0, count: amt + 1)\n    dp[0] = 1\n    // Переход состояний\n    for i in 1 ... n {\n        for a in 1 ... amt {\n            if coins[i - 1] > a {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a]\n            } else {\n                // Сумма двух решений: не брать или взять монету i\n                dp[a] = dp[a] + dp[a - coins[i - 1]]\n            }\n        }\n    }\n    return dp[amt]\n}\n
        coin_change_ii.js
        /* Размен монет II: динамическое программирование с оптимизацией памяти */\nfunction coinChangeIIDPComp(coins, amt) {\n    const n = coins.length;\n    // Инициализация таблицы dp\n    const dp = Array.from({ length: amt + 1 }, () => 0);\n    dp[0] = 1;\n    // Переход состояний\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a];\n            } else {\n                // Сумма двух решений: не брать или взять монету i\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
        coin_change_ii.ts
        /* Размен монет II: динамическое программирование с оптимизацией памяти */\nfunction coinChangeIIDPComp(coins: Array<number>, amt: number): number {\n    const n = coins.length;\n    // Инициализация таблицы dp\n    const dp = Array.from({ length: amt + 1 }, () => 0);\n    dp[0] = 1;\n    // Переход состояний\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a];\n            } else {\n                // Сумма двух решений: не брать или взять монету i\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
        coin_change_ii.dart
        /* Размен монет II: динамическое программирование с оптимизацией памяти */\nint coinChangeIIDPComp(List<int> coins, int amt) {\n  int n = coins.length;\n  // Инициализация таблицы dp\n  List<int> dp = List.filled(amt + 1, 0);\n  dp[0] = 1;\n  // Переход состояний\n  for (int i = 1; i <= n; i++) {\n    for (int a = 1; a <= amt; a++) {\n      if (coins[i - 1] > a) {\n        // Если целевая сумма превышена, монету i не выбирать\n        dp[a] = dp[a];\n      } else {\n        // Сумма двух решений: не брать или взять монету i\n        dp[a] = dp[a] + dp[a - coins[i - 1]];\n      }\n    }\n  }\n  return dp[amt];\n}\n
        coin_change_ii.rs
        /* Размен монет II: динамическое программирование с оптимизацией памяти */\nfn coin_change_ii_dp_comp(coins: &[i32], amt: usize) -> i32 {\n    let n = coins.len();\n    // Инициализация таблицы dp\n    let mut dp = vec![0; amt + 1];\n    dp[0] = 1;\n    // Переход состояний\n    for i in 1..=n {\n        for a in 1..=amt {\n            if coins[i - 1] > a as i32 {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a];\n            } else {\n                // Сумма двух решений: не брать или взять монету i\n                dp[a] = dp[a] + dp[a - coins[i - 1] as usize];\n            }\n        }\n    }\n    dp[amt]\n}\n
        coin_change_ii.c
        /* Размен монет II: динамическое программирование с оптимизацией памяти */\nint coinChangeIIDPComp(int coins[], int amt, int coinsSize) {\n    int n = coinsSize;\n    // Инициализация таблицы dp\n    int *dp = calloc(amt + 1, sizeof(int));\n    dp[0] = 1;\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a];\n            } else {\n                // Сумма двух решений: не брать или взять монету i\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    int res = dp[amt];\n    // Освободить память\n    free(dp);\n    return res;\n}\n
        coin_change_ii.kt
        /* Размен монет II: динамическое программирование с оптимизацией памяти */\nfun coinChangeIIDPComp(coins: IntArray, amt: Int): Int {\n    val n = coins.size\n    // Инициализация таблицы dp\n    val dp = IntArray(amt + 1)\n    dp[0] = 1\n    // Переход состояний\n    for (i in 1..n) {\n        for (a in 1..amt) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a]\n            } else {\n                // Сумма двух решений: не брать или взять монету i\n                dp[a] = dp[a] + dp[a - coins[i - 1]]\n            }\n        }\n    }\n    return dp[amt]\n}\n
        coin_change_ii.rb
        ### Размен монет II: динамическое программирование с оптимизацией памяти ###\ndef coin_change_ii_dp_comp(coins, amt)\n  n = coins.length\n  # Инициализация таблицы dp\n  dp = Array.new(amt + 1, 0)\n  dp[0] = 1\n  # Переход состояний\n  for i in 1...(n + 1)\n    # Прямой обход\n    for a in 1...(amt + 1)\n      if coins[i - 1] > a\n        # Если целевая сумма превышена, монету i не выбирать\n        dp[a] = dp[a]\n      else\n        # Сумма двух решений: не брать или взять монету i\n        dp[a] = dp[a] + dp[a - coins[i - 1]]\n      end\n    end\n  end\n  dp[amt]\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 14. Динамическое программирование","14.5   Задача о полном рюкзаке"],"tags":[]},{"location":"chapter_graph/","level":1,"title":"Глава 9.   Графы","text":"

        Abstract

        В жизни мы похожи на вершины, соединенные множеством невидимых ребер.

        Каждая встреча и каждое расставание оставляют в этой огромной сети свой след.

        ","path":["Глава 9. Графы","Глава 9.   Графы"],"tags":[]},{"location":"chapter_graph/#_1","level":2,"title":"Содержание главы","text":"
        • 9.1   Граф
        • 9.2   Базовые операции графа
        • 9.3   Обход графа
        • 9.4   Краткие итоги
        ","path":["Глава 9. Графы","Глава 9.   Графы"],"tags":[]},{"location":"chapter_graph/graph/","level":1,"title":"9.1   Граф","text":"

        Граф (graph) - это нелинейная структура данных, состоящая из вершин (vertex) и ребер (edge). Граф \\(G\\) можно абстрактно представить как множество вершин \\(V\\) и множество ребер \\(E\\) . Ниже приведен пример графа, содержащего 5 вершин и 7 ребер.

        \\[ \\begin{aligned} V & = \\{ 1, 2, 3, 4, 5 \\} \\newline E & = \\{ (1,2), (1,3), (1,5), (2,3), (2,4), (2,5), (4,5) \\} \\newline G & = \\{ V, E \\} \\newline \\end{aligned} \\]

        Если рассматривать вершины как узлы, а ребра как ссылки, соединяющие узлы, граф можно считать структурой данных, расширяющей связный список. Как показано на рисунке 9-1, по сравнению с линейными отношениями (связный список) и отношениями разделения (дерево), сетевые отношения (граф) обладают большей свободой и потому являются более сложными.

        Рисунок 9-1   Связь между связным списком, деревом и графом

        ","path":["Глава 9. Графы","9.1   Граф"],"tags":[]},{"location":"chapter_graph/graph/#911","level":2,"title":"9.1.1   Распространенные типы и термины графов","text":"

        В зависимости от наличия направления у ребер графы делятся на неориентированные графы (undirected graph) и ориентированные графы (directed graph) , как показано на рисунке 9-2.

        • В неориентированном графе ребро представляет двустороннюю связь между двумя вершинами, например дружеские отношения в социальных сетях.
        • В ориентированном графе ребро имеет направление, то есть ребра \\(A \\rightarrow B\\) и \\(A \\leftarrow B\\) независимы друг от друга, например отношения подписки и подписчиков.

        Рисунок 9-2   Ориентированный и неориентированный графы

        В зависимости от того, связаны ли все вершины между собой, граф делится на связный граф (connected graph) и несвязный граф (disconnected graph) , как показано на рисунке 9-3.

        • В связном графе из любой вершины можно достичь любой другой вершины.
        • В несвязном графе существует по крайней мере одна вершина, недостижимая из текущей.

        Рисунок 9-3   Связный и несвязный графы

        Мы также можем добавить к ребрам переменную \"вес\" и получить показанный ниже взвешенный граф (weighted graph). Например, в мобильных играх вроде Honor of Kings система рассчитывает \"близость\" между игроками по времени совместной игры, и такую сеть близости можно представить взвешенным графом.

        Рисунок 9-4   Взвешенный и невзвешенный графы

        Со структурой данных \"граф\" связаны следующие основные термины.

        • Смежность (adjacency): если между двумя вершинами существует ребро, то такие вершины называются смежными. На рисунке 9-4 с вершиной 1 смежны вершины 2, 3 и 5.
        • Путь (path): последовательность ребер от вершины A до вершины B называется путем из A в B. На рисунке 9-4 последовательность ребер 1-5-2-4 является одним из путей от вершины 1 к вершине 4.
        • Степень (degree): количество ребер, принадлежащих вершине. Для ориентированного графа входящая степень (in-degree) показывает, сколько ребер входит в вершину, а исходящая степень (out-degree) показывает, сколько ребер из нее выходит.
        ","path":["Глава 9. Графы","9.1   Граф"],"tags":[]},{"location":"chapter_graph/graph/#912","level":2,"title":"9.1.2   Представление графа","text":"

        Распространенные способы представления графа включают \"матрицу смежности\" и \"список смежности\". Ниже для примера рассматривается неориентированный граф.

        ","path":["Глава 9. Графы","9.1   Граф"],"tags":[]},{"location":"chapter_graph/graph/#1","level":3,"title":"1.   Матрица смежности","text":"

        Пусть число вершин графа равно \\(n\\) ; тогда матрица смежности (adjacency matrix) использует матрицу размера \\(n \\times n\\) для представления графа, где каждая строка и каждый столбец соответствуют вершине, а элементы матрицы показывают наличие или отсутствие ребра.

        Как показано на рисунке 9-5, обозначим матрицу смежности через \\(M\\) , а список вершин через \\(V\\) ; тогда элемент матрицы \\(M[i, j] = 1\\) означает наличие ребра между вершинами \\(V[i]\\) и \\(V[j]\\) , а элемент \\(M[i, j] = 0\\) означает отсутствие ребра.

        Рисунок 9-5   Представление графа матрицей смежности

        Матрица смежности обладает следующими особенностями.

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

        При представлении графа матрицей смежности можно напрямую обращаться к элементам матрицы и получать сведения о ребрах, поэтому операции добавления, удаления, поиска и изменения обладают высокой эффективностью и выполняются за \\(O(1)\\) . Однако пространственная сложность матрицы составляет \\(O(n^2)\\) , поэтому она требует значительных затрат памяти.

        ","path":["Глава 9. Графы","9.1   Граф"],"tags":[]},{"location":"chapter_graph/graph/#2","level":3,"title":"2.   Список смежности","text":"

        Список смежности (adjacency list) использует \\(n\\) списков для представления графа, где узлы списка обозначают вершины. \\(i\\)-й список соответствует вершине \\(i\\) и хранит все смежные с ней вершины, то есть все вершины, соединенные с данной вершиной. На рисунке 9-6 показан пример графа, представленного списком смежности.

        Рисунок 9-6   Представление графа списком смежности

        Список смежности хранит только реально существующие ребра, а общее число ребер обычно значительно меньше \\(n^2\\) , поэтому он лучше экономит память. Однако для поиска ребра в списке смежности требуется обходить список, поэтому по времени он уступает матрице смежности.

        Если посмотреть на рисунок 9-6, можно заметить, что структура списка смежности очень похожа на цепную адресацию в хеш-таблицах, поэтому здесь можно использовать похожие методы оптимизации эффективности. Например, если список слишком длинный, его можно преобразовать в AVL-дерево или красно-черное дерево, чтобы снизить временную сложность с \\(O(n)\\) до \\(O(\\log n)\\) ; также список можно преобразовать в хеш-таблицу, чтобы довести временную сложность до \\(O(1)\\) .

        ","path":["Глава 9. Графы","9.1   Граф"],"tags":[]},{"location":"chapter_graph/graph/#913","level":2,"title":"9.1.3   Типичные применения графов","text":"

        Как показано в таблице 9-1, многие реальные системы можно моделировать с помощью графов, а соответствующие задачи затем сводить к задачам вычислений на графах.

        Таблица 9-1   Распространенные графы в реальной жизни

        Вершина Ребро Задача вычислений на графе Социальные сети Пользователь Дружеская связь Рекомендация потенциальных друзей Линии метро Станция Связь между станциями Рекомендация кратчайшего маршрута Солнечная система Небесное тело Гравитационное взаимодействие между телами Вычисление орбит планет","path":["Глава 9. Графы","9.1   Граф"],"tags":[]},{"location":"chapter_graph/graph_operations/","level":1,"title":"9.2   Базовые операции графа","text":"

        Базовые операции графа можно разделить на операции над \"ребрами\" и операции над \"вершинами\". При двух способах представления, \"матрице смежности\" и \"списке смежности\", реализация этих операций различается.

        ","path":["Глава 9. Графы","9.2   Базовые операции графа"],"tags":[]},{"location":"chapter_graph/graph_operations/#921","level":2,"title":"9.2.1   Реализация на основе матрицы смежности","text":"

        Пусть дан неориентированный граф с числом вершин \\(n\\) . Тогда способы реализации различных операций показаны на рисунках ниже.

        • Добавление или удаление ребра: достаточно изменить соответствующее ребро в матрице смежности, что требует \\(O(1)\\) времени. Поскольку граф неориентированный, необходимо одновременно обновить ребра в обоих направлениях.
        • Добавление вершины: в конец матрицы смежности добавляется строка и столбец, полностью заполненные нулями; это требует \\(O(n)\\) времени.
        • Удаление вершины: из матрицы смежности удаляется строка и столбец. При удалении первой строки и первого столбца достигается худший случай, когда требуется \"сдвинуть влево вверх\" \\((n-1)^2\\) элементов, поэтому используется \\(O(n^2)\\) времени.
        • Инициализация: передаются \\(n\\) вершин, затем инициализируется список вершин vertices длины \\(n\\) , что требует \\(O(n)\\) времени; после этого инициализируется матрица смежности adjMat размера \\(n \\times n\\) , что требует \\(O(n^2)\\) времени.
        <1><2><3><4><5>

        Рисунок 9-7   Инициализация матрицы смежности, добавление и удаление ребер и вершин

        Ниже приведен код реализации графа на основе матрицы смежности:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_adjacency_matrix.py
        class GraphAdjMat:\n    \"\"\"Класс неориентированного графа на основе матрицы смежности\"\"\"\n\n    def __init__(self, vertices: list[int], edges: list[list[int]]):\n        \"\"\"Конструктор\"\"\"\n        # Список вершин: элементы представляют «значения вершин», а индексы — «индексы вершин»\n        self.vertices: list[int] = []\n        # Матрица смежности, где индексы строк и столбцов соответствуют «индексам вершин»\n        self.adj_mat: list[list[int]] = []\n        # Добавление вершины\n        for val in vertices:\n            self.add_vertex(val)\n        # Добавить ребра\n        # Обратите внимание: элементы edges представляют собой индексы вершин, то есть соответствуют индексам элементов vertices\n        for e in edges:\n            self.add_edge(e[0], e[1])\n\n    def size(self) -> int:\n        \"\"\"Получить число вершин\"\"\"\n        return len(self.vertices)\n\n    def add_vertex(self, val: int):\n        \"\"\"Добавление вершины\"\"\"\n        n = self.size()\n        # Добавить значение новой вершины в список вершин\n        self.vertices.append(val)\n        # Добавить строку в матрицу смежности\n        new_row = [0] * n\n        self.adj_mat.append(new_row)\n        # Добавить столбец в матрицу смежности\n        for row in self.adj_mat:\n            row.append(0)\n\n    def remove_vertex(self, index: int):\n        \"\"\"Удаление вершины\"\"\"\n        if index >= self.size():\n            raise IndexError()\n        # Удалить вершину с индексом index из списка вершин\n        self.vertices.pop(index)\n        # Удалить строку с индексом index из матрицы смежности\n        self.adj_mat.pop(index)\n        # Удалить столбец с индексом index из матрицы смежности\n        for row in self.adj_mat:\n            row.pop(index)\n\n    def add_edge(self, i: int, j: int):\n        \"\"\"Добавление ребра\"\"\"\n        # Параметры i и j соответствуют индексам элементов vertices\n        # Обработка выхода индекса за границы и случая равенства\n        if i < 0 or j < 0 or i >= self.size() or j >= self.size() or i == j:\n            raise IndexError()\n        # В неориентированном графе матрица смежности симметрична относительно главной диагонали, то есть выполняется (i, j) == (j, i)\n        self.adj_mat[i][j] = 1\n        self.adj_mat[j][i] = 1\n\n    def remove_edge(self, i: int, j: int):\n        \"\"\"Удаление ребра\"\"\"\n        # Параметры i и j соответствуют индексам элементов vertices\n        # Обработка выхода индекса за границы и случая равенства\n        if i < 0 or j < 0 or i >= self.size() or j >= self.size() or i == j:\n            raise IndexError()\n        self.adj_mat[i][j] = 0\n        self.adj_mat[j][i] = 0\n\n    def print(self):\n        \"\"\"Вывести матрицу смежности\"\"\"\n        print(\"Список вершин =\", self.vertices)\n        print(\"Матрица смежности =\")\n        print_matrix(self.adj_mat)\n
        graph_adjacency_matrix.cpp
        /* Класс неориентированного графа на основе матрицы смежности */\nclass GraphAdjMat {\n    vector<int> vertices;       // Список вершин: элементы представляют «значения вершин», а индексы — «индексы вершин»\n    vector<vector<int>> adjMat; // Матрица смежности, где индексы строк и столбцов соответствуют «индексам вершин»\n\n  public:\n    /* Конструктор */\n    GraphAdjMat(const vector<int> &vertices, const vector<vector<int>> &edges) {\n        // Добавление вершины\n        for (int val : vertices) {\n            addVertex(val);\n        }\n        // Добавить ребра\n        // Обратите внимание: элементы edges представляют собой индексы вершин, то есть соответствуют индексам элементов vertices\n        for (const vector<int> &edge : edges) {\n            addEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* Получить число вершин */\n    int size() const {\n        return vertices.size();\n    }\n\n    /* Добавление вершины */\n    void addVertex(int val) {\n        int n = size();\n        // Добавить значение новой вершины в список вершин\n        vertices.push_back(val);\n        // Добавить строку в матрицу смежности\n        adjMat.emplace_back(vector<int>(n, 0));\n        // Добавить столбец в матрицу смежности\n        for (vector<int> &row : adjMat) {\n            row.push_back(0);\n        }\n    }\n\n    /* Удаление вершины */\n    void removeVertex(int index) {\n        if (index >= size()) {\n            throw out_of_range(\"вершина не существует\");\n        }\n        // Удалить вершину с индексом index из списка вершин\n        vertices.erase(vertices.begin() + index);\n        // Удалить строку с индексом index из матрицы смежности\n        adjMat.erase(adjMat.begin() + index);\n        // Удалить столбец с индексом index из матрицы смежности\n        for (vector<int> &row : adjMat) {\n            row.erase(row.begin() + index);\n        }\n    }\n\n    /* Добавление ребра */\n    // Параметры i и j соответствуют индексам элементов vertices\n    void addEdge(int i, int j) {\n        // Обработка выхода индекса за границы и случая равенства\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {\n            throw out_of_range(\"вершина не существует\");\n        }\n        // В неориентированном графе матрица смежности симметрична относительно главной диагонали, то есть выполняется (i, j) == (j, i)\n        adjMat[i][j] = 1;\n        adjMat[j][i] = 1;\n    }\n\n    /* Удаление ребра */\n    // Параметры i и j соответствуют индексам элементов vertices\n    void removeEdge(int i, int j) {\n        // Обработка выхода индекса за границы и случая равенства\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {\n            throw out_of_range(\"вершина не существует\");\n        }\n        adjMat[i][j] = 0;\n        adjMat[j][i] = 0;\n    }\n\n    /* Вывести матрицу смежности */\n    void print() {\n        cout << \"Список вершин = \";\n        printVector(vertices);\n        cout << \"Матрица смежности =\" << endl;\n        printVectorMatrix(adjMat);\n    }\n};\n
        graph_adjacency_matrix.java
        /* Класс неориентированного графа на основе матрицы смежности */\nclass GraphAdjMat {\n    List<Integer> vertices; // Список вершин: элементы представляют «значения вершин», а индексы — «индексы вершин»\n    List<List<Integer>> adjMat; // Матрица смежности, где индексы строк и столбцов соответствуют «индексам вершин»\n\n    /* Конструктор */\n    public GraphAdjMat(int[] vertices, int[][] edges) {\n        this.vertices = new ArrayList<>();\n        this.adjMat = new ArrayList<>();\n        // Добавление вершины\n        for (int val : vertices) {\n            addVertex(val);\n        }\n        // Добавить ребра\n        // Обратите внимание: элементы edges представляют собой индексы вершин, то есть соответствуют индексам элементов vertices\n        for (int[] e : edges) {\n            addEdge(e[0], e[1]);\n        }\n    }\n\n    /* Получить число вершин */\n    public int size() {\n        return vertices.size();\n    }\n\n    /* Добавление вершины */\n    public void addVertex(int val) {\n        int n = size();\n        // Добавить значение новой вершины в список вершин\n        vertices.add(val);\n        // Добавить строку в матрицу смежности\n        List<Integer> newRow = new ArrayList<>(n);\n        for (int j = 0; j < n; j++) {\n            newRow.add(0);\n        }\n        adjMat.add(newRow);\n        // Добавить столбец в матрицу смежности\n        for (List<Integer> row : adjMat) {\n            row.add(0);\n        }\n    }\n\n    /* Удаление вершины */\n    public void removeVertex(int index) {\n        if (index >= size())\n            throw new IndexOutOfBoundsException();\n        // Удалить вершину с индексом index из списка вершин\n        vertices.remove(index);\n        // Удалить строку с индексом index из матрицы смежности\n        adjMat.remove(index);\n        // Удалить столбец с индексом index из матрицы смежности\n        for (List<Integer> row : adjMat) {\n            row.remove(index);\n        }\n    }\n\n    /* Добавление ребра */\n    // Параметры i и j соответствуют индексам элементов vertices\n    public void addEdge(int i, int j) {\n        // Обработка выхода индекса за границы и случая равенства\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j)\n            throw new IndexOutOfBoundsException();\n        // В неориентированном графе матрица смежности симметрична относительно главной диагонали, то есть выполняется (i, j) == (j, i)\n        adjMat.get(i).set(j, 1);\n        adjMat.get(j).set(i, 1);\n    }\n\n    /* Удаление ребра */\n    // Параметры i и j соответствуют индексам элементов vertices\n    public void removeEdge(int i, int j) {\n        // Обработка выхода индекса за границы и случая равенства\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j)\n            throw new IndexOutOfBoundsException();\n        adjMat.get(i).set(j, 0);\n        adjMat.get(j).set(i, 0);\n    }\n\n    /* Вывести матрицу смежности */\n    public void print() {\n        System.out.print(\"Список вершин = \");\n        System.out.println(vertices);\n        System.out.println(\"Матрица смежности =\");\n        PrintUtil.printMatrix(adjMat);\n    }\n}\n
        graph_adjacency_matrix.cs
        /* Класс неориентированного графа на основе матрицы смежности */\nclass GraphAdjMat {\n    List<int> vertices;     // Список вершин: элементы представляют «значения вершин», а индексы — «индексы вершин»\n    List<List<int>> adjMat; // Матрица смежности, где индексы строк и столбцов соответствуют «индексам вершин»\n\n    /* Конструктор */\n    public GraphAdjMat(int[] vertices, int[][] edges) {\n        this.vertices = [];\n        this.adjMat = [];\n        // Добавление вершины\n        foreach (int val in vertices) {\n            AddVertex(val);\n        }\n        // Добавить ребра\n        // Обратите внимание: элементы edges представляют собой индексы вершин, то есть соответствуют индексам элементов vertices\n        foreach (int[] e in edges) {\n            AddEdge(e[0], e[1]);\n        }\n    }\n\n    /* Получить число вершин */\n    int Size() {\n        return vertices.Count;\n    }\n\n    /* Добавление вершины */\n    public void AddVertex(int val) {\n        int n = Size();\n        // Добавить значение новой вершины в список вершин\n        vertices.Add(val);\n        // Добавить строку в матрицу смежности\n        List<int> newRow = new(n);\n        for (int j = 0; j < n; j++) {\n            newRow.Add(0);\n        }\n        adjMat.Add(newRow);\n        // Добавить столбец в матрицу смежности\n        foreach (List<int> row in adjMat) {\n            row.Add(0);\n        }\n    }\n\n    /* Удаление вершины */\n    public void RemoveVertex(int index) {\n        if (index >= Size())\n            throw new IndexOutOfRangeException();\n        // Удалить вершину с индексом index из списка вершин\n        vertices.RemoveAt(index);\n        // Удалить строку с индексом index из матрицы смежности\n        adjMat.RemoveAt(index);\n        // Удалить столбец с индексом index из матрицы смежности\n        foreach (List<int> row in adjMat) {\n            row.RemoveAt(index);\n        }\n    }\n\n    /* Добавление ребра */\n    // Параметры i и j соответствуют индексам элементов vertices\n    public void AddEdge(int i, int j) {\n        // Обработка выхода индекса за границы и случая равенства\n        if (i < 0 || j < 0 || i >= Size() || j >= Size() || i == j)\n            throw new IndexOutOfRangeException();\n        // В неориентированном графе матрица смежности симметрична относительно главной диагонали, то есть выполняется (i, j) == (j, i)\n        adjMat[i][j] = 1;\n        adjMat[j][i] = 1;\n    }\n\n    /* Удаление ребра */\n    // Параметры i и j соответствуют индексам элементов vertices\n    public void RemoveEdge(int i, int j) {\n        // Обработка выхода индекса за границы и случая равенства\n        if (i < 0 || j < 0 || i >= Size() || j >= Size() || i == j)\n            throw new IndexOutOfRangeException();\n        adjMat[i][j] = 0;\n        adjMat[j][i] = 0;\n    }\n\n    /* Вывести матрицу смежности */\n    public void Print() {\n        Console.Write(\"Список вершин = \");\n        PrintUtil.PrintList(vertices);\n        Console.WriteLine(\"Матрица смежности =\");\n        PrintUtil.PrintMatrix(adjMat);\n    }\n}\n
        graph_adjacency_matrix.go
        /* Класс неориентированного графа на основе матрицы смежности */\ntype graphAdjMat struct {\n    // Список вершин: элементы представляют «значения вершин», а индексы — «индексы вершин»\n    vertices []int\n    // Матрица смежности, где индексы строк и столбцов соответствуют «индексам вершин»\n    adjMat [][]int\n}\n\n/* Конструктор */\nfunc newGraphAdjMat(vertices []int, edges [][]int) *graphAdjMat {\n    // Добавление вершины\n    n := len(vertices)\n    adjMat := make([][]int, n)\n    for i := range adjMat {\n        adjMat[i] = make([]int, n)\n    }\n    // Инициализировать граф\n    g := &graphAdjMat{\n        vertices: vertices,\n        adjMat:   adjMat,\n    }\n    // Добавить ребра\n    // Обратите внимание: элементы edges представляют собой индексы вершин, то есть соответствуют индексам элементов vertices\n    for i := range edges {\n        g.addEdge(edges[i][0], edges[i][1])\n    }\n    return g\n}\n\n/* Получить число вершин */\nfunc (g *graphAdjMat) size() int {\n    return len(g.vertices)\n}\n\n/* Добавление вершины */\nfunc (g *graphAdjMat) addVertex(val int) {\n    n := g.size()\n    // Добавить значение новой вершины в список вершин\n    g.vertices = append(g.vertices, val)\n    // Добавить строку в матрицу смежности\n    newRow := make([]int, n)\n    g.adjMat = append(g.adjMat, newRow)\n    // Добавить столбец в матрицу смежности\n    for i := range g.adjMat {\n        g.adjMat[i] = append(g.adjMat[i], 0)\n    }\n}\n\n/* Удаление вершины */\nfunc (g *graphAdjMat) removeVertex(index int) {\n    if index >= g.size() {\n        return\n    }\n    // Удалить вершину с индексом index из списка вершин\n    g.vertices = append(g.vertices[:index], g.vertices[index+1:]...)\n    // Удалить строку с индексом index из матрицы смежности\n    g.adjMat = append(g.adjMat[:index], g.adjMat[index+1:]...)\n    // Удалить столбец с индексом index из матрицы смежности\n    for i := range g.adjMat {\n        g.adjMat[i] = append(g.adjMat[i][:index], g.adjMat[i][index+1:]...)\n    }\n}\n\n/* Добавление ребра */\n// Параметры i и j соответствуют индексам элементов vertices\nfunc (g *graphAdjMat) addEdge(i, j int) {\n    // Обработка выхода индекса за границы и случая равенства\n    if i < 0 || j < 0 || i >= g.size() || j >= g.size() || i == j {\n        fmt.Errorf(\"%s\", \"Index Out Of Bounds Exception\")\n    }\n    // В неориентированном графе матрица смежности симметрична относительно главной диагонали, то есть выполняется (i, j) == (j, i)\n    g.adjMat[i][j] = 1\n    g.adjMat[j][i] = 1\n}\n\n/* Удаление ребра */\n// Параметры i и j соответствуют индексам элементов vertices\nfunc (g *graphAdjMat) removeEdge(i, j int) {\n    // Обработка выхода индекса за границы и случая равенства\n    if i < 0 || j < 0 || i >= g.size() || j >= g.size() || i == j {\n        fmt.Errorf(\"%s\", \"Index Out Of Bounds Exception\")\n    }\n    g.adjMat[i][j] = 0\n    g.adjMat[j][i] = 0\n}\n\n/* Вывести матрицу смежности */\nfunc (g *graphAdjMat) print() {\n    fmt.Printf(\"\\tСписок вершин = %v\\n\", g.vertices)\n    fmt.Printf(\"\\tМатрица смежности = \\n\")\n    for i := range g.adjMat {\n        fmt.Printf(\"\\t\\t\\t%v\\n\", g.adjMat[i])\n    }\n}\n
        graph_adjacency_matrix.swift
        /* Класс неориентированного графа на основе матрицы смежности */\nclass GraphAdjMat {\n    private var vertices: [Int] // Список вершин: элементы представляют «значения вершин», а индексы — «индексы вершин»\n    private var adjMat: [[Int]] // Матрица смежности, где индексы строк и столбцов соответствуют «индексам вершин»\n\n    /* Конструктор */\n    init(vertices: [Int], edges: [[Int]]) {\n        self.vertices = []\n        adjMat = []\n        // Добавление вершины\n        for val in vertices {\n            addVertex(val: val)\n        }\n        // Добавить ребра\n        // Обратите внимание: элементы edges представляют собой индексы вершин, то есть соответствуют индексам элементов vertices\n        for e in edges {\n            addEdge(i: e[0], j: e[1])\n        }\n    }\n\n    /* Получить число вершин */\n    func size() -> Int {\n        vertices.count\n    }\n\n    /* Добавление вершины */\n    func addVertex(val: Int) {\n        let n = size()\n        // Добавить значение новой вершины в список вершин\n        vertices.append(val)\n        // Добавить строку в матрицу смежности\n        let newRow = Array(repeating: 0, count: n)\n        adjMat.append(newRow)\n        // Добавить столбец в матрицу смежности\n        for i in adjMat.indices {\n            adjMat[i].append(0)\n        }\n    }\n\n    /* Удаление вершины */\n    func removeVertex(index: Int) {\n        if index >= size() {\n            fatalError(\"Выход за границы диапазона\")\n        }\n        // Удалить вершину с индексом index из списка вершин\n        vertices.remove(at: index)\n        // Удалить строку с индексом index из матрицы смежности\n        adjMat.remove(at: index)\n        // Удалить столбец с индексом index из матрицы смежности\n        for i in adjMat.indices {\n            adjMat[i].remove(at: index)\n        }\n    }\n\n    /* Добавление ребра */\n    // Параметры i и j соответствуют индексам элементов vertices\n    func addEdge(i: Int, j: Int) {\n        // Обработка выхода индекса за границы и случая равенства\n        if i < 0 || j < 0 || i >= size() || j >= size() || i == j {\n            fatalError(\"Выход за границы диапазона\")\n        }\n        // В неориентированном графе матрица смежности симметрична относительно главной диагонали, то есть выполняется (i, j) == (j, i)\n        adjMat[i][j] = 1\n        adjMat[j][i] = 1\n    }\n\n    /* Удаление ребра */\n    // Параметры i и j соответствуют индексам элементов vertices\n    func removeEdge(i: Int, j: Int) {\n        // Обработка выхода индекса за границы и случая равенства\n        if i < 0 || j < 0 || i >= size() || j >= size() || i == j {\n            fatalError(\"Выход за границы диапазона\")\n        }\n        adjMat[i][j] = 0\n        adjMat[j][i] = 0\n    }\n\n    /* Вывести матрицу смежности */\n    func print() {\n        Swift.print(\"Список вершин = \", terminator: \"\")\n        Swift.print(vertices)\n        Swift.print(\"Матрица смежности =\")\n        PrintUtil.printMatrix(matrix: adjMat)\n    }\n}\n
        graph_adjacency_matrix.js
        /* Класс неориентированного графа на основе матрицы смежности */\nclass GraphAdjMat {\n    vertices; // Список вершин: элементы представляют «значения вершин», а индексы — «индексы вершин»\n    adjMat; // Матрица смежности, где индексы строк и столбцов соответствуют «индексам вершин»\n\n    /* Конструктор */\n    constructor(vertices, edges) {\n        this.vertices = [];\n        this.adjMat = [];\n        // Добавление вершины\n        for (const val of vertices) {\n            this.addVertex(val);\n        }\n        // Добавить ребра\n        // Обратите внимание: элементы edges представляют собой индексы вершин, то есть соответствуют индексам элементов vertices\n        for (const e of edges) {\n            this.addEdge(e[0], e[1]);\n        }\n    }\n\n    /* Получить число вершин */\n    size() {\n        return this.vertices.length;\n    }\n\n    /* Добавление вершины */\n    addVertex(val) {\n        const n = this.size();\n        // Добавить значение новой вершины в список вершин\n        this.vertices.push(val);\n        // Добавить строку в матрицу смежности\n        const newRow = [];\n        for (let j = 0; j < n; j++) {\n            newRow.push(0);\n        }\n        this.adjMat.push(newRow);\n        // Добавить столбец в матрицу смежности\n        for (const row of this.adjMat) {\n            row.push(0);\n        }\n    }\n\n    /* Удаление вершины */\n    removeVertex(index) {\n        if (index >= this.size()) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        // Удалить вершину с индексом index из списка вершин\n        this.vertices.splice(index, 1);\n\n        // Удалить строку с индексом index из матрицы смежности\n        this.adjMat.splice(index, 1);\n        // Удалить столбец с индексом index из матрицы смежности\n        for (const row of this.adjMat) {\n            row.splice(index, 1);\n        }\n    }\n\n    /* Добавление ребра */\n    // Параметры i и j соответствуют индексам элементов vertices\n    addEdge(i, j) {\n        // Обработка выхода индекса за границы и случая равенства\n        if (i < 0 || j < 0 || i >= this.size() || j >= this.size() || i === j) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        // В неориентированном графе матрица смежности симметрична относительно главной диагонали, то есть выполняется (i, j) === (j, i)\n        this.adjMat[i][j] = 1;\n        this.adjMat[j][i] = 1;\n    }\n\n    /* Удаление ребра */\n    // Параметры i и j соответствуют индексам элементов vertices\n    removeEdge(i, j) {\n        // Обработка выхода индекса за границы и случая равенства\n        if (i < 0 || j < 0 || i >= this.size() || j >= this.size() || i === j) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        this.adjMat[i][j] = 0;\n        this.adjMat[j][i] = 0;\n    }\n\n    /* Вывести матрицу смежности */\n    print() {\n        console.log('Список вершин = ', this.vertices);\n        console.log('Матрица смежности =', this.adjMat);\n    }\n}\n
        graph_adjacency_matrix.ts
        /* Класс неориентированного графа на основе матрицы смежности */\nclass GraphAdjMat {\n    vertices: number[]; // Список вершин: элементы представляют «значения вершин», а индексы — «индексы вершин»\n    adjMat: number[][]; // Матрица смежности, где индексы строк и столбцов соответствуют «индексам вершин»\n\n    /* Конструктор */\n    constructor(vertices: number[], edges: number[][]) {\n        this.vertices = [];\n        this.adjMat = [];\n        // Добавление вершины\n        for (const val of vertices) {\n            this.addVertex(val);\n        }\n        // Добавить ребра\n        // Обратите внимание: элементы edges представляют собой индексы вершин, то есть соответствуют индексам элементов vertices\n        for (const e of edges) {\n            this.addEdge(e[0], e[1]);\n        }\n    }\n\n    /* Получить число вершин */\n    size(): number {\n        return this.vertices.length;\n    }\n\n    /* Добавление вершины */\n    addVertex(val: number): void {\n        const n: number = this.size();\n        // Добавить значение новой вершины в список вершин\n        this.vertices.push(val);\n        // Добавить строку в матрицу смежности\n        const newRow: number[] = [];\n        for (let j: number = 0; j < n; j++) {\n            newRow.push(0);\n        }\n        this.adjMat.push(newRow);\n        // Добавить столбец в матрицу смежности\n        for (const row of this.adjMat) {\n            row.push(0);\n        }\n    }\n\n    /* Удаление вершины */\n    removeVertex(index: number): void {\n        if (index >= this.size()) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        // Удалить вершину с индексом index из списка вершин\n        this.vertices.splice(index, 1);\n\n        // Удалить строку с индексом index из матрицы смежности\n        this.adjMat.splice(index, 1);\n        // Удалить столбец с индексом index из матрицы смежности\n        for (const row of this.adjMat) {\n            row.splice(index, 1);\n        }\n    }\n\n    /* Добавление ребра */\n    // Параметры i и j соответствуют индексам элементов vertices\n    addEdge(i: number, j: number): void {\n        // Обработка выхода индекса за границы и случая равенства\n        if (i < 0 || j < 0 || i >= this.size() || j >= this.size() || i === j) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        // В неориентированном графе матрица смежности симметрична относительно главной диагонали, то есть выполняется (i, j) === (j, i)\n        this.adjMat[i][j] = 1;\n        this.adjMat[j][i] = 1;\n    }\n\n    /* Удаление ребра */\n    // Параметры i и j соответствуют индексам элементов vertices\n    removeEdge(i: number, j: number): void {\n        // Обработка выхода индекса за границы и случая равенства\n        if (i < 0 || j < 0 || i >= this.size() || j >= this.size() || i === j) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        this.adjMat[i][j] = 0;\n        this.adjMat[j][i] = 0;\n    }\n\n    /* Вывести матрицу смежности */\n    print(): void {\n        console.log('Список вершин = ', this.vertices);\n        console.log('Матрица смежности =', this.adjMat);\n    }\n}\n
        graph_adjacency_matrix.dart
        /* Класс неориентированного графа на основе матрицы смежности */\nclass GraphAdjMat {\n  List<int> vertices = []; // Элемент вершины: элемент представляет «значение вершины», индекс представляет «индекс вершины»\n  List<List<int>> adjMat = []; // Матрица смежности, где индексы строк и столбцов соответствуют «индексам вершин»\n\n  /* Конструктор */\n  GraphAdjMat(List<int> vertices, List<List<int>> edges) {\n    this.vertices = [];\n    this.adjMat = [];\n    // Добавление вершины\n    for (int val in vertices) {\n      addVertex(val);\n    }\n    // Добавить ребра\n    // Обратите внимание: элементы edges представляют собой индексы вершин, то есть соответствуют индексам элементов vertices\n    for (List<int> e in edges) {\n      addEdge(e[0], e[1]);\n    }\n  }\n\n  /* Получить число вершин */\n  int size() {\n    return vertices.length;\n  }\n\n  /* Добавление вершины */\n  void addVertex(int val) {\n    int n = size();\n    // Добавить значение новой вершины в список вершин\n    vertices.add(val);\n    // Добавить строку в матрицу смежности\n    List<int> newRow = List.filled(n, 0, growable: true);\n    adjMat.add(newRow);\n    // Добавить столбец в матрицу смежности\n    for (List<int> row in adjMat) {\n      row.add(0);\n    }\n  }\n\n  /* Удаление вершины */\n  void removeVertex(int index) {\n    if (index >= size()) {\n      throw IndexError;\n    }\n    // Удалить вершину с индексом index из списка вершин\n    vertices.removeAt(index);\n    // Удалить строку с индексом index из матрицы смежности\n    adjMat.removeAt(index);\n    // Удалить столбец с индексом index из матрицы смежности\n    for (List<int> row in adjMat) {\n      row.removeAt(index);\n    }\n  }\n\n  /* Добавление ребра */\n  // Параметры i и j соответствуют индексам элементов vertices\n  void addEdge(int i, int j) {\n    // Обработка выхода индекса за границы и случая равенства\n    if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {\n      throw IndexError;\n    }\n    // В неориентированном графе матрица смежности симметрична относительно главной диагонали, то есть выполняется (i, j) == (j, i)\n    adjMat[i][j] = 1;\n    adjMat[j][i] = 1;\n  }\n\n  /* Удаление ребра */\n  // Параметры i и j соответствуют индексам элементов vertices\n  void removeEdge(int i, int j) {\n    // Обработка выхода индекса за границы и случая равенства\n    if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {\n      throw IndexError;\n    }\n    adjMat[i][j] = 0;\n    adjMat[j][i] = 0;\n  }\n\n  /* Вывести матрицу смежности */\n  void printAdjMat() {\n    print(\"Список вершин = $vertices\");\n    print(\"Матрица смежности = \");\n    printMatrix(adjMat);\n  }\n}\n
        graph_adjacency_matrix.rs
        /* Тип неориентированного графа на основе матрицы смежности */\npub struct GraphAdjMat {\n    // Список вершин: элементы представляют «значения вершин», а индексы — «индексы вершин»\n    pub vertices: Vec<i32>,\n    // Матрица смежности, где индексы строк и столбцов соответствуют «индексам вершин»\n    pub adj_mat: Vec<Vec<i32>>,\n}\n\nimpl GraphAdjMat {\n    /* Конструктор */\n    pub fn new(vertices: Vec<i32>, edges: Vec<[usize; 2]>) -> Self {\n        let mut graph = GraphAdjMat {\n            vertices: vec![],\n            adj_mat: vec![],\n        };\n        // Добавление вершины\n        for val in vertices {\n            graph.add_vertex(val);\n        }\n        // Добавить ребра\n        // Обратите внимание: элементы edges представляют собой индексы вершин, то есть соответствуют индексам элементов vertices\n        for edge in edges {\n            graph.add_edge(edge[0], edge[1])\n        }\n\n        graph\n    }\n\n    /* Получить число вершин */\n    pub fn size(&self) -> usize {\n        self.vertices.len()\n    }\n\n    /* Добавление вершины */\n    pub fn add_vertex(&mut self, val: i32) {\n        let n = self.size();\n        // Добавить значение новой вершины в список вершин\n        self.vertices.push(val);\n        // Добавить строку в матрицу смежности\n        self.adj_mat.push(vec![0; n]);\n        // Добавить столбец в матрицу смежности\n        for row in self.adj_mat.iter_mut() {\n            row.push(0);\n        }\n    }\n\n    /* Удаление вершины */\n    pub fn remove_vertex(&mut self, index: usize) {\n        if index >= self.size() {\n            panic!(\"index error\")\n        }\n        // Удалить вершину с индексом index из списка вершин\n        self.vertices.remove(index);\n        // Удалить строку с индексом index из матрицы смежности\n        self.adj_mat.remove(index);\n        // Удалить столбец с индексом index из матрицы смежности\n        for row in self.adj_mat.iter_mut() {\n            row.remove(index);\n        }\n    }\n\n    /* Добавление ребра */\n    pub fn add_edge(&mut self, i: usize, j: usize) {\n        // Параметры i и j соответствуют индексам элементов vertices\n        // Обработка выхода индекса за границы и случая равенства\n        if i >= self.size() || j >= self.size() || i == j {\n            panic!(\"index error\")\n        }\n        // В неориентированном графе матрица смежности симметрична относительно главной диагонали, то есть выполняется (i, j) == (j, i)\n        self.adj_mat[i][j] = 1;\n        self.adj_mat[j][i] = 1;\n    }\n\n    /* Удаление ребра */\n    // Параметры i и j соответствуют индексам элементов vertices\n    pub fn remove_edge(&mut self, i: usize, j: usize) {\n        // Параметры i и j соответствуют индексам элементов vertices\n        // Обработка выхода индекса за границы и случая равенства\n        if i >= self.size() || j >= self.size() || i == j {\n            panic!(\"index error\")\n        }\n        self.adj_mat[i][j] = 0;\n        self.adj_mat[j][i] = 0;\n    }\n\n    /* Вывести матрицу смежности */\n    pub fn print(&self) {\n        println!(\"Список вершин = {:?}\", self.vertices);\n        println!(\"Матрица смежности =\");\n        println!(\"[\");\n        for row in &self.adj_mat {\n            println!(\"  {:?},\", row);\n        }\n        println!(\"]\")\n    }\n}\n
        graph_adjacency_matrix.c
        /* Структура неориентированного графа на основе матрицы смежности */\ntypedef struct {\n    int vertices[MAX_SIZE];\n    int adjMat[MAX_SIZE][MAX_SIZE];\n    int size;\n} GraphAdjMat;\n\n/* Конструктор */\nGraphAdjMat *newGraphAdjMat() {\n    GraphAdjMat *graph = (GraphAdjMat *)malloc(sizeof(GraphAdjMat));\n    graph->size = 0;\n    for (int i = 0; i < MAX_SIZE; i++) {\n        for (int j = 0; j < MAX_SIZE; j++) {\n            graph->adjMat[i][j] = 0;\n        }\n    }\n    return graph;\n}\n\n/* Деструктор */\nvoid delGraphAdjMat(GraphAdjMat *graph) {\n    free(graph);\n}\n\n/* Добавление вершины */\nvoid addVertex(GraphAdjMat *graph, int val) {\n    if (graph->size == MAX_SIZE) {\n        fprintf(stderr, \"Количество вершин графа уже достигло максимума\\n\");\n        return;\n    }\n    // Добавить n-ю вершину и обнулить n-ю строку и столбец\n    int n = graph->size;\n    graph->vertices[n] = val;\n    for (int i = 0; i <= n; i++) {\n        graph->adjMat[n][i] = graph->adjMat[i][n] = 0;\n    }\n    graph->size++;\n}\n\n/* Удаление вершины */\nvoid removeVertex(GraphAdjMat *graph, int index) {\n    if (index < 0 || index >= graph->size) {\n        fprintf(stderr, \"индекс вершины выходит за границы\\n\");\n        return;\n    }\n    // Удалить вершину с индексом index из списка вершин\n    for (int i = index; i < graph->size - 1; i++) {\n        graph->vertices[i] = graph->vertices[i + 1];\n    }\n    // Удалить строку с индексом index из матрицы смежности\n    for (int i = index; i < graph->size - 1; i++) {\n        for (int j = 0; j < graph->size; j++) {\n            graph->adjMat[i][j] = graph->adjMat[i + 1][j];\n        }\n    }\n    // Удалить столбец с индексом index из матрицы смежности\n    for (int i = 0; i < graph->size; i++) {\n        for (int j = index; j < graph->size - 1; j++) {\n            graph->adjMat[i][j] = graph->adjMat[i][j + 1];\n        }\n    }\n    graph->size--;\n}\n\n/* Добавление ребра */\n// Параметры i и j соответствуют индексам элементов vertices\nvoid addEdge(GraphAdjMat *graph, int i, int j) {\n    if (i < 0 || j < 0 || i >= graph->size || j >= graph->size || i == j) {\n        fprintf(stderr, \"индексы ребра выходят за границы или совпадают\\n\");\n        return;\n    }\n    graph->adjMat[i][j] = 1;\n    graph->adjMat[j][i] = 1;\n}\n\n/* Удаление ребра */\n// Параметры i и j соответствуют индексам элементов vertices\nvoid removeEdge(GraphAdjMat *graph, int i, int j) {\n    if (i < 0 || j < 0 || i >= graph->size || j >= graph->size || i == j) {\n        fprintf(stderr, \"индексы ребра выходят за границы или совпадают\\n\");\n        return;\n    }\n    graph->adjMat[i][j] = 0;\n    graph->adjMat[j][i] = 0;\n}\n\n/* Вывести матрицу смежности */\nvoid printGraphAdjMat(GraphAdjMat *graph) {\n    printf(\"Список вершин = \");\n    printArray(graph->vertices, graph->size);\n    printf(\"Матрица смежности =\\n\");\n    for (int i = 0; i < graph->size; i++) {\n        printArray(graph->adjMat[i], graph->size);\n    }\n}\n
        graph_adjacency_matrix.kt
        /* Класс неориентированного графа на основе матрицы смежности */\nclass GraphAdjMat(vertices: IntArray, edges: Array<IntArray>) {\n    val vertices = mutableListOf<Int>() // Список вершин: элементы представляют «значения вершин», а индексы — «индексы вершин»\n    val adjMat = mutableListOf<MutableList<Int>>() // Матрица смежности, где индексы строк и столбцов соответствуют «индексам вершин»\n\n    /* Конструктор */\n    init {\n        // Добавление вершины\n        for (vertex in vertices) {\n            addVertex(vertex)\n        }\n        // Добавить ребра\n        // Обратите внимание: элементы edges представляют собой индексы вершин, то есть соответствуют индексам элементов vertices\n        for (edge in edges) {\n            addEdge(edge[0], edge[1])\n        }\n    }\n\n    /* Получить число вершин */\n    fun size(): Int {\n        return vertices.size\n    }\n\n    /* Добавление вершины */\n    fun addVertex(_val: Int) {\n        val n = size()\n        // Добавить значение новой вершины в список вершин\n        vertices.add(_val)\n        // Добавить строку в матрицу смежности\n        val newRow = mutableListOf<Int>()\n        for (j in 0..<n) {\n            newRow.add(0)\n        }\n        adjMat.add(newRow)\n        // Добавить столбец в матрицу смежности\n        for (row in adjMat) {\n            row.add(0)\n        }\n    }\n\n    /* Удаление вершины */\n    fun removeVertex(index: Int) {\n        if (index >= size())\n            throw IndexOutOfBoundsException()\n        // Удалить вершину с индексом index из списка вершин\n        vertices.removeAt(index)\n        // Удалить строку с индексом index из матрицы смежности\n        adjMat.removeAt(index)\n        // Удалить столбец с индексом index из матрицы смежности\n        for (row in adjMat) {\n            row.removeAt(index)\n        }\n    }\n\n    /* Добавление ребра */\n    // Параметры i и j соответствуют индексам элементов vertices\n    fun addEdge(i: Int, j: Int) {\n        // Обработка выхода индекса за границы и случая равенства\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j)\n            throw IndexOutOfBoundsException()\n        // В неориентированном графе матрица смежности симметрична относительно главной диагонали, то есть выполняется (i, j) == (j, i)\n        adjMat[i][j] = 1\n        adjMat[j][i] = 1\n    }\n\n    /* Удаление ребра */\n    // Параметры i и j соответствуют индексам элементов vertices\n    fun removeEdge(i: Int, j: Int) {\n        // Обработка выхода индекса за границы и случая равенства\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j)\n            throw IndexOutOfBoundsException()\n        adjMat[i][j] = 0\n        adjMat[j][i] = 0\n    }\n\n    /* Вывести матрицу смежности */\n    fun print() {\n        print(\"Список вершин = \")\n        println(vertices)\n        println(\"Матрица смежности =\")\n        printMatrix(adjMat)\n    }\n}\n
        graph_adjacency_matrix.rb
        ### Класс неориентированного графа на основе матрицы смежности ###\nclass GraphAdjMat\n  def initialize(vertices, edges)\n    ### Конструктор ###\n    # Список вершин: элементы представляют «значения вершин», а индексы — «индексы вершин»\n    @vertices = []\n    # Матрица смежности, где индексы строк и столбцов соответствуют «индексам вершин»\n    @adj_mat = []\n    # Добавление вершины\n    vertices.each { |val| add_vertex(val) }\n    # Добавить ребра\n    # Обратите внимание: элементы edges представляют собой индексы вершин, то есть соответствуют индексам элементов vertices\n    edges.each { |e| add_edge(e[0], e[1]) }\n  end\n\n  ### Получение числа вершин ###\n  def size\n    @vertices.length\n  end\n\n  ### Добавление вершины ###\n  def add_vertex(val)\n    n = size\n    # Добавить значение новой вершины в список вершин\n    @vertices << val\n    # Добавить строку в матрицу смежности\n    new_row = Array.new(n, 0)\n    @adj_mat << new_row\n    # Добавить столбец в матрицу смежности\n    @adj_mat.each { |row| row << 0 }\n  end\n\n  ### Удаление вершины ###\n  def remove_vertex(index)\n    raise IndexError if index >= size\n\n    # Удалить вершину с индексом index из списка вершин\n    @vertices.delete_at(index)\n    # Удалить строку с индексом index из матрицы смежности\n    @adj_mat.delete_at(index)\n    # Удалить столбец с индексом index из матрицы смежности\n    @adj_mat.each { |row| row.delete_at(index) }\n  end\n\n  ### Добавление ребра ###\n  def add_edge(i, j)\n    # Параметры i и j соответствуют индексам элементов vertices\n    # Обработка выхода индекса за границы и случая равенства\n    if i < 0 || j < 0 || i >= size || j >= size || i == j\n      raise IndexError\n    end\n    # В неориентированном графе матрица смежности симметрична относительно главной диагонали, то есть выполняется (i, j) == (j, i)\n    @adj_mat[i][j] = 1\n    @adj_mat[j][i] = 1\n  end\n\n  ### Удаление ребра ###\n  def remove_edge(i, j)\n    # Параметры i и j соответствуют индексам элементов vertices\n    # Обработка выхода индекса за границы и случая равенства\n    if i < 0 || j < 0 || i >= size || j >= size || i == j\n      raise IndexError\n    end\n    @adj_mat[i][j] = 0\n    @adj_mat[j][i] = 0\n  end\n\n  ### Вывести матрицу смежности ###\n  def __print__\n    puts \"Список вершин = #{@vertices}\"\n    puts 'Матрица смежности ='\n    print_matrix(@adj_mat)\n  end\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 9. Графы","9.2   Базовые операции графа"],"tags":[]},{"location":"chapter_graph/graph_operations/#922","level":2,"title":"9.2.2   Реализация на основе списка смежности","text":"

        Пусть неориентированный граф содержит в сумме \\(n\\) вершин и \\(m\\) ребер. Тогда различные операции можно реализовать способом, показанным на рисунках ниже.

        • Добавление ребра: достаточно добавить ребро в конец списка, соответствующего вершине; это требует \\(O(1)\\) времени. Поскольку граф неориентированный, необходимо одновременно добавить ребра в обоих направлениях.
        • Удаление ребра: нужно найти и удалить указанное ребро в списке, соответствующем вершине; это требует \\(O(m)\\) времени. В неориентированном графе необходимо удалить ребра в обоих направлениях.
        • Добавление вершины: в список смежности добавляется еще один список, а новая вершина становится его головным узлом; это требует \\(O(1)\\) времени.
        • Удаление вершины: требуется пройти по всему списку смежности и удалить все ребра, содержащие указанную вершину; это требует \\(O(n + m)\\) времени.
        • Инициализация: в списке смежности создаются \\(n\\) вершин и \\(2m\\) ребер; это требует \\(O(n + m)\\) времени.
        <1><2><3><4><5>

        Рисунок 9-8   Инициализация списка смежности, добавление и удаление ребер и вершин

        Ниже приведен код списка смежности. По сравнению с рисунками выше, реальная реализация имеет следующие отличия.

        • Чтобы упростить добавление и удаление вершин, а также сделать код проще, мы используем список, то есть динамический массив, вместо связного списка.
        • Для хранения списка смежности используется хеш-таблица, где key - это экземпляр вершины, а value - список смежных вершин данной вершины.

        Кроме того, в списке смежности используется класс Vertex для представления вершины. Причина в том, что если, как и в матрице смежности, различать вершины по индексам списка, то при удалении вершины с индексом \\(i\\) пришлось бы обходить весь список смежности и уменьшать на \\(1\\) все индексы, большие \\(i\\) , что крайне неэффективно. Если же каждая вершина является уникальным экземпляром Vertex , то после удаления одной вершины остальные вершины менять уже не требуется.

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_adjacency_list.py
        class GraphAdjList:\n    \"\"\"Класс неориентированного графа на основе списка смежности\"\"\"\n\n    def __init__(self, edges: list[list[Vertex]]):\n        \"\"\"Конструктор\"\"\"\n        # Список смежности, где key — вершина, а value — все смежные ей вершины\n        self.adj_list = dict[Vertex, list[Vertex]]()\n        # Добавить все вершины и ребра\n        for edge in edges:\n            self.add_vertex(edge[0])\n            self.add_vertex(edge[1])\n            self.add_edge(edge[0], edge[1])\n\n    def size(self) -> int:\n        \"\"\"Получить число вершин\"\"\"\n        return len(self.adj_list)\n\n    def add_edge(self, vet1: Vertex, vet2: Vertex):\n        \"\"\"Добавление ребра\"\"\"\n        if vet1 not in self.adj_list or vet2 not in self.adj_list or vet1 == vet2:\n            raise ValueError()\n        # Добавить ребро vet1 - vet2\n        self.adj_list[vet1].append(vet2)\n        self.adj_list[vet2].append(vet1)\n\n    def remove_edge(self, vet1: Vertex, vet2: Vertex):\n        \"\"\"Удаление ребра\"\"\"\n        if vet1 not in self.adj_list or vet2 not in self.adj_list or vet1 == vet2:\n            raise ValueError()\n        # Удалить ребро vet1 - vet2\n        self.adj_list[vet1].remove(vet2)\n        self.adj_list[vet2].remove(vet1)\n\n    def add_vertex(self, vet: Vertex):\n        \"\"\"Добавление вершины\"\"\"\n        if vet in self.adj_list:\n            return\n        # Добавить новый список в список смежности\n        self.adj_list[vet] = []\n\n    def remove_vertex(self, vet: Vertex):\n        \"\"\"Удаление вершины\"\"\"\n        if vet not in self.adj_list:\n            raise ValueError()\n        # Удалить из списка смежности список, соответствующий вершине vet\n        self.adj_list.pop(vet)\n        # Обойти списки других вершин и удалить все ребра, содержащие vet\n        for vertex in self.adj_list:\n            if vet in self.adj_list[vertex]:\n                self.adj_list[vertex].remove(vet)\n\n    def print(self):\n        \"\"\"Вывести список смежности\"\"\"\n        print(\"Список смежности =\")\n        for vertex in self.adj_list:\n            tmp = [v.val for v in self.adj_list[vertex]]\n            print(f\"{vertex.val}: {tmp},\")\n
        graph_adjacency_list.cpp
        /* Класс неориентированного графа на основе списка смежности */\nclass GraphAdjList {\n  public:\n    // Список смежности, где key — вершина, а value — все смежные ей вершины\n    unordered_map<Vertex *, vector<Vertex *>> adjList;\n\n    /* Удалить указанный узел из vector */\n    void remove(vector<Vertex *> &vec, Vertex *vet) {\n        for (int i = 0; i < vec.size(); i++) {\n            if (vec[i] == vet) {\n                vec.erase(vec.begin() + i);\n                break;\n            }\n        }\n    }\n\n    /* Конструктор */\n    GraphAdjList(const vector<vector<Vertex *>> &edges) {\n        // Добавить все вершины и ребра\n        for (const vector<Vertex *> &edge : edges) {\n            addVertex(edge[0]);\n            addVertex(edge[1]);\n            addEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* Получить число вершин */\n    int size() {\n        return adjList.size();\n    }\n\n    /* Добавление ребра */\n    void addEdge(Vertex *vet1, Vertex *vet2) {\n        if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2)\n            throw invalid_argument(\"вершина не существует\");\n        // Добавить ребро vet1 - vet2\n        adjList[vet1].push_back(vet2);\n        adjList[vet2].push_back(vet1);\n    }\n\n    /* Удаление ребра */\n    void removeEdge(Vertex *vet1, Vertex *vet2) {\n        if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2)\n            throw invalid_argument(\"вершина не существует\");\n        // Удалить ребро vet1 - vet2\n        remove(adjList[vet1], vet2);\n        remove(adjList[vet2], vet1);\n    }\n\n    /* Добавление вершины */\n    void addVertex(Vertex *vet) {\n        if (adjList.count(vet))\n            return;\n        // Добавить новый список в список смежности\n        adjList[vet] = vector<Vertex *>();\n    }\n\n    /* Удаление вершины */\n    void removeVertex(Vertex *vet) {\n        if (!adjList.count(vet))\n            throw invalid_argument(\"вершина не существует\");\n        // Удалить из списка смежности список, соответствующий вершине vet\n        adjList.erase(vet);\n        // Обойти списки других вершин и удалить все ребра, содержащие vet\n        for (auto &adj : adjList) {\n            remove(adj.second, vet);\n        }\n    }\n\n    /* Вывести список смежности */\n    void print() {\n        cout << \"Список смежности =\" << endl;\n        for (auto &adj : adjList) {\n            const auto &key = adj.first;\n            const auto &vec = adj.second;\n            cout << key->val << \": \";\n            printVector(vetsToVals(vec));\n        }\n    }\n};\n
        graph_adjacency_list.java
        /* Класс неориентированного графа на основе списка смежности */\nclass GraphAdjList {\n    // Список смежности, где key — вершина, а value — все смежные ей вершины\n    Map<Vertex, List<Vertex>> adjList;\n\n    /* Конструктор */\n    public GraphAdjList(Vertex[][] edges) {\n        this.adjList = new HashMap<>();\n        // Добавить все вершины и ребра\n        for (Vertex[] edge : edges) {\n            addVertex(edge[0]);\n            addVertex(edge[1]);\n            addEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* Получить число вершин */\n    public int size() {\n        return adjList.size();\n    }\n\n    /* Добавление ребра */\n    public void addEdge(Vertex vet1, Vertex vet2) {\n        if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n            throw new IllegalArgumentException();\n        // Добавить ребро vet1 - vet2\n        adjList.get(vet1).add(vet2);\n        adjList.get(vet2).add(vet1);\n    }\n\n    /* Удаление ребра */\n    public void removeEdge(Vertex vet1, Vertex vet2) {\n        if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n            throw new IllegalArgumentException();\n        // Удалить ребро vet1 - vet2\n        adjList.get(vet1).remove(vet2);\n        adjList.get(vet2).remove(vet1);\n    }\n\n    /* Добавление вершины */\n    public void addVertex(Vertex vet) {\n        if (adjList.containsKey(vet))\n            return;\n        // Добавить новый список в список смежности\n        adjList.put(vet, new ArrayList<>());\n    }\n\n    /* Удаление вершины */\n    public void removeVertex(Vertex vet) {\n        if (!adjList.containsKey(vet))\n            throw new IllegalArgumentException();\n        // Удалить из списка смежности список, соответствующий вершине vet\n        adjList.remove(vet);\n        // Обойти списки других вершин и удалить все ребра, содержащие vet\n        for (List<Vertex> list : adjList.values()) {\n            list.remove(vet);\n        }\n    }\n\n    /* Вывести список смежности */\n    public void print() {\n        System.out.println(\"Список смежности =\");\n        for (Map.Entry<Vertex, List<Vertex>> pair : adjList.entrySet()) {\n            List<Integer> tmp = new ArrayList<>();\n            for (Vertex vertex : pair.getValue())\n                tmp.add(vertex.val);\n            System.out.println(pair.getKey().val + \": \" + tmp + \",\");\n        }\n    }\n}\n
        graph_adjacency_list.cs
        /* Класс неориентированного графа на основе списка смежности */\nclass GraphAdjList {\n    // Список смежности, где key — вершина, а value — все смежные ей вершины\n    public Dictionary<Vertex, List<Vertex>> adjList;\n\n    /* Конструктор */\n    public GraphAdjList(Vertex[][] edges) {\n        adjList = [];\n        // Добавить все вершины и ребра\n        foreach (Vertex[] edge in edges) {\n            AddVertex(edge[0]);\n            AddVertex(edge[1]);\n            AddEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* Получить число вершин */\n    int Size() {\n        return adjList.Count;\n    }\n\n    /* Добавление ребра */\n    public void AddEdge(Vertex vet1, Vertex vet2) {\n        if (!adjList.ContainsKey(vet1) || !adjList.ContainsKey(vet2) || vet1 == vet2)\n            throw new InvalidOperationException();\n        // Добавить ребро vet1 - vet2\n        adjList[vet1].Add(vet2);\n        adjList[vet2].Add(vet1);\n    }\n\n    /* Удаление ребра */\n    public void RemoveEdge(Vertex vet1, Vertex vet2) {\n        if (!adjList.ContainsKey(vet1) || !adjList.ContainsKey(vet2) || vet1 == vet2)\n            throw new InvalidOperationException();\n        // Удалить ребро vet1 - vet2\n        adjList[vet1].Remove(vet2);\n        adjList[vet2].Remove(vet1);\n    }\n\n    /* Добавление вершины */\n    public void AddVertex(Vertex vet) {\n        if (adjList.ContainsKey(vet))\n            return;\n        // Добавить новый список в список смежности\n        adjList.Add(vet, []);\n    }\n\n    /* Удаление вершины */\n    public void RemoveVertex(Vertex vet) {\n        if (!adjList.ContainsKey(vet))\n            throw new InvalidOperationException();\n        // Удалить из списка смежности список, соответствующий вершине vet\n        adjList.Remove(vet);\n        // Обойти списки других вершин и удалить все ребра, содержащие vet\n        foreach (List<Vertex> list in adjList.Values) {\n            list.Remove(vet);\n        }\n    }\n\n    /* Вывести список смежности */\n    public void Print() {\n        Console.WriteLine(\"Список смежности =\");\n        foreach (KeyValuePair<Vertex, List<Vertex>> pair in adjList) {\n            List<int> tmp = [];\n            foreach (Vertex vertex in pair.Value)\n                tmp.Add(vertex.val);\n            Console.WriteLine(pair.Key.val + \": [\" + string.Join(\", \", tmp) + \"],\");\n        }\n    }\n}\n
        graph_adjacency_list.go
        /* Класс неориентированного графа на основе списка смежности */\ntype graphAdjList struct {\n    // Список смежности, где key — вершина, а value — все смежные ей вершины\n    adjList map[Vertex][]Vertex\n}\n\n/* Конструктор */\nfunc newGraphAdjList(edges [][]Vertex) *graphAdjList {\n    g := &graphAdjList{\n        adjList: make(map[Vertex][]Vertex),\n    }\n    // Добавить все вершины и ребра\n    for _, edge := range edges {\n        g.addVertex(edge[0])\n        g.addVertex(edge[1])\n        g.addEdge(edge[0], edge[1])\n    }\n    return g\n}\n\n/* Получить число вершин */\nfunc (g *graphAdjList) size() int {\n    return len(g.adjList)\n}\n\n/* Добавление ребра */\nfunc (g *graphAdjList) addEdge(vet1 Vertex, vet2 Vertex) {\n    _, ok1 := g.adjList[vet1]\n    _, ok2 := g.adjList[vet2]\n    if !ok1 || !ok2 || vet1 == vet2 {\n        panic(\"error\")\n    }\n    // Добавить ребро vet1 - vet2, добавив анонимную struct{}\n    g.adjList[vet1] = append(g.adjList[vet1], vet2)\n    g.adjList[vet2] = append(g.adjList[vet2], vet1)\n}\n\n/* Удаление ребра */\nfunc (g *graphAdjList) removeEdge(vet1 Vertex, vet2 Vertex) {\n    _, ok1 := g.adjList[vet1]\n    _, ok2 := g.adjList[vet2]\n    if !ok1 || !ok2 || vet1 == vet2 {\n        panic(\"error\")\n    }\n    // Удалить ребро vet1 - vet2\n    g.adjList[vet1] = DeleteSliceElms(g.adjList[vet1], vet2)\n    g.adjList[vet2] = DeleteSliceElms(g.adjList[vet2], vet1)\n}\n\n/* Добавление вершины */\nfunc (g *graphAdjList) addVertex(vet Vertex) {\n    _, ok := g.adjList[vet]\n    if ok {\n        return\n    }\n    // Добавить новый список в список смежности\n    g.adjList[vet] = make([]Vertex, 0)\n}\n\n/* Удаление вершины */\nfunc (g *graphAdjList) removeVertex(vet Vertex) {\n    _, ok := g.adjList[vet]\n    if !ok {\n        panic(\"error\")\n    }\n    // Удалить из списка смежности список, соответствующий вершине vet\n    delete(g.adjList, vet)\n    // Обойти списки других вершин и удалить все ребра, содержащие vet\n    for v, list := range g.adjList {\n        g.adjList[v] = DeleteSliceElms(list, vet)\n    }\n}\n\n/* Вывести список смежности */\nfunc (g *graphAdjList) print() {\n    var builder strings.Builder\n    fmt.Printf(\"Список смежности = \\n\")\n    for k, v := range g.adjList {\n        builder.WriteString(\"\\t\\t\" + strconv.Itoa(k.Val) + \": \")\n        for _, vet := range v {\n            builder.WriteString(strconv.Itoa(vet.Val) + \" \")\n        }\n        fmt.Println(builder.String())\n        builder.Reset()\n    }\n}\n
        graph_adjacency_list.swift
        /* Класс неориентированного графа на основе списка смежности */\nclass GraphAdjList {\n    // Список смежности, где key — вершина, а value — все смежные ей вершины\n    public private(set) var adjList: [Vertex: [Vertex]]\n\n    /* Конструктор */\n    public init(edges: [[Vertex]]) {\n        adjList = [:]\n        // Добавить все вершины и ребра\n        for edge in edges {\n            addVertex(vet: edge[0])\n            addVertex(vet: edge[1])\n            addEdge(vet1: edge[0], vet2: edge[1])\n        }\n    }\n\n    /* Получить число вершин */\n    public func size() -> Int {\n        adjList.count\n    }\n\n    /* Добавление ребра */\n    public func addEdge(vet1: Vertex, vet2: Vertex) {\n        if adjList[vet1] == nil || adjList[vet2] == nil || vet1 == vet2 {\n            fatalError(\"Неверный аргумент\")\n        }\n        // Добавить ребро vet1 - vet2\n        adjList[vet1]?.append(vet2)\n        adjList[vet2]?.append(vet1)\n    }\n\n    /* Удаление ребра */\n    public func removeEdge(vet1: Vertex, vet2: Vertex) {\n        if adjList[vet1] == nil || adjList[vet2] == nil || vet1 == vet2 {\n            fatalError(\"Неверный аргумент\")\n        }\n        // Удалить ребро vet1 - vet2\n        adjList[vet1]?.removeAll { $0 == vet2 }\n        adjList[vet2]?.removeAll { $0 == vet1 }\n    }\n\n    /* Добавление вершины */\n    public func addVertex(vet: Vertex) {\n        if adjList[vet] != nil {\n            return\n        }\n        // Добавить новый список в список смежности\n        adjList[vet] = []\n    }\n\n    /* Удаление вершины */\n    public func removeVertex(vet: Vertex) {\n        if adjList[vet] == nil {\n            fatalError(\"Неверный аргумент\")\n        }\n        // Удалить из списка смежности список, соответствующий вершине vet\n        adjList.removeValue(forKey: vet)\n        // Обойти списки других вершин и удалить все ребра, содержащие vet\n        for key in adjList.keys {\n            adjList[key]?.removeAll { $0 == vet }\n        }\n    }\n\n    /* Вывести список смежности */\n    public func print() {\n        Swift.print(\"Список смежности =\")\n        for (vertex, list) in adjList {\n            let list = list.map { $0.val }\n            Swift.print(\"\\(vertex.val): \\(list),\")\n        }\n    }\n}\n
        graph_adjacency_list.js
        /* Класс неориентированного графа на основе списка смежности */\nclass GraphAdjList {\n    // Список смежности, где key — вершина, а value — все смежные ей вершины\n    adjList;\n\n    /* Конструктор */\n    constructor(edges) {\n        this.adjList = new Map();\n        // Добавить все вершины и ребра\n        for (const edge of edges) {\n            this.addVertex(edge[0]);\n            this.addVertex(edge[1]);\n            this.addEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* Получить число вершин */\n    size() {\n        return this.adjList.size;\n    }\n\n    /* Добавление ребра */\n    addEdge(vet1, vet2) {\n        if (\n            !this.adjList.has(vet1) ||\n            !this.adjList.has(vet2) ||\n            vet1 === vet2\n        ) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // Добавить ребро vet1 - vet2\n        this.adjList.get(vet1).push(vet2);\n        this.adjList.get(vet2).push(vet1);\n    }\n\n    /* Удаление ребра */\n    removeEdge(vet1, vet2) {\n        if (\n            !this.adjList.has(vet1) ||\n            !this.adjList.has(vet2) ||\n            vet1 === vet2 ||\n            this.adjList.get(vet1).indexOf(vet2) === -1\n        ) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // Удалить ребро vet1 - vet2\n        this.adjList.get(vet1).splice(this.adjList.get(vet1).indexOf(vet2), 1);\n        this.adjList.get(vet2).splice(this.adjList.get(vet2).indexOf(vet1), 1);\n    }\n\n    /* Добавление вершины */\n    addVertex(vet) {\n        if (this.adjList.has(vet)) return;\n        // Добавить новый список в список смежности\n        this.adjList.set(vet, []);\n    }\n\n    /* Удаление вершины */\n    removeVertex(vet) {\n        if (!this.adjList.has(vet)) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // Удалить из списка смежности список, соответствующий вершине vet\n        this.adjList.delete(vet);\n        // Обойти списки других вершин и удалить все ребра, содержащие vet\n        for (const set of this.adjList.values()) {\n            const index = set.indexOf(vet);\n            if (index > -1) {\n                set.splice(index, 1);\n            }\n        }\n    }\n\n    /* Вывести список смежности */\n    print() {\n        console.log('Список смежности =');\n        for (const [key, value] of this.adjList) {\n            const tmp = [];\n            for (const vertex of value) {\n                tmp.push(vertex.val);\n            }\n            console.log(key.val + ': ' + tmp.join());\n        }\n    }\n}\n
        graph_adjacency_list.ts
        /* Класс неориентированного графа на основе списка смежности */\nclass GraphAdjList {\n    // Список смежности, где key — вершина, а value — все смежные ей вершины\n    adjList: Map<Vertex, Vertex[]>;\n\n    /* Конструктор */\n    constructor(edges: Vertex[][]) {\n        this.adjList = new Map();\n        // Добавить все вершины и ребра\n        for (const edge of edges) {\n            this.addVertex(edge[0]);\n            this.addVertex(edge[1]);\n            this.addEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* Получить число вершин */\n    size(): number {\n        return this.adjList.size;\n    }\n\n    /* Добавление ребра */\n    addEdge(vet1: Vertex, vet2: Vertex): void {\n        if (\n            !this.adjList.has(vet1) ||\n            !this.adjList.has(vet2) ||\n            vet1 === vet2\n        ) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // Добавить ребро vet1 - vet2\n        this.adjList.get(vet1).push(vet2);\n        this.adjList.get(vet2).push(vet1);\n    }\n\n    /* Удаление ребра */\n    removeEdge(vet1: Vertex, vet2: Vertex): void {\n        if (\n            !this.adjList.has(vet1) ||\n            !this.adjList.has(vet2) ||\n            vet1 === vet2 ||\n            this.adjList.get(vet1).indexOf(vet2) === -1\n        ) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // Удалить ребро vet1 - vet2\n        this.adjList.get(vet1).splice(this.adjList.get(vet1).indexOf(vet2), 1);\n        this.adjList.get(vet2).splice(this.adjList.get(vet2).indexOf(vet1), 1);\n    }\n\n    /* Добавление вершины */\n    addVertex(vet: Vertex): void {\n        if (this.adjList.has(vet)) return;\n        // Добавить новый список в список смежности\n        this.adjList.set(vet, []);\n    }\n\n    /* Удаление вершины */\n    removeVertex(vet: Vertex): void {\n        if (!this.adjList.has(vet)) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // Удалить из списка смежности список, соответствующий вершине vet\n        this.adjList.delete(vet);\n        // Обойти списки других вершин и удалить все ребра, содержащие vet\n        for (const set of this.adjList.values()) {\n            const index: number = set.indexOf(vet);\n            if (index > -1) {\n                set.splice(index, 1);\n            }\n        }\n    }\n\n    /* Вывести список смежности */\n    print(): void {\n        console.log('Список смежности =');\n        for (const [key, value] of this.adjList.entries()) {\n            const tmp = [];\n            for (const vertex of value) {\n                tmp.push(vertex.val);\n            }\n            console.log(key.val + ': ' + tmp.join());\n        }\n    }\n}\n
        graph_adjacency_list.dart
        /* Класс неориентированного графа на основе списка смежности */\nclass GraphAdjList {\n  // Список смежности, где key — вершина, а value — все смежные ей вершины\n  Map<Vertex, List<Vertex>> adjList = {};\n\n  /* Конструктор */\n  GraphAdjList(List<List<Vertex>> edges) {\n    for (List<Vertex> edge in edges) {\n      addVertex(edge[0]);\n      addVertex(edge[1]);\n      addEdge(edge[0], edge[1]);\n    }\n  }\n\n  /* Получить число вершин */\n  int size() {\n    return adjList.length;\n  }\n\n  /* Добавление ребра */\n  void addEdge(Vertex vet1, Vertex vet2) {\n    if (!adjList.containsKey(vet1) ||\n        !adjList.containsKey(vet2) ||\n        vet1 == vet2) {\n      throw ArgumentError;\n    }\n    // Добавить ребро vet1 - vet2\n    adjList[vet1]!.add(vet2);\n    adjList[vet2]!.add(vet1);\n  }\n\n  /* Удаление ребра */\n  void removeEdge(Vertex vet1, Vertex vet2) {\n    if (!adjList.containsKey(vet1) ||\n        !adjList.containsKey(vet2) ||\n        vet1 == vet2) {\n      throw ArgumentError;\n    }\n    // Удалить ребро vet1 - vet2\n    adjList[vet1]!.remove(vet2);\n    adjList[vet2]!.remove(vet1);\n  }\n\n  /* Добавление вершины */\n  void addVertex(Vertex vet) {\n    if (adjList.containsKey(vet)) return;\n    // Добавить новый список в список смежности\n    adjList[vet] = [];\n  }\n\n  /* Удаление вершины */\n  void removeVertex(Vertex vet) {\n    if (!adjList.containsKey(vet)) {\n      throw ArgumentError;\n    }\n    // Удалить из списка смежности список, соответствующий вершине vet\n    adjList.remove(vet);\n    // Обойти списки других вершин и удалить все ребра, содержащие vet\n    adjList.forEach((key, value) {\n      value.remove(vet);\n    });\n  }\n\n  /* Вывести список смежности */\n  void printAdjList() {\n    print(\"Список смежности =\");\n    adjList.forEach((key, value) {\n      List<int> tmp = [];\n      for (Vertex vertex in value) {\n        tmp.add(vertex.val);\n      }\n      print(\"${key.val}: $tmp,\");\n    });\n  }\n}\n
        graph_adjacency_list.rs
        /* Тип неориентированного графа на основе списка смежности */\npub struct GraphAdjList {\n    // Список смежности, где key — вершина, а value — все смежные ей вершины\n    pub adj_list: HashMap<Vertex, Vec<Vertex>>, // maybe HashSet<Vertex> for value part is better?\n}\n\nimpl GraphAdjList {\n    /* Конструктор */\n    pub fn new(edges: Vec<[Vertex; 2]>) -> Self {\n        let mut graph = GraphAdjList {\n            adj_list: HashMap::new(),\n        };\n        // Добавить все вершины и ребра\n        for edge in edges {\n            graph.add_vertex(edge[0]);\n            graph.add_vertex(edge[1]);\n            graph.add_edge(edge[0], edge[1]);\n        }\n\n        graph\n    }\n\n    /* Получить число вершин */\n    #[allow(unused)]\n    pub fn size(&self) -> usize {\n        self.adj_list.len()\n    }\n\n    /* Добавление ребра */\n    pub fn add_edge(&mut self, vet1: Vertex, vet2: Vertex) {\n        if vet1 == vet2 {\n            panic!(\"value error\");\n        }\n        // Добавить ребро vet1 - vet2\n        self.adj_list.entry(vet1).or_default().push(vet2);\n        self.adj_list.entry(vet2).or_default().push(vet1);\n    }\n\n    /* Удаление ребра */\n    #[allow(unused)]\n    pub fn remove_edge(&mut self, vet1: Vertex, vet2: Vertex) {\n        if vet1 == vet2 {\n            panic!(\"value error\");\n        }\n        // Удалить ребро vet1 - vet2\n        self.adj_list\n            .entry(vet1)\n            .and_modify(|v| v.retain(|&e| e != vet2));\n        self.adj_list\n            .entry(vet2)\n            .and_modify(|v| v.retain(|&e| e != vet1));\n    }\n\n    /* Добавление вершины */\n    pub fn add_vertex(&mut self, vet: Vertex) {\n        if self.adj_list.contains_key(&vet) {\n            return;\n        }\n        // Добавить новый список в список смежности\n        self.adj_list.insert(vet, vec![]);\n    }\n\n    /* Удаление вершины */\n    #[allow(unused)]\n    pub fn remove_vertex(&mut self, vet: Vertex) {\n        // Удалить из списка смежности список, соответствующий вершине vet\n        self.adj_list.remove(&vet);\n        // Обойти списки других вершин и удалить все ребра, содержащие vet\n        for list in self.adj_list.values_mut() {\n            list.retain(|&v| v != vet);\n        }\n    }\n\n    /* Вывести список смежности */\n    pub fn print(&self) {\n        println!(\"Список смежности =\");\n        for (vertex, list) in &self.adj_list {\n            let list = list.iter().map(|vertex| vertex.val).collect::<Vec<i32>>();\n            println!(\"{}: {:?},\", vertex.val, list);\n        }\n    }\n}\n
        graph_adjacency_list.c
        /* Структура узла */\ntypedef struct AdjListNode {\n    Vertex *vertex;           // Вершина\n    struct AdjListNode *next; // Узел-преемник\n} AdjListNode;\n\n/* Найти узел, соответствующий вершине */\nAdjListNode *findNode(GraphAdjList *graph, Vertex *vet) {\n    for (int i = 0; i < graph->size; i++) {\n        if (graph->heads[i]->vertex == vet) {\n            return graph->heads[i];\n        }\n    }\n    return NULL;\n}\n\n/* Вспомогательная функция добавления ребра */\nvoid addEdgeHelper(AdjListNode *head, Vertex *vet) {\n    AdjListNode *node = (AdjListNode *)malloc(sizeof(AdjListNode));\n    node->vertex = vet;\n    // Вставка в голову\n    node->next = head->next;\n    head->next = node;\n}\n\n/* Вспомогательная функция удаления ребра */\nvoid removeEdgeHelper(AdjListNode *head, Vertex *vet) {\n    AdjListNode *pre = head;\n    AdjListNode *cur = head->next;\n    // Искать в связном списке узел, соответствующий vet\n    while (cur != NULL && cur->vertex != vet) {\n        pre = cur;\n        cur = cur->next;\n    }\n    if (cur == NULL)\n        return;\n    // Удалить из связного списка узел, соответствующий vet\n    pre->next = cur->next;\n    // Освободить память\n    free(cur);\n}\n\n/* Класс неориентированного графа на основе списка смежности */\ntypedef struct {\n    AdjListNode *heads[MAX_SIZE]; // Массив узлов\n    int size;                     // Количество узлов\n} GraphAdjList;\n\n/* Конструктор */\nGraphAdjList *newGraphAdjList() {\n    GraphAdjList *graph = (GraphAdjList *)malloc(sizeof(GraphAdjList));\n    if (!graph) {\n        return NULL;\n    }\n    graph->size = 0;\n    for (int i = 0; i < MAX_SIZE; i++) {\n        graph->heads[i] = NULL;\n    }\n    return graph;\n}\n\n/* Деструктор */\nvoid delGraphAdjList(GraphAdjList *graph) {\n    for (int i = 0; i < graph->size; i++) {\n        AdjListNode *cur = graph->heads[i];\n        while (cur != NULL) {\n            AdjListNode *next = cur->next;\n            if (cur != graph->heads[i]) {\n                free(cur);\n            }\n            cur = next;\n        }\n        free(graph->heads[i]->vertex);\n        free(graph->heads[i]);\n    }\n    free(graph);\n}\n\n/* Найти узел, соответствующий вершине */\nAdjListNode *findNode(GraphAdjList *graph, Vertex *vet) {\n    for (int i = 0; i < graph->size; i++) {\n        if (graph->heads[i]->vertex == vet) {\n            return graph->heads[i];\n        }\n    }\n    return NULL;\n}\n\n/* Добавление ребра */\nvoid addEdge(GraphAdjList *graph, Vertex *vet1, Vertex *vet2) {\n    AdjListNode *head1 = findNode(graph, vet1);\n    AdjListNode *head2 = findNode(graph, vet2);\n    assert(head1 != NULL && head2 != NULL && head1 != head2);\n    // Добавить ребро vet1 - vet2\n    addEdgeHelper(head1, vet2);\n    addEdgeHelper(head2, vet1);\n}\n\n/* Удаление ребра */\nvoid removeEdge(GraphAdjList *graph, Vertex *vet1, Vertex *vet2) {\n    AdjListNode *head1 = findNode(graph, vet1);\n    AdjListNode *head2 = findNode(graph, vet2);\n    assert(head1 != NULL && head2 != NULL);\n    // Удалить ребро vet1 - vet2\n    removeEdgeHelper(head1, head2->vertex);\n    removeEdgeHelper(head2, head1->vertex);\n}\n\n/* Добавление вершины */\nvoid addVertex(GraphAdjList *graph, Vertex *vet) {\n    assert(graph != NULL && graph->size < MAX_SIZE);\n    AdjListNode *head = (AdjListNode *)malloc(sizeof(AdjListNode));\n    head->vertex = vet;\n    head->next = NULL;\n    // Добавить новый список в список смежности\n    graph->heads[graph->size++] = head;\n}\n\n/* Удаление вершины */\nvoid removeVertex(GraphAdjList *graph, Vertex *vet) {\n    AdjListNode *node = findNode(graph, vet);\n    assert(node != NULL);\n    // Удалить из списка смежности список, соответствующий вершине vet\n    AdjListNode *cur = node, *pre = NULL;\n    while (cur) {\n        pre = cur;\n        cur = cur->next;\n        free(pre);\n    }\n    // Обойти списки других вершин и удалить все ребра, содержащие vet\n    for (int i = 0; i < graph->size; i++) {\n        cur = graph->heads[i];\n        pre = NULL;\n        while (cur) {\n            pre = cur;\n            cur = cur->next;\n            if (cur && cur->vertex == vet) {\n                pre->next = cur->next;\n                free(cur);\n                break;\n            }\n        }\n    }\n    // Сдвинуть вершины после данной вперед, чтобы заполнить образовавшийся пробел\n    int i;\n    for (i = 0; i < graph->size; i++) {\n        if (graph->heads[i] == node)\n            break;\n    }\n    for (int j = i; j < graph->size - 1; j++) {\n        graph->heads[j] = graph->heads[j + 1];\n    }\n    graph->size--;\n    free(vet);\n}\n
        graph_adjacency_list.kt
        /* Класс неориентированного графа на основе списка смежности */\nclass GraphAdjList(edges: Array<Array<Vertex?>>) {\n    // Список смежности, где key — вершина, а value — все смежные ей вершины\n    val adjList = HashMap<Vertex, MutableList<Vertex>>()\n\n    /* Конструктор */\n    init {\n        // Добавить все вершины и ребра\n        for (edge in edges) {\n            addVertex(edge[0]!!)\n            addVertex(edge[1]!!)\n            addEdge(edge[0]!!, edge[1]!!)\n        }\n    }\n\n    /* Получить число вершин */\n    fun size(): Int {\n        return adjList.size\n    }\n\n    /* Добавление ребра */\n    fun addEdge(vet1: Vertex, vet2: Vertex) {\n        if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n            throw IllegalArgumentException()\n        // Добавить ребро vet1 - vet2\n        adjList[vet1]?.add(vet2)\n        adjList[vet2]?.add(vet1)\n    }\n\n    /* Удаление ребра */\n    fun removeEdge(vet1: Vertex, vet2: Vertex) {\n        if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n            throw IllegalArgumentException()\n        // Удалить ребро vet1 - vet2\n        adjList[vet1]?.remove(vet2)\n        adjList[vet2]?.remove(vet1)\n    }\n\n    /* Добавление вершины */\n    fun addVertex(vet: Vertex) {\n        if (adjList.containsKey(vet))\n            return\n        // Добавить новый список в список смежности\n        adjList[vet] = mutableListOf()\n    }\n\n    /* Удаление вершины */\n    fun removeVertex(vet: Vertex) {\n        if (!adjList.containsKey(vet))\n            throw IllegalArgumentException()\n        // Удалить из списка смежности список, соответствующий вершине vet\n        adjList.remove(vet)\n        // Обойти списки других вершин и удалить все ребра, содержащие vet\n        for (list in adjList.values) {\n            list.remove(vet)\n        }\n    }\n\n    /* Вывести список смежности */\n    fun print() {\n        println(\"Список смежности =\")\n        for (pair in adjList.entries) {\n            val tmp = mutableListOf<Int>()\n            for (vertex in pair.value) {\n                tmp.add(vertex._val)\n            }\n            println(\"${pair.key._val}: $tmp,\")\n        }\n    }\n}\n
        graph_adjacency_list.rb
        ### Класс неориентированного графа на основе списка смежности ###\nclass GraphAdjList\n  attr_reader :adj_list\n\n  ### Конструктор ###\n  def initialize(edges)\n    # Список смежности, где key — вершина, а value — все смежные ей вершины\n    @adj_list = {}\n    # Добавить все вершины и ребра\n    for edge in edges\n      add_vertex(edge[0])\n      add_vertex(edge[1])\n      add_edge(edge[0], edge[1])\n    end\n  end\n\n  ### Получение числа вершин ###\n  def size\n    @adj_list.length\n  end\n\n  ### Добавление ребра ###\n  def add_edge(vet1, vet2)\n    raise ArgumentError if !@adj_list.include?(vet1) || !@adj_list.include?(vet2)\n\n    @adj_list[vet1] << vet2\n    @adj_list[vet2] << vet1\n  end\n\n  ### Удаление ребра ###\n  def remove_edge(vet1, vet2)\n    raise ArgumentError if !@adj_list.include?(vet1) || !@adj_list.include?(vet2)\n\n    # Удалить ребро vet1 - vet2\n    @adj_list[vet1].delete(vet2)\n    @adj_list[vet2].delete(vet1)\n  end\n\n  ### Добавление вершины ###\n  def add_vertex(vet)\n    return if @adj_list.include?(vet)\n\n    # Добавить новый список в список смежности\n    @adj_list[vet] = []\n  end\n\n  ### Удаление вершины ###\n  def remove_vertex(vet)\n    raise ArgumentError unless @adj_list.include?(vet)\n\n    # Удалить из списка смежности список, соответствующий вершине vet\n    @adj_list.delete(vet)\n    # Обойти списки других вершин и удалить все ребра, содержащие vet\n    for vertex in @adj_list\n      @adj_list[vertex.first].delete(vet) if @adj_list[vertex.first].include?(vet)\n    end\n  end\n\n  ### Вывести список смежности ###\n  def __print__\n    puts 'Список смежности ='\n    for vertex in @adj_list\n      tmp = @adj_list[vertex.first].map { |v| v.val }\n      puts \"#{vertex.first.val}: #{tmp},\"\n    end\n  end\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 9. Графы","9.2   Базовые операции графа"],"tags":[]},{"location":"chapter_graph/graph_operations/#923","level":2,"title":"9.2.3   Сравнение эффективности","text":"

        Пусть в графе имеется \\(n\\) вершин и \\(m\\) ребер. В таблице 9-2 сравниваются временная и пространственная эффективность матрицы смежности и списка смежности. Обратите внимание: список смежности (связный список) соответствует реализации из этой статьи, а список смежности (хеш-таблица) означает вариант, в котором все списки заменены хеш-таблицами.

        Таблица 9-2   Сравнение матрицы смежности и списка смежности

        Матрица смежности Список смежности (связный список) Список смежности (хеш-таблица) Проверка смежности \\(O(1)\\) \\(O(n)\\) \\(O(1)\\) Добавление ребра \\(O(1)\\) \\(O(1)\\) \\(O(1)\\) Удаление ребра \\(O(1)\\) \\(O(n)\\) \\(O(1)\\) Добавление вершины \\(O(n)\\) \\(O(1)\\) \\(O(1)\\) Удаление вершины \\(O(n^2)\\) \\(O(n + m)\\) \\(O(n)\\) Занимаемая память \\(O(n^2)\\) \\(O(n + m)\\) \\(O(n + m)\\)

        Если смотреть только на таблицу, может показаться, что список смежности на основе хеш-таблицы является лучшим и по времени, и по памяти. Но на практике операции над ребрами в матрице смежности обычно выполняются быстрее, потому что там нужен лишь один доступ к массиву или одно присваивание. В целом матрица смежности воплощает принцип \"обмена пространства на время\", а список смежности - принцип \"обмена времени на пространство\".

        ","path":["Глава 9. Графы","9.2   Базовые операции графа"],"tags":[]},{"location":"chapter_graph/graph_traversal/","level":1,"title":"9.3   Обход графа","text":"

        Дерево представляет отношение \"один ко многим\", тогда как граф обладает большей свободой и может выражать произвольные отношения \"многие ко многим\". Поэтому дерево можно рассматривать как частный случай графа. Очевидно, что операции обхода дерева также являются частным случаем операций обхода графа.

        И графы, и деревья требуют применения алгоритмов обхода. Способы обхода графа также делятся на два типа: обход в ширину и обход в глубину.

        ","path":["Глава 9. Графы","9.3   Обход графа"],"tags":[]},{"location":"chapter_graph/graph_traversal/#931","level":2,"title":"9.3.1   Обход в ширину","text":"

        Обход в ширину - это способ обхода от ближнего к дальнему, при котором начиная с некоторого узла сначала посещают ближайшие вершины, а затем слой за слоем расширяются наружу. Как показано на рисунке 9-9, начиная с вершины в левом верхнем углу, мы сначала обходим все смежные вершины этой вершины, затем все смежные вершины следующей вершины и так далее, пока не будут посещены все вершины.

        Рисунок 9-9   Обход графа в ширину

        ","path":["Глава 9. Графы","9.3   Обход графа"],"tags":[]},{"location":"chapter_graph/graph_traversal/#1","level":3,"title":"1.   Реализация алгоритма","text":"

        BFS обычно реализуется с помощью очереди, код приведен ниже. Очередь обладает свойством \"первым пришел - первым вышел\", что хорошо соответствует идее BFS \"от ближнего к дальнему\".

        1. Поместить стартовую вершину обхода startVet в очередь и запустить цикл.
        2. На каждой итерации цикла извлекать вершину из головы очереди и записывать ее посещение, после чего добавлять все смежные вершины этой вершины в хвост очереди.
        3. Повторять шаг 2. до тех пор, пока не будут посещены все вершины.

        Чтобы предотвратить повторный обход вершин, нам нужно хеш-множество visited , в котором записывается, какие вершины уже посещены.

        Tip

        Хеш-множество можно рассматривать как хеш-таблицу, которая хранит только key и не хранит value . Оно позволяет выполнять добавление, удаление и проверку наличия key за \\(O(1)\\) времени. Благодаря уникальности key хеш-множество обычно используется, например, для устранения повторов.

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_bfs.py
        def graph_bfs(graph: GraphAdjList, start_vet: Vertex) -> list[Vertex]:\n    \"\"\"Обход в ширину\"\"\"\n    # Использовать список смежности для представления графа, чтобы получать все смежные вершины заданной вершины\n    # Последовательность обхода вершин\n    res = []\n    # Хеш-множество для хранения уже посещенных вершин\n    visited = set[Vertex]([start_vet])\n    # Очередь используется для реализации BFS\n    que = deque[Vertex]([start_vet])\n    # Начиная с вершины vet, продолжать цикл, пока не будут посещены все вершины\n    while len(que) > 0:\n        vet = que.popleft()  # Извлечь головную вершину из очереди\n        res.append(vet)  # Отметить посещенную вершину\n        # Обойти все смежные вершины данной вершины\n        for adj_vet in graph.adj_list[vet]:\n            if adj_vet in visited:\n                continue  # Пропустить уже посещенную вершину\n            que.append(adj_vet)  # Помещать в очередь только непосещенные вершины\n            visited.add(adj_vet)  # Отметить эту вершину как посещенную\n    # Вернуть последовательность обхода вершин\n    return res\n
        graph_bfs.cpp
        /* Обход в ширину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nvector<Vertex *> graphBFS(GraphAdjList &graph, Vertex *startVet) {\n    // Последовательность обхода вершин\n    vector<Vertex *> res;\n    // Хеш-множество для хранения уже посещенных вершин\n    unordered_set<Vertex *> visited = {startVet};\n    // Очередь используется для реализации BFS\n    queue<Vertex *> que;\n    que.push(startVet);\n    // Начиная с вершины vet, продолжать цикл, пока не будут посещены все вершины\n    while (!que.empty()) {\n        Vertex *vet = que.front();\n        que.pop();          // Извлечь головную вершину из очереди\n        res.push_back(vet); // Отметить посещенную вершину\n        // Обойти все смежные вершины данной вершины\n        for (auto adjVet : graph.adjList[vet]) {\n            if (visited.count(adjVet))\n                continue;            // Пропустить уже посещенную вершину\n            que.push(adjVet);        // Помещать в очередь только непосещенные вершины\n            visited.emplace(adjVet); // Отметить эту вершину как посещенную\n        }\n    }\n    // Вернуть последовательность обхода вершин\n    return res;\n}\n
        graph_bfs.java
        /* Обход в ширину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nList<Vertex> graphBFS(GraphAdjList graph, Vertex startVet) {\n    // Последовательность обхода вершин\n    List<Vertex> res = new ArrayList<>();\n    // Хеш-множество для хранения уже посещенных вершин\n    Set<Vertex> visited = new HashSet<>();\n    visited.add(startVet);\n    // Очередь используется для реализации BFS\n    Queue<Vertex> que = new LinkedList<>();\n    que.offer(startVet);\n    // Начиная с вершины vet, продолжать цикл, пока не будут посещены все вершины\n    while (!que.isEmpty()) {\n        Vertex vet = que.poll(); // Извлечь головную вершину из очереди\n        res.add(vet);            // Отметить посещенную вершину\n        // Обойти все смежные вершины данной вершины\n        for (Vertex adjVet : graph.adjList.get(vet)) {\n            if (visited.contains(adjVet))\n                continue;        // Пропустить уже посещенную вершину\n            que.offer(adjVet);   // Помещать в очередь только непосещенные вершины\n            visited.add(adjVet); // Отметить эту вершину как посещенную\n        }\n    }\n    // Вернуть последовательность обхода вершин\n    return res;\n}\n
        graph_bfs.cs
        /* Обход в ширину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nList<Vertex> GraphBFS(GraphAdjList graph, Vertex startVet) {\n    // Последовательность обхода вершин\n    List<Vertex> res = [];\n    // Хеш-множество для хранения уже посещенных вершин\n    HashSet<Vertex> visited = [startVet];\n    // Очередь используется для реализации BFS\n    Queue<Vertex> que = new();\n    que.Enqueue(startVet);\n    // Начиная с вершины vet, продолжать цикл, пока не будут посещены все вершины\n    while (que.Count > 0) {\n        Vertex vet = que.Dequeue(); // Извлечь головную вершину из очереди\n        res.Add(vet);               // Отметить посещенную вершину\n        foreach (Vertex adjVet in graph.adjList[vet]) {\n            if (visited.Contains(adjVet)) {\n                continue;          // Пропустить уже посещенную вершину\n            }\n            que.Enqueue(adjVet);   // Помещать в очередь только непосещенные вершины\n            visited.Add(adjVet);   // Отметить эту вершину как посещенную\n        }\n    }\n\n    // Вернуть последовательность обхода вершин\n    return res;\n}\n
        graph_bfs.go
        /* Обход в ширину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nfunc graphBFS(g *graphAdjList, startVet Vertex) []Vertex {\n    // Последовательность обхода вершин\n    res := make([]Vertex, 0)\n    // Хеш-множество для хранения уже посещенных вершин\n    visited := make(map[Vertex]struct{})\n    visited[startVet] = struct{}{}\n    // Очередь используется для реализации BFS, срез используется для имитации очереди\n    queue := make([]Vertex, 0)\n    queue = append(queue, startVet)\n    // Начиная с вершины vet, продолжать цикл, пока не будут посещены все вершины\n    for len(queue) > 0 {\n        // Извлечь головную вершину из очереди\n        vet := queue[0]\n        queue = queue[1:]\n        // Отметить посещенную вершину\n        res = append(res, vet)\n        // Обойти все смежные вершины данной вершины\n        for _, adjVet := range g.adjList[vet] {\n            _, isExist := visited[adjVet]\n            // Помещать в очередь только непосещенные вершины\n            if !isExist {\n                queue = append(queue, adjVet)\n                visited[adjVet] = struct{}{}\n            }\n        }\n    }\n    // Вернуть последовательность обхода вершин\n    return res\n}\n
        graph_bfs.swift
        /* Обход в ширину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nfunc graphBFS(graph: GraphAdjList, startVet: Vertex) -> [Vertex] {\n    // Последовательность обхода вершин\n    var res: [Vertex] = []\n    // Хеш-множество для хранения уже посещенных вершин\n    var visited: Set<Vertex> = [startVet]\n    // Очередь используется для реализации BFS\n    var que: [Vertex] = [startVet]\n    // Начиная с вершины vet, продолжать цикл, пока не будут посещены все вершины\n    while !que.isEmpty {\n        let vet = que.removeFirst() // Извлечь головную вершину из очереди\n        res.append(vet) // Отметить посещенную вершину\n        // Обойти все смежные вершины данной вершины\n        for adjVet in graph.adjList[vet] ?? [] {\n            if visited.contains(adjVet) {\n                continue // Пропустить уже посещенную вершину\n            }\n            que.append(adjVet) // Помещать в очередь только непосещенные вершины\n            visited.insert(adjVet) // Отметить эту вершину как посещенную\n        }\n    }\n    // Вернуть последовательность обхода вершин\n    return res\n}\n
        graph_bfs.js
        /* Обход в ширину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nfunction graphBFS(graph, startVet) {\n    // Последовательность обхода вершин\n    const res = [];\n    // Хеш-множество для хранения уже посещенных вершин\n    const visited = new Set();\n    visited.add(startVet);\n    // Очередь используется для реализации BFS\n    const que = [startVet];\n    // Начиная с вершины vet, продолжать цикл, пока не будут посещены все вершины\n    while (que.length) {\n        const vet = que.shift(); // Извлечь головную вершину из очереди\n        res.push(vet); // Отметить посещенную вершину\n        // Обойти все смежные вершины данной вершины\n        for (const adjVet of graph.adjList.get(vet) ?? []) {\n            if (visited.has(adjVet)) {\n                continue; // Пропустить уже посещенную вершину\n            }\n            que.push(adjVet); // Помещать в очередь только непосещенные вершины\n            visited.add(adjVet); // Отметить эту вершину как посещенную\n        }\n    }\n    // Вернуть последовательность обхода вершин\n    return res;\n}\n
        graph_bfs.ts
        /* Обход в ширину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nfunction graphBFS(graph: GraphAdjList, startVet: Vertex): Vertex[] {\n    // Последовательность обхода вершин\n    const res: Vertex[] = [];\n    // Хеш-множество для хранения уже посещенных вершин\n    const visited: Set<Vertex> = new Set();\n    visited.add(startVet);\n    // Очередь используется для реализации BFS\n    const que = [startVet];\n    // Начиная с вершины vet, продолжать цикл, пока не будут посещены все вершины\n    while (que.length) {\n        const vet = que.shift(); // Извлечь головную вершину из очереди\n        res.push(vet); // Отметить посещенную вершину\n        // Обойти все смежные вершины данной вершины\n        for (const adjVet of graph.adjList.get(vet) ?? []) {\n            if (visited.has(adjVet)) {\n                continue; // Пропустить уже посещенную вершину\n            }\n            que.push(adjVet); // Помещать в очередь только непосещенные вершины\n            visited.add(adjVet); // Отметить эту вершину как посещенную\n        }\n    }\n    // Вернуть последовательность обхода вершин\n    return res;\n}\n
        graph_bfs.dart
        /* Обход в ширину */\nList<Vertex> graphBFS(GraphAdjList graph, Vertex startVet) {\n  // Использовать список смежности для представления графа, чтобы получать все смежные вершины заданной вершины\n  // Последовательность обхода вершин\n  List<Vertex> res = [];\n  // Хеш-множество для хранения уже посещенных вершин\n  Set<Vertex> visited = {};\n  visited.add(startVet);\n  // Очередь используется для реализации BFS\n  Queue<Vertex> que = Queue();\n  que.add(startVet);\n  // Начиная с вершины vet, продолжать цикл, пока не будут посещены все вершины\n  while (que.isNotEmpty) {\n    Vertex vet = que.removeFirst(); // Извлечь головную вершину из очереди\n    res.add(vet); // Отметить посещенную вершину\n    // Обойти все смежные вершины данной вершины\n    for (Vertex adjVet in graph.adjList[vet]!) {\n      if (visited.contains(adjVet)) {\n        continue; // Пропустить уже посещенную вершину\n      }\n      que.add(adjVet); // Помещать в очередь только непосещенные вершины\n      visited.add(adjVet); // Отметить эту вершину как посещенную\n    }\n  }\n  // Вернуть последовательность обхода вершин\n  return res;\n}\n
        graph_bfs.rs
        /* Обход в ширину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nfn graph_bfs(graph: GraphAdjList, start_vet: Vertex) -> Vec<Vertex> {\n    // Последовательность обхода вершин\n    let mut res = vec![];\n    // Хеш-множество для хранения уже посещенных вершин\n    let mut visited = HashSet::new();\n    visited.insert(start_vet);\n    // Очередь используется для реализации BFS\n    let mut que = VecDeque::new();\n    que.push_back(start_vet);\n    // Начиная с вершины vet, продолжать цикл, пока не будут посещены все вершины\n    while let Some(vet) = que.pop_front() {\n        res.push(vet); // Отметить посещенную вершину\n\n        // Обойти все смежные вершины данной вершины\n        if let Some(adj_vets) = graph.adj_list.get(&vet) {\n            for &adj_vet in adj_vets {\n                if visited.contains(&adj_vet) {\n                    continue; // Пропустить уже посещенную вершину\n                }\n                que.push_back(adj_vet); // Помещать в очередь только непосещенные вершины\n                visited.insert(adj_vet); // Отметить эту вершину как посещенную\n            }\n        }\n    }\n    // Вернуть последовательность обхода вершин\n    res\n}\n
        graph_bfs.c
        /* Структура очереди узлов */\ntypedef struct {\n    Vertex *vertices[MAX_SIZE];\n    int front, rear, size;\n} Queue;\n\n/* Конструктор */\nQueue *newQueue() {\n    Queue *q = (Queue *)malloc(sizeof(Queue));\n    q->front = q->rear = q->size = 0;\n    return q;\n}\n\n/* Проверка, пуста ли очередь */\nint isEmpty(Queue *q) {\n    return q->size == 0;\n}\n\n/* Операция добавления в очередь */\nvoid enqueue(Queue *q, Vertex *vet) {\n    q->vertices[q->rear] = vet;\n    q->rear = (q->rear + 1) % MAX_SIZE;\n    q->size++;\n}\n\n/* Операция извлечения из очереди */\nVertex *dequeue(Queue *q) {\n    Vertex *vet = q->vertices[q->front];\n    q->front = (q->front + 1) % MAX_SIZE;\n    q->size--;\n    return vet;\n}\n\n/* Проверить, была ли вершина уже посещена */\nint isVisited(Vertex **visited, int size, Vertex *vet) {\n    // Искать узел обходом за O(n) времени\n    for (int i = 0; i < size; i++) {\n        if (visited[i] == vet)\n            return 1;\n    }\n    return 0;\n}\n\n/* Обход в ширину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nvoid graphBFS(GraphAdjList *graph, Vertex *startVet, Vertex **res, int *resSize, Vertex **visited, int *visitedSize) {\n    // Очередь используется для реализации BFS\n    Queue *queue = newQueue();\n    enqueue(queue, startVet);\n    visited[(*visitedSize)++] = startVet;\n    // Начиная с вершины vet, продолжать цикл, пока не будут посещены все вершины\n    while (!isEmpty(queue)) {\n        Vertex *vet = dequeue(queue); // Извлечь головную вершину из очереди\n        res[(*resSize)++] = vet;      // Отметить посещенную вершину\n        // Обойти все смежные вершины данной вершины\n        AdjListNode *node = findNode(graph, vet);\n        while (node != NULL) {\n            // Пропустить уже посещенную вершину\n            if (!isVisited(visited, *visitedSize, node->vertex)) {\n                enqueue(queue, node->vertex);             // Помещать в очередь только непосещенные вершины\n                visited[(*visitedSize)++] = node->vertex; // Отметить эту вершину как посещенную\n            }\n            node = node->next;\n        }\n    }\n    // Освободить память\n    free(queue);\n}\n
        graph_bfs.kt
        /* Обход в ширину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nfun graphBFS(graph: GraphAdjList, startVet: Vertex): MutableList<Vertex?> {\n    // Последовательность обхода вершин\n    val res = mutableListOf<Vertex?>()\n    // Хеш-множество для хранения уже посещенных вершин\n    val visited = HashSet<Vertex>()\n    visited.add(startVet)\n    // Очередь используется для реализации BFS\n    val que = LinkedList<Vertex>()\n    que.offer(startVet)\n    // Начиная с вершины vet, продолжать цикл, пока не будут посещены все вершины\n    while (!que.isEmpty()) {\n        val vet = que.poll() // Извлечь головную вершину из очереди\n        res.add(vet)         // Отметить посещенную вершину\n        // Обойти все смежные вершины данной вершины\n        for (adjVet in graph.adjList[vet]!!) {\n            if (visited.contains(adjVet))\n                continue        // Пропустить уже посещенную вершину\n            que.offer(adjVet)   // Помещать в очередь только непосещенные вершины\n            visited.add(adjVet) // Отметить эту вершину как посещенную\n        }\n    }\n    // Вернуть последовательность обхода вершин\n    return res\n}\n
        graph_bfs.rb
        ### Обход в ширину ###\ndef graph_bfs(graph, start_vet)\n  # Использовать список смежности для представления графа, чтобы получать все смежные вершины заданной вершины\n  # Последовательность обхода вершин\n  res = []\n  # Хеш-множество для хранения уже посещенных вершин\n  visited = Set.new([start_vet])\n  # Очередь используется для реализации BFS\n  que = [start_vet]\n  # Начиная с вершины vet, продолжать цикл, пока не будут посещены все вершины\n  while que.length > 0\n    vet = que.shift # Извлечь головную вершину из очереди\n    res << vet # Отметить посещенную вершину\n    # Обойти все смежные вершины данной вершины\n    for adj_vet in graph.adj_list[vet]\n      next if visited.include?(adj_vet) # Пропустить уже посещенную вершину\n      que << adj_vet # Помещать в очередь только непосещенные вершины\n      visited.add(adj_vet) # Отметить эту вершину как посещенную\n    end\n  end\n  # Вернуть последовательность обхода вершин\n  res\nend\n
        Визуализация кода

        Во весь экран >

        Код сравнительно абстрактен, поэтому рекомендуется сверяться с рисунками ниже для лучшего понимания.

        <1><2><3><4><5><6><7><8><9><10><11>

        Рисунок 9-10   Шаги обхода графа в ширину

        Является ли последовательность обхода в ширину единственной?

        Нет. Обход в ширину требует только соблюдения порядка \"от ближнего к дальнему\", а порядок обхода нескольких вершин на одинаковом расстоянии может произвольно меняться. Например, на рисунке 9-10 можно поменять местами порядок посещения вершин \\(1\\) и \\(3\\) , а вершины \\(2\\), \\(4\\), \\(6\\) также можно переставлять произвольно.

        ","path":["Глава 9. Графы","9.3   Обход графа"],"tags":[]},{"location":"chapter_graph/graph_traversal/#2","level":3,"title":"2.   Анализ сложности","text":"

        Временная сложность: все вершины по одному разу помещаются в очередь и извлекаются из нее, что требует \\(O(|V|)\\) времени; при обходе смежных вершин, поскольку граф неориентированный, все ребра будут посещены по \\(2\\) раза, что требует \\(O(2|E|)\\) времени; в сумме получается \\(O(|V| + |E|)\\) .

        Пространственная сложность: список res , хеш-множество visited и очередь que в худшем случае могут содержать до \\(|V|\\) вершин, поэтому требуется \\(O(|V|)\\) памяти.

        ","path":["Глава 9. Графы","9.3   Обход графа"],"tags":[]},{"location":"chapter_graph/graph_traversal/#932","level":2,"title":"9.3.2   Обход в глубину","text":"

        Обход в глубину - это способ обхода, при котором сначала идут до самого конца, а когда дальше идти нельзя, возвращаются назад. Как показано на рисунке 9-11, начиная с вершины в левом верхнем углу, мы выбираем некоторую смежную вершину текущей вершины, идем до упора, затем возвращаемся назад, снова идем до упора и так далее, пока не будут посещены все вершины.

        Рисунок 9-11   Обход графа в глубину

        ","path":["Глава 9. Графы","9.3   Обход графа"],"tags":[]},{"location":"chapter_graph/graph_traversal/#1_1","level":3,"title":"1.   Реализация алгоритма","text":"

        Такой алгоритмический шаблон \"дойти до конца и вернуться\" обычно реализуется через рекурсию. Подобно обходу в ширину, в обходе в глубину мы также используем хеш-множество visited для записи уже посещенных вершин и тем самым избегаем повторного посещения.

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_dfs.py
        def dfs(graph: GraphAdjList, visited: set[Vertex], res: list[Vertex], vet: Vertex):\n    \"\"\"Вспомогательная функция обхода в глубину\"\"\"\n    res.append(vet)  # Отметить посещенную вершину\n    visited.add(vet)  # Отметить эту вершину как посещенную\n    # Обойти все смежные вершины данной вершины\n    for adjVet in graph.adj_list[vet]:\n        if adjVet in visited:\n            continue  # Пропустить уже посещенную вершину\n        # Рекурсивно обходить смежные вершины\n        dfs(graph, visited, res, adjVet)\n\ndef graph_dfs(graph: GraphAdjList, start_vet: Vertex) -> list[Vertex]:\n    \"\"\"Обход в глубину\"\"\"\n    # Использовать список смежности для представления графа, чтобы получать все смежные вершины заданной вершины\n    # Последовательность обхода вершин\n    res = []\n    # Хеш-множество для хранения уже посещенных вершин\n    visited = set[Vertex]()\n    dfs(graph, visited, res, start_vet)\n    return res\n
        graph_dfs.cpp
        /* Вспомогательная функция обхода в глубину */\nvoid dfs(GraphAdjList &graph, unordered_set<Vertex *> &visited, vector<Vertex *> &res, Vertex *vet) {\n    res.push_back(vet);   // Отметить посещенную вершину\n    visited.emplace(vet); // Отметить эту вершину как посещенную\n    // Обойти все смежные вершины данной вершины\n    for (Vertex *adjVet : graph.adjList[vet]) {\n        if (visited.count(adjVet))\n            continue; // Пропустить уже посещенную вершину\n        // Рекурсивно обходить смежные вершины\n        dfs(graph, visited, res, adjVet);\n    }\n}\n\n/* Обход в глубину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nvector<Vertex *> graphDFS(GraphAdjList &graph, Vertex *startVet) {\n    // Последовательность обхода вершин\n    vector<Vertex *> res;\n    // Хеш-множество для хранения уже посещенных вершин\n    unordered_set<Vertex *> visited;\n    dfs(graph, visited, res, startVet);\n    return res;\n}\n
        graph_dfs.java
        /* Вспомогательная функция обхода в глубину */\nvoid dfs(GraphAdjList graph, Set<Vertex> visited, List<Vertex> res, Vertex vet) {\n    res.add(vet);     // Отметить посещенную вершину\n    visited.add(vet); // Отметить эту вершину как посещенную\n    // Обойти все смежные вершины данной вершины\n    for (Vertex adjVet : graph.adjList.get(vet)) {\n        if (visited.contains(adjVet))\n            continue; // Пропустить уже посещенную вершину\n        // Рекурсивно обходить смежные вершины\n        dfs(graph, visited, res, adjVet);\n    }\n}\n\n/* Обход в глубину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nList<Vertex> graphDFS(GraphAdjList graph, Vertex startVet) {\n    // Последовательность обхода вершин\n    List<Vertex> res = new ArrayList<>();\n    // Хеш-множество для хранения уже посещенных вершин\n    Set<Vertex> visited = new HashSet<>();\n    dfs(graph, visited, res, startVet);\n    return res;\n}\n
        graph_dfs.cs
        /* Вспомогательная функция обхода в глубину */\nvoid DFS(GraphAdjList graph, HashSet<Vertex> visited, List<Vertex> res, Vertex vet) {\n    res.Add(vet);     // Отметить посещенную вершину\n    visited.Add(vet); // Отметить эту вершину как посещенную\n    // Обойти все смежные вершины данной вершины\n    foreach (Vertex adjVet in graph.adjList[vet]) {\n        if (visited.Contains(adjVet)) {\n            continue; // Пропустить уже посещенную вершину\n        }\n        // Рекурсивно обходить смежные вершины\n        DFS(graph, visited, res, adjVet);\n    }\n}\n\n/* Обход в глубину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nList<Vertex> GraphDFS(GraphAdjList graph, Vertex startVet) {\n    // Последовательность обхода вершин\n    List<Vertex> res = [];\n    // Хеш-множество для хранения уже посещенных вершин\n    HashSet<Vertex> visited = [];\n    DFS(graph, visited, res, startVet);\n    return res;\n}\n
        graph_dfs.go
        /* Вспомогательная функция обхода в глубину */\nfunc dfs(g *graphAdjList, visited map[Vertex]struct{}, res *[]Vertex, vet Vertex) {\n    // Операция append возвращает новую ссылку, поэтому исходную ссылку нужно заново присвоить новому срезу\n    *res = append(*res, vet)\n    visited[vet] = struct{}{}\n    // Обойти все смежные вершины данной вершины\n    for _, adjVet := range g.adjList[vet] {\n        _, isExist := visited[adjVet]\n        // Рекурсивно обходить смежные вершины\n        if !isExist {\n            dfs(g, visited, res, adjVet)\n        }\n    }\n}\n\n/* Обход в глубину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nfunc graphDFS(g *graphAdjList, startVet Vertex) []Vertex {\n    // Последовательность обхода вершин\n    res := make([]Vertex, 0)\n    // Хеш-множество для хранения уже посещенных вершин\n    visited := make(map[Vertex]struct{})\n    dfs(g, visited, &res, startVet)\n    // Вернуть последовательность обхода вершин\n    return res\n}\n
        graph_dfs.swift
        /* Вспомогательная функция обхода в глубину */\nfunc dfs(graph: GraphAdjList, visited: inout Set<Vertex>, res: inout [Vertex], vet: Vertex) {\n    res.append(vet) // Отметить посещенную вершину\n    visited.insert(vet) // Отметить эту вершину как посещенную\n    // Обойти все смежные вершины данной вершины\n    for adjVet in graph.adjList[vet] ?? [] {\n        if visited.contains(adjVet) {\n            continue // Пропустить уже посещенную вершину\n        }\n        // Рекурсивно обходить смежные вершины\n        dfs(graph: graph, visited: &visited, res: &res, vet: adjVet)\n    }\n}\n\n/* Обход в глубину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nfunc graphDFS(graph: GraphAdjList, startVet: Vertex) -> [Vertex] {\n    // Последовательность обхода вершин\n    var res: [Vertex] = []\n    // Хеш-множество для хранения уже посещенных вершин\n    var visited: Set<Vertex> = []\n    dfs(graph: graph, visited: &visited, res: &res, vet: startVet)\n    return res\n}\n
        graph_dfs.js
        /* Обход в глубину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nfunction dfs(graph, visited, res, vet) {\n    res.push(vet); // Отметить посещенную вершину\n    visited.add(vet); // Отметить эту вершину как посещенную\n    // Обойти все смежные вершины данной вершины\n    for (const adjVet of graph.adjList.get(vet)) {\n        if (visited.has(adjVet)) {\n            continue; // Пропустить уже посещенную вершину\n        }\n        // Рекурсивно обходить смежные вершины\n        dfs(graph, visited, res, adjVet);\n    }\n}\n\n/* Обход в глубину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nfunction graphDFS(graph, startVet) {\n    // Последовательность обхода вершин\n    const res = [];\n    // Хеш-множество для хранения уже посещенных вершин\n    const visited = new Set();\n    dfs(graph, visited, res, startVet);\n    return res;\n}\n
        graph_dfs.ts
        /* Вспомогательная функция обхода в глубину */\nfunction dfs(\n    graph: GraphAdjList,\n    visited: Set<Vertex>,\n    res: Vertex[],\n    vet: Vertex\n): void {\n    res.push(vet); // Отметить посещенную вершину\n    visited.add(vet); // Отметить эту вершину как посещенную\n    // Обойти все смежные вершины данной вершины\n    for (const adjVet of graph.adjList.get(vet)) {\n        if (visited.has(adjVet)) {\n            continue; // Пропустить уже посещенную вершину\n        }\n        // Рекурсивно обходить смежные вершины\n        dfs(graph, visited, res, adjVet);\n    }\n}\n\n/* Обход в глубину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nfunction graphDFS(graph: GraphAdjList, startVet: Vertex): Vertex[] {\n    // Последовательность обхода вершин\n    const res: Vertex[] = [];\n    // Хеш-множество для хранения уже посещенных вершин\n    const visited: Set<Vertex> = new Set();\n    dfs(graph, visited, res, startVet);\n    return res;\n}\n
        graph_dfs.dart
        /* Вспомогательная функция обхода в глубину */\nvoid dfs(\n  GraphAdjList graph,\n  Set<Vertex> visited,\n  List<Vertex> res,\n  Vertex vet,\n) {\n  res.add(vet); // Отметить посещенную вершину\n  visited.add(vet); // Отметить эту вершину как посещенную\n  // Обойти все смежные вершины данной вершины\n  for (Vertex adjVet in graph.adjList[vet]!) {\n    if (visited.contains(adjVet)) {\n      continue; // Пропустить уже посещенную вершину\n    }\n    // Рекурсивно обходить смежные вершины\n    dfs(graph, visited, res, adjVet);\n  }\n}\n\n/* Обход в глубину */\nList<Vertex> graphDFS(GraphAdjList graph, Vertex startVet) {\n  // Последовательность обхода вершин\n  List<Vertex> res = [];\n  // Хеш-множество для хранения уже посещенных вершин\n  Set<Vertex> visited = {};\n  dfs(graph, visited, res, startVet);\n  return res;\n}\n
        graph_dfs.rs
        /* Вспомогательная функция обхода в глубину */\nfn dfs(graph: &GraphAdjList, visited: &mut HashSet<Vertex>, res: &mut Vec<Vertex>, vet: Vertex) {\n    res.push(vet); // Отметить посещенную вершину\n    visited.insert(vet); // Отметить эту вершину как посещенную\n                         // Обойти все смежные вершины данной вершины\n    if let Some(adj_vets) = graph.adj_list.get(&vet) {\n        for &adj_vet in adj_vets {\n            if visited.contains(&adj_vet) {\n                continue; // Пропустить уже посещенную вершину\n            }\n            // Рекурсивно обходить смежные вершины\n            dfs(graph, visited, res, adj_vet);\n        }\n    }\n}\n\n/* Обход в глубину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nfn graph_dfs(graph: GraphAdjList, start_vet: Vertex) -> Vec<Vertex> {\n    // Последовательность обхода вершин\n    let mut res = vec![];\n    // Хеш-множество для хранения уже посещенных вершин\n    let mut visited = HashSet::new();\n    dfs(&graph, &mut visited, &mut res, start_vet);\n\n    res\n}\n
        graph_dfs.c
        /* Проверить, была ли вершина уже посещена */\nint isVisited(Vertex **res, int size, Vertex *vet) {\n    // Искать узел обходом за O(n) времени\n    for (int i = 0; i < size; i++) {\n        if (res[i] == vet) {\n            return 1;\n        }\n    }\n    return 0;\n}\n\n/* Вспомогательная функция обхода в глубину */\nvoid dfs(GraphAdjList *graph, Vertex **res, int *resSize, Vertex *vet) {\n    // Отметить посещенную вершину\n    res[(*resSize)++] = vet;\n    // Обойти все смежные вершины данной вершины\n    AdjListNode *node = findNode(graph, vet);\n    while (node != NULL) {\n        // Пропустить уже посещенную вершину\n        if (!isVisited(res, *resSize, node->vertex)) {\n            // Рекурсивно обходить смежные вершины\n            dfs(graph, res, resSize, node->vertex);\n        }\n        node = node->next;\n    }\n}\n\n/* Обход в глубину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nvoid graphDFS(GraphAdjList *graph, Vertex *startVet, Vertex **res, int *resSize) {\n    dfs(graph, res, resSize, startVet);\n}\n
        graph_dfs.kt
        /* Вспомогательная функция обхода в глубину */\nfun dfs(\n    graph: GraphAdjList,\n    visited: MutableSet<Vertex?>,\n    res: MutableList<Vertex?>,\n    vet: Vertex?\n) {\n    res.add(vet)     // Отметить посещенную вершину\n    visited.add(vet) // Отметить эту вершину как посещенную\n    // Обойти все смежные вершины данной вершины\n    for (adjVet in graph.adjList[vet]!!) {\n        if (visited.contains(adjVet))\n            continue  // Пропустить уже посещенную вершину\n        // Рекурсивно обходить смежные вершины\n        dfs(graph, visited, res, adjVet)\n    }\n}\n\n/* Обход в глубину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nfun graphDFS(graph: GraphAdjList, startVet: Vertex?): MutableList<Vertex?> {\n    // Последовательность обхода вершин\n    val res = mutableListOf<Vertex?>()\n    // Хеш-множество для хранения уже посещенных вершин\n    val visited = HashSet<Vertex?>()\n    dfs(graph, visited, res, startVet)\n    return res\n}\n
        graph_dfs.rb
        ### Вспомогательная функция обхода в глубину ###\ndef dfs(graph, visited, res, vet)\n  res << vet # Отметить посещенную вершину\n  visited.add(vet) # Отметить эту вершину как посещенную\n  # Обойти все смежные вершины данной вершины\n  for adj_vet in graph.adj_list[vet]\n    next if visited.include?(adj_vet) # Пропустить уже посещенную вершину\n    # Рекурсивно обходить смежные вершины\n    dfs(graph, visited, res, adj_vet)\n  end\nend\n\n### Обход в глубину ###\ndef graph_dfs(graph, start_vet)\n  # Использовать список смежности для представления графа, чтобы получать все смежные вершины заданной вершины\n  # Последовательность обхода вершин\n  res = []\n  # Хеш-множество для хранения уже посещенных вершин\n  visited = Set.new\n  dfs(graph, visited, res, start_vet)\n  res\nend\n
        Визуализация кода

        Во весь экран >

        Алгоритмический процесс обхода в глубину показан на рисунках ниже.

        • Прямая пунктирная линия обозначает нисходящую рекурсию , то есть запуск нового рекурсивного метода для посещения новой вершины.
        • Изогнутая пунктирная линия обозначает восходящую рекурсию , то есть данный рекурсивный метод завершился и управление вернулось туда, откуда он был вызван.

        Чтобы лучше понять алгоритм, рекомендуется совместить рисунки ниже с кодом и мысленно проследить весь процесс DFS, включая моменты запуска и возврата каждого рекурсивного вызова.

        <1><2><3><4><5><6><7><8><9><10><11>

        Рисунок 9-12   Шаги обхода графа в глубину

        Является ли последовательность обхода в глубину единственной?

        Как и в случае обхода в ширину, последовательность DFS тоже не является единственной. Для заданной вершины допустимо сначала углубиться в любое направление, то есть порядок смежных вершин может быть произвольным, и все такие варианты будут корректными обходами в глубину.

        Если взять в качестве примера обход дерева, то варианты \"корень \\(\\rightarrow\\) лево \\(\\rightarrow\\) право\", \"лево \\(\\rightarrow\\) корень \\(\\rightarrow\\) право\" и \"лево \\(\\rightarrow\\) право \\(\\rightarrow\\) корень\" соответствуют прямому, симметричному и обратному обходам соответственно. Они показывают три разных приоритета обхода, но все они относятся к обходу в глубину.

        ","path":["Глава 9. Графы","9.3   Обход графа"],"tags":[]},{"location":"chapter_graph/graph_traversal/#2_1","level":3,"title":"2.   Анализ сложности","text":"

        Временная сложность: все вершины будут посещены по \\(1\\) разу, что требует \\(O(|V|)\\) времени; все ребра будут посещены по \\(2\\) раза, что требует \\(O(2|E|)\\) времени; суммарно получается \\(O(|V| + |E|)\\) .

        Пространственная сложность: число вершин в списке res и хеш-множестве visited в худшем случае достигает \\(|V|\\) , максимальная глубина рекурсии тоже равна \\(|V|\\) , поэтому требуется \\(O(|V|)\\) памяти.

        ","path":["Глава 9. Графы","9.3   Обход графа"],"tags":[]},{"location":"chapter_graph/summary/","level":1,"title":"9.4   Краткие итоги","text":"","path":["Глава 9. Графы","9.4   Краткие итоги"],"tags":[]},{"location":"chapter_graph/summary/#1","level":3,"title":"1.   Основные моменты","text":"
        • Граф состоит из вершин и ребер и может быть задан как множество вершин и множество ребер.
        • По сравнению с линейными отношениями (связный список) и отношениями разделения (дерево), сетевые отношения (граф) обладают большей свободой и потому более сложны.
        • Ребра ориентированного графа имеют направление, в связном графе любые вершины достижимы, а во взвешенном графе каждое ребро содержит переменную веса.
        • Матрица смежности использует матрицу для представления графа: каждая строка и каждый столбец соответствуют вершине, а элементы матрицы показывают, есть между двумя вершинами ребро или нет. Матрица смежности эффективна в операциях добавления, удаления, поиска и изменения, но расходует больше памяти.
        • Список смежности использует несколько списков для представления графа; \\(i\\)-й список соответствует вершине \\(i\\) и хранит все ее смежные вершины. По сравнению с матрицей смежности список смежности экономит пространство, но для поиска ребра в нем приходится обходить список, поэтому по времени он уступает.
        • Когда списки в списке смежности становятся слишком длинными, их можно преобразовать в красно-черное дерево или хеш-таблицу, чтобы повысить эффективность поиска.
        • С точки зрения алгоритмической идеи матрица смежности отражает принцип \"обмена пространства на время\", а список смежности - принцип \"обмена времени на пространство\".
        • Графы можно использовать для моделирования различных реальных систем, таких как социальные сети, линии метро и так далее.
        • Дерево является частным случаем графа, а обход дерева - частным случаем обхода графа.
        • Обход графа в ширину представляет собой способ поиска, который расширяется от ближнего к дальнему и обычно реализуется с помощью очереди.
        • Обход графа в глубину представляет собой способ поиска, который сначала идет до самого конца, а затем возвращается назад, когда путь исчерпан; обычно он реализуется на основе рекурсии.
        ","path":["Глава 9. Графы","9.4   Краткие итоги"],"tags":[]},{"location":"chapter_graph/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

        Q: Что считается путем: последовательность вершин или последовательность ребер?

        Определение в разных языковых версиях Википедии различается: в английской версии путь определяется как \"последовательность ребер\", а в русской версии - как \"последовательность вершин\". В английской версии исходная формулировка выглядит так: In graph theory, a path in a graph is a finite or infinite sequence of edges which joins a sequence of vertices.

        В этой книге путь рассматривается как последовательность ребер, а не как последовательность вершин. Причина в том, что между двумя вершинами может существовать несколько ребер, и в таком случае каждому ребру соответствует свой путь.

        Q: Есть ли в несвязном графе вершины, до которых нельзя дойти?

        В несвязном графе, начиная из некоторой вершины, по крайней мере одна вершина оказывается недостижимой. Чтобы обойти весь несвязный граф, нужно задать несколько стартовых точек и обойти все связные компоненты графа.

        Q: Есть ли требования к порядку вершин в списке \"всех вершин, соединенных с данной вершиной\" в списке смежности?

        Порядок может быть произвольным. Но на практике может понадобиться сортировка по определенному правилу, например по порядку добавления вершин или по возрастанию значений вершин; это помогает быстро находить вершины с некоторым экстремальным свойством.

        ","path":["Глава 9. Графы","9.4   Краткие итоги"],"tags":[]},{"location":"chapter_greedy/","level":1,"title":"Глава 15.   Жадность","text":"

        Abstract

        Подсолнух поворачивается к солнцу, постоянно стремясь к наилучшим условиям для роста.

        Жадная стратегия через цепочку простых выборов постепенно приводит к наилучшему ответу.

        ","path":["Глава 15. Жадность","Глава 15.   Жадность"],"tags":[]},{"location":"chapter_greedy/#_1","level":2,"title":"Содержание главы","text":"
        • 15.1   Жадный алгоритм
        • 15.2   Задача о дробном рюкзаке
        • 15.3   Задача о максимальной вместимости
        • 15.4   Задача о максимальном произведении разбиения
        • 15.5   Резюме
        ","path":["Глава 15. Жадность","Глава 15.   Жадность"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/","level":1,"title":"15.2   Задача о дробном рюкзаке","text":"

        Question

        Дано \\(n\\) предметов. Вес предмета \\(i\\) равен \\(wgt[i-1]\\), ценность равна \\(val[i-1]\\), также дан рюкзак вместимостью \\(cap\\). Каждый предмет можно выбрать только один раз, но разрешается взять лишь часть предмета, а ценность вычисляется пропорционально взятому весу. Требуется найти максимальную ценность предметов в рюкзаке при ограниченной вместимости. Пример показан на рисунке 15-3.

        Рисунок 15-3   Пример данных для задачи о дробном рюкзаке

        Задача о дробном рюкзаке в целом очень похожа на задачу о рюкзаке 0-1: состояние включает текущий предмет \\(i\\) и вместимость \\(c\\), а цель состоит в нахождении максимальной ценности при заданной вместимости рюкзака.

        Отличие в том, что здесь разрешено брать только часть предмета. Как показано на рисунке 15-4, мы можем произвольно делить предмет и вычислять соответствующую ценность пропорционально весу.

        1. Для предмета \\(i\\) его ценность на единицу веса равна \\(val[i-1] / wgt[i-1]\\), сокращенно - удельная ценность.
        2. Если взять часть предмета \\(i\\) весом \\(w\\), то ценность рюкзака увеличится на \\(w \\times val[i-1] / wgt[i-1]\\).

        Рисунок 15-4   Ценность предмета на единицу веса

        ","path":["Глава 15. Жадность","15.2   Задача о дробном рюкзаке"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/#1","level":3,"title":"1.   Определение жадной стратегии","text":"

        Максимизация общей ценности предметов в рюкзаке по сути равносильна максимизации ценности на единицу веса. Отсюда естественно выводится следующая жадная стратегия.

        1. Отсортировать предметы по убыванию удельной ценности.
        2. Перебирать все предметы и на каждом шаге жадно выбирать предмет с наибольшей удельной ценностью.
        3. Если оставшейся вместимости рюкзака недостаточно, взять часть текущего предмета, чтобы заполнить рюкзак.

        Рисунок 15-5   Жадная стратегия для задачи о дробном рюкзаке

        ","path":["Глава 15. Жадность","15.2   Задача о дробном рюкзаке"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/#2","level":3,"title":"2.   Код реализации","text":"

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

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby fractional_knapsack.py
        class Item:\n    \"\"\"Предмет\"\"\"\n\n    def __init__(self, w: int, v: int):\n        self.w = w  # Вес предмета\n        self.v = v  # Стоимость предмета\n\ndef fractional_knapsack(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"Дробный рюкзак: жадный алгоритм\"\"\"\n    # Создать список предметов с двумя свойствами: вес и стоимость\n    items = [Item(w, v) for w, v in zip(wgt, val)]\n    # Отсортировать по удельной стоимости item.v / item.w в порядке убывания\n    items.sort(key=lambda item: item.v / item.w, reverse=True)\n    # Циклический жадный выбор\n    res = 0\n    for item in items:\n        if item.w <= cap:\n            # Если оставшейся вместимости достаточно, положить в рюкзак текущий предмет целиком\n            res += item.v\n            cap -= item.w\n        else:\n            # Если оставшейся вместимости недостаточно, положить в рюкзак часть текущего предмета\n            res += (item.v / item.w) * cap\n            # Свободной вместимости больше не осталось, поэтому выйти из цикла\n            break\n    return res\n
        fractional_knapsack.cpp
        /* Предмет */\nclass Item {\n  public:\n    int w; // Вес предмета\n    int v; // Стоимость предмета\n\n    Item(int w, int v) : w(w), v(v) {\n    }\n};\n\n/* Дробный рюкзак: жадный алгоритм */\ndouble fractionalKnapsack(vector<int> &wgt, vector<int> &val, int cap) {\n    // Создать список предметов с двумя свойствами: вес и стоимость\n    vector<Item> items;\n    for (int i = 0; i < wgt.size(); i++) {\n        items.push_back(Item(wgt[i], val[i]));\n    }\n    // Отсортировать по удельной стоимости item.v / item.w в порядке убывания\n    sort(items.begin(), items.end(), [](Item &a, Item &b) { return (double)a.v / a.w > (double)b.v / b.w; });\n    // Циклический жадный выбор\n    double res = 0;\n    for (auto &item : items) {\n        if (item.w <= cap) {\n            // Если оставшейся вместимости достаточно, положить в рюкзак текущий предмет целиком\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // Если оставшейся вместимости недостаточно, положить в рюкзак часть текущего предмета\n            res += (double)item.v / item.w * cap;\n            // Свободной вместимости больше не осталось, поэтому выйти из цикла\n            break;\n        }\n    }\n    return res;\n}\n
        fractional_knapsack.java
        /* Предмет */\nclass Item {\n    int w; // Вес предмета\n    int v; // Стоимость предмета\n\n    public Item(int w, int v) {\n        this.w = w;\n        this.v = v;\n    }\n}\n\n/* Дробный рюкзак: жадный алгоритм */\ndouble fractionalKnapsack(int[] wgt, int[] val, int cap) {\n    // Создать список предметов с двумя свойствами: вес и стоимость\n    Item[] items = new Item[wgt.length];\n    for (int i = 0; i < wgt.length; i++) {\n        items[i] = new Item(wgt[i], val[i]);\n    }\n    // Отсортировать по удельной стоимости item.v / item.w в порядке убывания\n    Arrays.sort(items, Comparator.comparingDouble(item -> -((double) item.v / item.w)));\n    // Циклический жадный выбор\n    double res = 0;\n    for (Item item : items) {\n        if (item.w <= cap) {\n            // Если оставшейся вместимости достаточно, положить в рюкзак текущий предмет целиком\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // Если оставшейся вместимости недостаточно, положить в рюкзак часть текущего предмета\n            res += (double) item.v / item.w * cap;\n            // Свободной вместимости больше не осталось, поэтому выйти из цикла\n            break;\n        }\n    }\n    return res;\n}\n
        fractional_knapsack.cs
        /* Предмет */\nclass Item(int w, int v) {\n    public int w = w; // Вес предмета\n    public int v = v; // Стоимость предмета\n}\n\n/* Дробный рюкзак: жадный алгоритм */\ndouble FractionalKnapsack(int[] wgt, int[] val, int cap) {\n    // Создать список предметов с двумя свойствами: вес и стоимость\n    Item[] items = new Item[wgt.Length];\n    for (int i = 0; i < wgt.Length; i++) {\n        items[i] = new Item(wgt[i], val[i]);\n    }\n    // Отсортировать по удельной стоимости item.v / item.w в порядке убывания\n    Array.Sort(items, (x, y) => (y.v / y.w).CompareTo(x.v / x.w));\n    // Циклический жадный выбор\n    double res = 0;\n    foreach (Item item in items) {\n        if (item.w <= cap) {\n            // Если оставшейся вместимости достаточно, положить в рюкзак текущий предмет целиком\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // Если оставшейся вместимости недостаточно, положить в рюкзак часть текущего предмета\n            res += (double)item.v / item.w * cap;\n            // Свободной вместимости больше не осталось, поэтому выйти из цикла\n            break;\n        }\n    }\n    return res;\n}\n
        fractional_knapsack.go
        /* Предмет */\ntype Item struct {\n    w int // Вес предмета\n    v int // Стоимость предмета\n}\n\n/* Дробный рюкзак: жадный алгоритм */\nfunc fractionalKnapsack(wgt []int, val []int, cap int) float64 {\n    // Создать список предметов с двумя свойствами: вес и стоимость\n    items := make([]Item, len(wgt))\n    for i := 0; i < len(wgt); i++ {\n        items[i] = Item{wgt[i], val[i]}\n    }\n    // Отсортировать по удельной стоимости item.v / item.w в порядке убывания\n    sort.Slice(items, func(i, j int) bool {\n        return float64(items[i].v)/float64(items[i].w) > float64(items[j].v)/float64(items[j].w)\n    })\n    // Циклический жадный выбор\n    res := 0.0\n    for _, item := range items {\n        if item.w <= cap {\n            // Если оставшейся вместимости достаточно, положить в рюкзак текущий предмет целиком\n            res += float64(item.v)\n            cap -= item.w\n        } else {\n            // Если оставшейся вместимости недостаточно, положить в рюкзак часть текущего предмета\n            res += float64(item.v) / float64(item.w) * float64(cap)\n            // Свободной вместимости больше не осталось, поэтому выйти из цикла\n            break\n        }\n    }\n    return res\n}\n
        fractional_knapsack.swift
        /* Предмет */\nclass Item {\n    var w: Int // Вес предмета\n    var v: Int // Стоимость предмета\n\n    init(w: Int, v: Int) {\n        self.w = w\n        self.v = v\n    }\n}\n\n/* Дробный рюкзак: жадный алгоритм */\nfunc fractionalKnapsack(wgt: [Int], val: [Int], cap: Int) -> Double {\n    // Создать список предметов с двумя свойствами: вес и стоимость\n    var items = zip(wgt, val).map { Item(w: $0, v: $1) }\n    // Отсортировать по удельной стоимости item.v / item.w в порядке убывания\n    items.sort { -(Double($0.v) / Double($0.w)) < -(Double($1.v) / Double($1.w)) }\n    // Циклический жадный выбор\n    var res = 0.0\n    var cap = cap\n    for item in items {\n        if item.w <= cap {\n            // Если оставшейся вместимости достаточно, положить в рюкзак текущий предмет целиком\n            res += Double(item.v)\n            cap -= item.w\n        } else {\n            // Если оставшейся вместимости недостаточно, положить в рюкзак часть текущего предмета\n            res += Double(item.v) / Double(item.w) * Double(cap)\n            // Свободной вместимости больше не осталось, поэтому выйти из цикла\n            break\n        }\n    }\n    return res\n}\n
        fractional_knapsack.js
        /* Предмет */\nclass Item {\n    constructor(w, v) {\n        this.w = w; // Вес предмета\n        this.v = v; // Стоимость предмета\n    }\n}\n\n/* Дробный рюкзак: жадный алгоритм */\nfunction fractionalKnapsack(wgt, val, cap) {\n    // Создать список предметов с двумя свойствами: вес и стоимость\n    const items = wgt.map((w, i) => new Item(w, val[i]));\n    // Отсортировать по удельной стоимости item.v / item.w в порядке убывания\n    items.sort((a, b) => b.v / b.w - a.v / a.w);\n    // Циклический жадный выбор\n    let res = 0;\n    for (const item of items) {\n        if (item.w <= cap) {\n            // Если оставшейся вместимости достаточно, положить в рюкзак текущий предмет целиком\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // Если оставшейся вместимости недостаточно, положить в рюкзак часть текущего предмета\n            res += (item.v / item.w) * cap;\n            // Свободной вместимости больше не осталось, поэтому выйти из цикла\n            break;\n        }\n    }\n    return res;\n}\n
        fractional_knapsack.ts
        /* Предмет */\nclass Item {\n    w: number; // Вес предмета\n    v: number; // Стоимость предмета\n\n    constructor(w: number, v: number) {\n        this.w = w;\n        this.v = v;\n    }\n}\n\n/* Дробный рюкзак: жадный алгоритм */\nfunction fractionalKnapsack(wgt: number[], val: number[], cap: number): number {\n    // Создать список предметов с двумя свойствами: вес и стоимость\n    const items: Item[] = wgt.map((w, i) => new Item(w, val[i]));\n    // Отсортировать по удельной стоимости item.v / item.w в порядке убывания\n    items.sort((a, b) => b.v / b.w - a.v / a.w);\n    // Циклический жадный выбор\n    let res = 0;\n    for (const item of items) {\n        if (item.w <= cap) {\n            // Если оставшейся вместимости достаточно, положить в рюкзак текущий предмет целиком\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // Если оставшейся вместимости недостаточно, положить в рюкзак часть текущего предмета\n            res += (item.v / item.w) * cap;\n            // Свободной вместимости больше не осталось, поэтому выйти из цикла\n            break;\n        }\n    }\n    return res;\n}\n
        fractional_knapsack.dart
        /* Предмет */\nclass Item {\n  int w; // Вес предмета\n  int v; // Стоимость предмета\n\n  Item(this.w, this.v);\n}\n\n/* Дробный рюкзак: жадный алгоритм */\ndouble fractionalKnapsack(List<int> wgt, List<int> val, int cap) {\n  // Создать список предметов с двумя свойствами: вес и стоимость\n  List<Item> items = List.generate(wgt.length, (i) => Item(wgt[i], val[i]));\n  // Отсортировать по удельной стоимости item.v / item.w в порядке убывания\n  items.sort((a, b) => (b.v / b.w).compareTo(a.v / a.w));\n  // Циклический жадный выбор\n  double res = 0;\n  for (Item item in items) {\n    if (item.w <= cap) {\n      // Если оставшейся вместимости достаточно, положить в рюкзак текущий предмет целиком\n      res += item.v;\n      cap -= item.w;\n    } else {\n      // Если оставшейся вместимости недостаточно, положить в рюкзак часть текущего предмета\n      res += item.v / item.w * cap;\n      // Свободной вместимости больше не осталось, поэтому выйти из цикла\n      break;\n    }\n  }\n  return res;\n}\n
        fractional_knapsack.rs
        /* Предмет */\nstruct Item {\n    w: i32, // Вес предмета\n    v: i32, // Стоимость предмета\n}\n\nimpl Item {\n    fn new(w: i32, v: i32) -> Self {\n        Self { w, v }\n    }\n}\n\n/* Дробный рюкзак: жадный алгоритм */\nfn fractional_knapsack(wgt: &[i32], val: &[i32], mut cap: i32) -> f64 {\n    // Создать список предметов с двумя свойствами: вес и стоимость\n    let mut items = wgt\n        .iter()\n        .zip(val.iter())\n        .map(|(&w, &v)| Item::new(w, v))\n        .collect::<Vec<Item>>();\n    // Отсортировать по удельной стоимости item.v / item.w в порядке убывания\n    items.sort_by(|a, b| {\n        (b.v as f64 / b.w as f64)\n            .partial_cmp(&(a.v as f64 / a.w as f64))\n            .unwrap()\n    });\n    // Циклический жадный выбор\n    let mut res = 0.0;\n    for item in &items {\n        if item.w <= cap {\n            // Если оставшейся вместимости достаточно, положить в рюкзак текущий предмет целиком\n            res += item.v as f64;\n            cap -= item.w;\n        } else {\n            // Если оставшейся вместимости недостаточно, положить в рюкзак часть текущего предмета\n            res += item.v as f64 / item.w as f64 * cap as f64;\n            // Свободной вместимости больше не осталось, поэтому выйти из цикла\n            break;\n        }\n    }\n    res\n}\n
        fractional_knapsack.c
        /* Предмет */\ntypedef struct {\n    int w; // Вес предмета\n    int v; // Стоимость предмета\n} Item;\n\n/* Дробный рюкзак: жадный алгоритм */\nfloat fractionalKnapsack(int wgt[], int val[], int itemCount, int cap) {\n    // Создать список предметов с двумя свойствами: вес и стоимость\n    Item *items = malloc(sizeof(Item) * itemCount);\n    for (int i = 0; i < itemCount; i++) {\n        items[i] = (Item){.w = wgt[i], .v = val[i]};\n    }\n    // Отсортировать по удельной стоимости item.v / item.w в порядке убывания\n    qsort(items, (size_t)itemCount, sizeof(Item), sortByValueDensity);\n    // Циклический жадный выбор\n    float res = 0.0;\n    for (int i = 0; i < itemCount; i++) {\n        if (items[i].w <= cap) {\n            // Если оставшейся вместимости достаточно, положить в рюкзак текущий предмет целиком\n            res += items[i].v;\n            cap -= items[i].w;\n        } else {\n            // Если оставшейся вместимости недостаточно, положить в рюкзак часть текущего предмета\n            res += (float)cap / items[i].w * items[i].v;\n            cap = 0;\n            break;\n        }\n    }\n    free(items);\n    return res;\n}\n
        fractional_knapsack.kt
        /* Предмет */\nclass Item(\n    val w: Int, // Предмет\n    val v: Int  // Стоимость предмета\n)\n\n/* Дробный рюкзак: жадный алгоритм */\nfun fractionalKnapsack(wgt: IntArray, _val: IntArray, c: Int): Double {\n    // Создать список предметов с двумя свойствами: вес и стоимость\n    var cap = c\n    val items = arrayOfNulls<Item>(wgt.size)\n    for (i in wgt.indices) {\n        items[i] = Item(wgt[i], _val[i])\n    }\n    // Отсортировать по удельной стоимости item.v / item.w в порядке убывания\n    items.sortBy { item: Item? -> -(item!!.v.toDouble() / item.w) }\n    // Циклический жадный выбор\n    var res = 0.0\n    for (item in items) {\n        if (item!!.w <= cap) {\n            // Если оставшейся вместимости достаточно, положить в рюкзак текущий предмет целиком\n            res += item.v\n            cap -= item.w\n        } else {\n            // Если оставшейся вместимости недостаточно, положить в рюкзак часть текущего предмета\n            res += item.v.toDouble() / item.w * cap\n            // Свободной вместимости больше не осталось, поэтому выйти из цикла\n            break\n        }\n    }\n    return res\n}\n
        fractional_knapsack.rb
        ### Предмет ###\nclass Item\n  attr_accessor :w # Вес предмета\n  attr_accessor :v # Стоимость предмета\n\n  def initialize(w, v)\n    @w = w\n    @v = v\n  end\nend\n\n### Дробный рюкзак: жадный алгоритм ###\ndef fractional_knapsack(wgt, val, cap)\n  # Создать список предметов с двумя свойствами: вес и стоимость\n  items = wgt.each_with_index.map { |w, i| Item.new(w, val[i]) }\n  # Отсортировать по удельной стоимости item.v / item.w в порядке убывания\n  items.sort! { |a, b| (b.v.to_f / b.w) <=> (a.v.to_f / a.w) }\n  # Циклический жадный выбор\n  res = 0\n  for item in items\n    if item.w <= cap\n      # Если оставшейся вместимости достаточно, положить в рюкзак текущий предмет целиком\n      res += item.v\n      cap -= item.w\n    else\n      # Если оставшейся вместимости недостаточно, положить в рюкзак часть текущего предмета\n      res += (item.v.to_f / item.w) * cap\n      # Свободной вместимости больше не осталось, поэтому выйти из цикла\n      break\n    end\n  end\n  res\nend\n
        Визуализация кода

        Во весь экран >

        Встроенный алгоритм сортировки обычно имеет временную сложность \\(O(n \\log n)\\), а пространственная сложность обычно равна \\(O(\\log n)\\) или \\(O(n)\\), в зависимости от конкретной реализации в языке программирования.

        Помимо сортировки, в худшем случае потребуется пройти весь список предметов, но это не меняет асимптотику, поэтому итоговая временная сложность равна \\(O(n \\log n)\\), где \\(n\\) - число предметов.

        Поскольку инициализируется список объектов Item, пространственная сложность равна \\(O(n)\\).

        ","path":["Глава 15. Жадность","15.2   Задача о дробном рюкзаке"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/#3","level":3,"title":"3.   Доказательство корректности","text":"

        Используем доказательство от противного. Предположим, что предмет \\(x\\) имеет наибольшую удельную ценность, некоторый алгоритм получил максимальную ценность res, но в найденном решении предмет \\(x\\) отсутствует.

        Теперь вынем из рюкзака произвольный предмет единичного веса и заменим его на предмет \\(x\\) того же веса. Поскольку предмет \\(x\\) имеет наибольшую удельную ценность, общая ценность после замены обязательно станет больше res. Это противоречит тому, что res является оптимальным решением, а значит оптимальное решение обязательно содержит предмет \\(x\\).

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

        Как показано на рисунке 15-6, если рассматривать вес предметов и их удельную ценность как горизонтальную и вертикальную оси двумерной диаграммы, то задачу о дробном рюкзаке можно интерпретировать как «поиск максимальной площади, ограниченной конечным отрезком по горизонтали». Эта аналогия помогает понять корректность жадной стратегии с геометрической точки зрения.

        Рисунок 15-6   Геометрическая интерпретация задачи о дробном рюкзаке

        ","path":["Глава 15. Жадность","15.2   Задача о дробном рюкзаке"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/","level":1,"title":"15.1   Жадный алгоритм","text":"

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

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

        • Динамическое программирование при получении текущего решения учитывает все предыдущие решения и использует ответы для прошлых подзадач, чтобы построить ответ для текущей подзадачи.
        • Жадный алгоритм не учитывает предыдущие решения, а просто движется вперед, каждый раз делая жадный выбор и постепенно сужая область задачи, пока она не будет решена.

        Чтобы лучше понять принцип работы жадного алгоритма, разберем его на примере задачи «размен монет». Эта задача уже встречалась в разделе «задача о полном рюкзаке», поэтому она наверняка вам знакома.

        Question

        Дано \\(n\\) видов монет. Номинал монеты \\(i\\) равен \\(coins[i - 1]\\), целевая сумма равна \\(amt\\), причем каждую монету можно брать неограниченное число раз. Требуется найти минимальное число монет, которыми можно набрать целевую сумму. Если набрать сумму невозможно, верните \\(-1\\).

        Жадная стратегия для этой задачи показана на рисунке 15-1. Для заданной целевой суммы мы жадно выбираем монету, которая не превышает ее и находится к ней ближе всего, и повторяем этот шаг, пока не получим нужную сумму.

        Рисунок 15-1   Жадная стратегия для задачи о размене монет

        Ниже приведен код реализации.

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change_greedy.py
        def coin_change_greedy(coins: list[int], amt: int) -> int:\n    \"\"\"Размен монет: жадный алгоритм\"\"\"\n    # Предположить, что список coins упорядочен\n    i = len(coins) - 1\n    count = 0\n    # Циклически выполнять жадный выбор, пока не останется суммы\n    while amt > 0:\n        # Найти монету, которая меньше остатка суммы и наиболее к нему близка\n        while i > 0 and coins[i] > amt:\n            i -= 1\n        # Выбрать coins[i]\n        amt -= coins[i]\n        count += 1\n    # Если допустимое решение не найдено, вернуть -1\n    return count if amt == 0 else -1\n
        coin_change_greedy.cpp
        /* Размен монет: жадный алгоритм */\nint coinChangeGreedy(vector<int> &coins, int amt) {\n    // Предположить, что список coins упорядочен\n    int i = coins.size() - 1;\n    int count = 0;\n    // Циклически выполнять жадный выбор, пока не останется суммы\n    while (amt > 0) {\n        // Найти монету, которая меньше остатка суммы и наиболее к нему близка\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // Выбрать coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // Если допустимое решение не найдено, вернуть -1\n    return amt == 0 ? count : -1;\n}\n
        coin_change_greedy.java
        /* Размен монет: жадный алгоритм */\nint coinChangeGreedy(int[] coins, int amt) {\n    // Предположить, что список coins упорядочен\n    int i = coins.length - 1;\n    int count = 0;\n    // Циклически выполнять жадный выбор, пока не останется суммы\n    while (amt > 0) {\n        // Найти монету, которая меньше остатка суммы и наиболее к нему близка\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // Выбрать coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // Если допустимое решение не найдено, вернуть -1\n    return amt == 0 ? count : -1;\n}\n
        coin_change_greedy.cs
        /* Размен монет: жадный алгоритм */\nint CoinChangeGreedy(int[] coins, int amt) {\n    // Предположить, что список coins упорядочен\n    int i = coins.Length - 1;\n    int count = 0;\n    // Циклически выполнять жадный выбор, пока не останется суммы\n    while (amt > 0) {\n        // Найти монету, которая меньше остатка суммы и наиболее к нему близка\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // Выбрать coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // Если допустимое решение не найдено, вернуть -1\n    return amt == 0 ? count : -1;\n}\n
        coin_change_greedy.go
        /* Размен монет: жадный алгоритм */\nfunc coinChangeGreedy(coins []int, amt int) int {\n    // Предположить, что список coins упорядочен\n    i := len(coins) - 1\n    count := 0\n    // Циклически выполнять жадный выбор, пока не останется суммы\n    for amt > 0 {\n        // Найти монету, которая меньше остатка суммы и наиболее к нему близка\n        for i > 0 && coins[i] > amt {\n            i--\n        }\n        // Выбрать coins[i]\n        amt -= coins[i]\n        count++\n    }\n    // Если допустимое решение не найдено, вернуть -1\n    if amt != 0 {\n        return -1\n    }\n    return count\n}\n
        coin_change_greedy.swift
        /* Размен монет: жадный алгоритм */\nfunc coinChangeGreedy(coins: [Int], amt: Int) -> Int {\n    // Предположить, что список coins упорядочен\n    var i = coins.count - 1\n    var count = 0\n    var amt = amt\n    // Циклически выполнять жадный выбор, пока не останется суммы\n    while amt > 0 {\n        // Найти монету, которая меньше остатка суммы и наиболее к нему близка\n        while i > 0 && coins[i] > amt {\n            i -= 1\n        }\n        // Выбрать coins[i]\n        amt -= coins[i]\n        count += 1\n    }\n    // Если допустимое решение не найдено, вернуть -1\n    return amt == 0 ? count : -1\n}\n
        coin_change_greedy.js
        /* Размен монет: жадный алгоритм */\nfunction coinChangeGreedy(coins, amt) {\n    // Предположить, что массив coins упорядочен\n    let i = coins.length - 1;\n    let count = 0;\n    // Циклически выполнять жадный выбор, пока не останется суммы\n    while (amt > 0) {\n        // Найти монету, которая меньше остатка суммы и наиболее к нему близка\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // Выбрать coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // Если допустимое решение не найдено, вернуть -1\n    return amt === 0 ? count : -1;\n}\n
        coin_change_greedy.ts
        /* Размен монет: жадный алгоритм */\nfunction coinChangeGreedy(coins: number[], amt: number): number {\n    // Предположить, что массив coins упорядочен\n    let i = coins.length - 1;\n    let count = 0;\n    // Циклически выполнять жадный выбор, пока не останется суммы\n    while (amt > 0) {\n        // Найти монету, которая меньше остатка суммы и наиболее к нему близка\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // Выбрать coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // Если допустимое решение не найдено, вернуть -1\n    return amt === 0 ? count : -1;\n}\n
        coin_change_greedy.dart
        /* Размен монет: жадный алгоритм */\nint coinChangeGreedy(List<int> coins, int amt) {\n  // Предположить, что список coins упорядочен\n  int i = coins.length - 1;\n  int count = 0;\n  // Циклически выполнять жадный выбор, пока не останется суммы\n  while (amt > 0) {\n    // Найти монету, которая меньше остатка суммы и наиболее к нему близка\n    while (i > 0 && coins[i] > amt) {\n      i--;\n    }\n    // Выбрать coins[i]\n    amt -= coins[i];\n    count++;\n  }\n  // Если допустимое решение не найдено, вернуть -1\n  return amt == 0 ? count : -1;\n}\n
        coin_change_greedy.rs
        /* Размен монет: жадный алгоритм */\nfn coin_change_greedy(coins: &[i32], mut amt: i32) -> i32 {\n    // Предположить, что список coins упорядочен\n    let mut i = coins.len() - 1;\n    let mut count = 0;\n    // Циклически выполнять жадный выбор, пока не останется суммы\n    while amt > 0 {\n        // Найти монету, которая меньше остатка суммы и наиболее к нему близка\n        while i > 0 && coins[i] > amt {\n            i -= 1;\n        }\n        // Выбрать coins[i]\n        amt -= coins[i];\n        count += 1;\n    }\n    // Если допустимое решение не найдено, вернуть -1\n    if amt == 0 {\n        count\n    } else {\n        -1\n    }\n}\n
        coin_change_greedy.c
        /* Размен монет: жадный алгоритм */\nint coinChangeGreedy(int *coins, int size, int amt) {\n    // Предположить, что список coins упорядочен\n    int i = size - 1;\n    int count = 0;\n    // Циклически выполнять жадный выбор, пока не останется суммы\n    while (amt > 0) {\n        // Найти монету, которая меньше остатка суммы и наиболее к нему близка\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // Выбрать coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // Если допустимое решение не найдено, вернуть -1\n    return amt == 0 ? count : -1;\n}\n
        coin_change_greedy.kt
        /* Размен монет: жадный алгоритм */\nfun coinChangeGreedy(coins: IntArray, amt: Int): Int {\n    // Предположить, что список coins упорядочен\n    var am = amt\n    var i = coins.size - 1\n    var count = 0\n    // Циклически выполнять жадный выбор, пока не останется суммы\n    while (am > 0) {\n        // Найти монету, которая меньше остатка суммы и наиболее к нему близка\n        while (i > 0 && coins[i] > am) {\n            i--\n        }\n        // Выбрать coins[i]\n        am -= coins[i]\n        count++\n    }\n    // Если допустимое решение не найдено, вернуть -1\n    return if (am == 0) count else -1\n}\n
        coin_change_greedy.rb
        ### Размен монет: жадный алгоритм ###\ndef coin_change_greedy(coins, amt)\n  # Предположить, что список coins упорядочен\n  i = coins.length - 1\n  count = 0\n  # Циклически выполнять жадный выбор, пока не останется суммы\n  while amt > 0\n    # Найти монету, которая меньше остатка суммы и наиболее к нему близка\n    while i > 0 && coins[i] > amt\n      i -= 1\n    end\n    # Выбрать coins[i]\n    amt -= coins[i]\n    count += 1\n  end\n  # Если допустимое решение не найдено, вернуть -1\n  amt == 0 ? count : -1\nend\n
        Визуализация кода

        Во весь экран >

        У вас может невольно вырваться: «Эврика!» Жадный алгоритм решает задачу размена монет всего примерно десятью строками кода.

        ","path":["Глава 15. Жадность","15.1   Жадный алгоритм"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1511","level":2,"title":"15.1.1   Преимущества и ограничения жадного алгоритма","text":"

        Жадный алгоритм не только прост в реализации, но и обычно очень эффективен. В приведенном выше коде обозначим минимальный номинал монеты через \\(\\min(coins)\\), тогда жадный выбор выполняется не более чем \\(amt / \\min(coins)\\) раз, а временная сложность равна \\(O(amt / \\min(coins))\\). Это на порядок меньше, чем временная сложность решения через динамическое программирование \\(O(n \\times amt)\\).

        Однако для некоторых наборов номиналов монет жадный алгоритм не может найти оптимальный ответ. Ниже показаны два примера.

        • Положительный пример \\(coins = [1, 5, 10, 20, 50, 100]\\): для такого набора монет при любом \\(amt\\) жадный алгоритм находит оптимальное решение.
        • Отрицательный пример \\(coins = [1, 20, 50]\\): пусть \\(amt = 60\\). Жадный алгоритм найдет только комбинацию \\(50 + 1 \\times 10\\), то есть всего \\(11\\) монет, тогда как динамическое программирование находит оптимум \\(20 + 20 + 20\\), где требуется лишь \\(3\\) монеты.
        • Отрицательный пример \\(coins = [1, 49, 50]\\): пусть \\(amt = 98\\). Жадный алгоритм найдет только комбинацию \\(50 + 1 \\times 48\\), то есть всего \\(49\\) монет, тогда как динамическое программирование находит оптимум \\(49 + 49\\), где требуется лишь \\(2\\) монеты.

        Рисунок 15-2   Примеры, где жадный алгоритм не находит оптимального решения

        Иными словами, в задаче о размене монет жадный алгоритм не гарантирует нахождение глобально оптимального решения и иногда может приводить к очень плохому ответу. Для этой задачи больше подходит динамическое программирование.

        В общем случае жадный алгоритм применим в двух следующих ситуациях.

        1. Можно гарантировать нахождение оптимального решения: в таком случае жадный алгоритм часто является лучшим выбором, поскольку обычно он эффективнее, чем поиск с возвратом и динамическое программирование.
        2. Можно найти приближенно оптимальное решение: в таком случае жадный алгоритм тоже полезен. Для многих сложных задач поиск глобального оптимума очень труден, и возможность быстро найти субоптимальный ответ уже весьма ценна.
        ","path":["Глава 15. Жадность","15.1   Жадный алгоритм"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1512","level":2,"title":"15.1.2   Свойства жадного алгоритма","text":"

        Тогда возникает вопрос: какие задачи подходят для решения жадным алгоритмом? Или, другими словами, в каких случаях жадный алгоритм может гарантировать оптимальный ответ?

        По сравнению с динамическим программированием условия применения жадного алгоритма более строгие. В основном нас интересуют два свойства задачи.

        • Свойство жадного выбора: только когда локально оптимальный выбор всегда может привести к глобально оптимальному решению, жадный алгоритм способен гарантировать оптимум.
        • Оптимальная подструктура: оптимальное решение исходной задачи содержит оптимальные решения подзадач.

        Оптимальная подструктура уже обсуждалась в главе «Динамическое программирование», поэтому здесь не будем повторяться. Стоит отметить, что у некоторых задач оптимальная подструктура не столь очевидна, но их все равно можно решать жадным алгоритмом.

        Основное внимание мы уделяем тому, как определить свойство жадного выбора. Хотя формулировка выглядит довольно простой, на практике для многих задач доказать свойство жадного выбора совсем не легко.

        Например, в задаче о размене монет легко привести контрпример и опровергнуть свойство жадного выбора, но вот доказать его истинность намного сложнее. Если спросить: для каких наборов монет можно использовать жадный алгоритм? - обычно удается дать лишь интуитивный или примерный ответ, а не строгое математическое доказательство.

        Quote

        Существует статья, в которой приводится алгоритм со временной сложностью \\(O(n^3)\\) для определения того, можно ли с помощью жадного алгоритма находить оптимальный размен для любой суммы в заданной системе монет.

        Pearson, D. A polynomial-time algorithm for the change-making problem[J]. Operations Research Letters, 2005, 33(3): 231-234.

        ","path":["Глава 15. Жадность","15.1   Жадный алгоритм"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1513","level":2,"title":"15.1.3   Этапы решения задач жадным алгоритмом","text":"

        Процесс решения жадной задачи в общем виде можно разбить на три шага.

        1. Анализ задачи: разобраться в свойствах задачи, включая определение состояний, целевой функции и ограничений. Этот этап присутствует и в поиске с возвратом, и в динамическом программировании.
        2. Определение жадной стратегии: определить, какой жадный выбор следует делать на каждом шаге. Эта стратегия должна уменьшать размер задачи на каждом этапе и в итоге привести к решению всей задачи.
        3. Доказательство корректности: обычно требуется доказать, что задача обладает свойством жадного выбора и оптимальной подструктурой. На этом этапе может понадобиться математическое доказательство, например индукция или доказательство от противного.

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

        • Жадные стратегии для разных задач сильно различаются. Для многих задач стратегия довольно очевидна, и до нее можно дойти за счет общих рассуждений и нескольких проб. Но в более сложных задачах жадная стратегия может быть очень скрытой, и тут уже многое зависит от опыта решения задач и алгоритмической подготовки.
        • Некоторые жадные стратегии выглядят убедительно, но оказываются обманчивыми. Бывает, что мы с уверенностью придумали жадную стратегию, написали код и отправили его на проверку, а часть тестов не проходит. Причина в том, что спроектированная стратегия лишь «частично верна», и описанная выше задача о размене монет - типичный пример.

        Чтобы гарантировать корректность, нужно дать строгое математическое доказательство жадной стратегии, обычно с использованием доказательства от противного или математической индукции.

        Однако и доказательство корректности может оказаться непростой задачей. Если идей нет, мы обычно начинаем отлаживать код на тестовых примерах, постепенно меняя и проверяя жадную стратегию.

        ","path":["Глава 15. Жадность","15.1   Жадный алгоритм"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1514","level":2,"title":"15.1.4   Типичные задачи для жадного алгоритма","text":"

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

        • Задача о размене монет: при некоторых системах монет жадный алгоритм всегда дает оптимальный ответ.
        • Задача о расписании интервалов: пусть есть несколько задач, каждая выполняется в некотором временном интервале, и требуется завершить как можно больше задач. Если каждый раз выбирать задачу с самым ранним временем окончания, то жадный алгоритм дает оптимальный ответ.
        • Задача о дробном рюкзаке: дана группа предметов и грузоподъемность. Требуется выбрать предметы так, чтобы их общий вес не превышал ограничение, а общая ценность была максимальной. Если каждый раз выбирать предмет с наилучшим отношением стоимости к весу, то в некоторых случаях жадный алгоритм дает оптимальный ответ.
        • Задача о покупке и продаже акций: дана история цен акции. Можно совершать несколько сделок, но если акция уже куплена, то до продажи покупать снова нельзя. Цель - получить максимальную прибыль.
        • Код Хаффмана: это жадный алгоритм для сжатия данных без потерь. Построив дерево Хаффмана и каждый раз объединяя два узла с наименьшей частотой, мы получаем дерево с минимальной взвешенной длиной пути, то есть минимальной длиной кодирования.
        • Алгоритм Дейкстры: это жадный алгоритм решения задачи о кратчайших путях от заданной исходной вершины до всех остальных вершин.
        ","path":["Глава 15. Жадность","15.1   Жадный алгоритм"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/","level":1,"title":"15.3   Задача о максимальной вместимости","text":"

        Question

        Дан массив \\(ht\\), где каждый элемент обозначает высоту вертикальной перегородки. Любые две перегородки в массиве вместе с пространством между ними образуют контейнер.

        Вместимость контейнера равна произведению высоты и ширины (площади), где высота определяется более короткой перегородкой, а ширина - разностью индексов двух перегородок в массиве.

        Требуется выбрать две перегородки так, чтобы образованный ими контейнер имел максимальную вместимость. Пример показан на рисунке 15-7.

        Рисунок 15-7   Пример данных для задачи о максимальной вместимости

        Контейнер образуется произвольными двумя перегородками, поэтому состоянием задачи служит пара индексов этих перегородок, обозначим ее как \\([i, j]\\).

        Согласно условию, вместимость равна произведению высоты на ширину, где высота определяется короткой перегородкой, а ширина - разностью индексов двух перегородок. Обозначим вместимость через \\(cap[i, j]\\), тогда формула принимает вид:

        \\[ cap[i, j] = \\min(ht[i], ht[j]) \\times (j - i) \\]

        Пусть длина массива равна \\(n\\). Тогда число пар перегородок, то есть общее число состояний, равно \\(C_n^2 = \\frac{n(n - 1)}{2}\\). Самый прямолинейный подход - перебрать все состояния, после чего найти максимальную вместимость. Его временная сложность равна \\(O(n^2)\\).

        ","path":["Глава 15. Жадность","15.3   Задача о максимальной вместимости"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/#1","level":3,"title":"1.   Определение жадной стратегии","text":"

        У этой задачи есть и более эффективное решение. Как показано на рисунке 15-8, рассмотрим состояние \\([i, j]\\), где индексы удовлетворяют \\(i < j\\), а высоты - условию \\(ht[i] < ht[j]\\), то есть \\(i\\) - короткая перегородка, а \\(j\\) - длинная.

        Рисунок 15-8   Начальное состояние

        Как показано на рисунке 15-9, если в этот момент сдвинуть длинную перегородку \\(j\\) ближе к короткой перегородке \\(i\\), то вместимость обязательно уменьшится.

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

        Рисунок 15-9   Состояние после перемещения длинной перегородки внутрь

        Рассуждая в обратную сторону, только сдвигая короткую перегородку \\(i\\) внутрь, мы можем получить шанс увеличить вместимость. Хотя ширина при этом обязательно уменьшится, высота может возрасти (если после перемещения короткая перегородка \\(i\\) станет выше). Например, на рисунке 15-10 после перемещения короткой перегородки площадь увеличивается.

        Рисунок 15-10   Состояние после перемещения короткой перегородки внутрь

        Отсюда и выводится жадная стратегия для этой задачи: инициализировать два указателя по краям контейнера и на каждом шаге сдвигать внутрь указатель, соответствующий короткой перегородке, пока указатели не встретятся.

        На рисунках ниже показан процесс выполнения этой жадной стратегии.

        1. В начальном состоянии указатели \\(i\\) и \\(j\\) стоят на двух концах массива.
        2. Вычислить вместимость текущего состояния \\(cap[i, j]\\) и обновить максимальную вместимость.
        3. Сравнить высоты перегородок \\(i\\) и \\(j\\), после чего сдвинуть короткую перегородку на одну позицию внутрь.
        4. Повторять шаги 2. и 3. до тех пор, пока \\(i\\) и \\(j\\) не встретятся.
        <1><2><3><4><5><6><7><8><9>

        Рисунок 15-11   Жадный процесс решения задачи о максимальной вместимости

        ","path":["Глава 15. Жадность","15.3   Задача о максимальной вместимости"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/#2","level":3,"title":"2.   Код реализации","text":"

        Цикл в коде выполняется не более \\(n\\) раз, поэтому временная сложность равна \\(O(n)\\).

        Переменные \\(i\\), \\(j\\), \\(res\\) используют дополнительную память постоянного размера, поэтому пространственная сложность равна \\(O(1)\\).

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby max_capacity.py
        def max_capacity(ht: list[int]) -> int:\n    \"\"\"Максимальная вместимость: жадный алгоритм\"\"\"\n    # Инициализировать i и j так, чтобы они располагались по двум концам массива\n    i, j = 0, len(ht) - 1\n    # Начальная максимальная вместимость равна 0\n    res = 0\n    # Выполнять жадный выбор в цикле, пока две доски не встретятся\n    while i < j:\n        # Обновить максимальную вместимость\n        cap = min(ht[i], ht[j]) * (j - i)\n        res = max(res, cap)\n        # Сдвигать внутрь более короткую сторону\n        if ht[i] < ht[j]:\n            i += 1\n        else:\n            j -= 1\n    return res\n
        max_capacity.cpp
        /* Максимальная вместимость: жадный алгоритм */\nint maxCapacity(vector<int> &ht) {\n    // Инициализировать i и j так, чтобы они располагались по двум концам массива\n    int i = 0, j = ht.size() - 1;\n    // Начальная максимальная вместимость равна 0\n    int res = 0;\n    // Выполнять жадный выбор в цикле, пока две доски не встретятся\n    while (i < j) {\n        // Обновить максимальную вместимость\n        int cap = min(ht[i], ht[j]) * (j - i);\n        res = max(res, cap);\n        // Сдвигать внутрь более короткую сторону\n        if (ht[i] < ht[j]) {\n            i++;\n        } else {\n            j--;\n        }\n    }\n    return res;\n}\n
        max_capacity.java
        /* Максимальная вместимость: жадный алгоритм */\nint maxCapacity(int[] ht) {\n    // Инициализировать i и j так, чтобы они располагались по двум концам массива\n    int i = 0, j = ht.length - 1;\n    // Начальная максимальная вместимость равна 0\n    int res = 0;\n    // Выполнять жадный выбор в цикле, пока две доски не встретятся\n    while (i < j) {\n        // Обновить максимальную вместимость\n        int cap = Math.min(ht[i], ht[j]) * (j - i);\n        res = Math.max(res, cap);\n        // Сдвигать внутрь более короткую сторону\n        if (ht[i] < ht[j]) {\n            i++;\n        } else {\n            j--;\n        }\n    }\n    return res;\n}\n
        max_capacity.cs
        /* Максимальная вместимость: жадный алгоритм */\nint MaxCapacity(int[] ht) {\n    // Инициализировать i и j так, чтобы они располагались по двум концам массива\n    int i = 0, j = ht.Length - 1;\n    // Начальная максимальная вместимость равна 0\n    int res = 0;\n    // Выполнять жадный выбор в цикле, пока две доски не встретятся\n    while (i < j) {\n        // Обновить максимальную вместимость\n        int cap = Math.Min(ht[i], ht[j]) * (j - i);\n        res = Math.Max(res, cap);\n        // Сдвигать внутрь более короткую сторону\n        if (ht[i] < ht[j]) {\n            i++;\n        } else {\n            j--;\n        }\n    }\n    return res;\n}\n
        max_capacity.go
        /* Максимальная вместимость: жадный алгоритм */\nfunc maxCapacity(ht []int) int {\n    // Инициализировать i и j так, чтобы они располагались по двум концам массива\n    i, j := 0, len(ht)-1\n    // Начальная максимальная вместимость равна 0\n    res := 0\n    // Выполнять жадный выбор в цикле, пока две доски не встретятся\n    for i < j {\n        // Обновить максимальную вместимость\n        capacity := int(math.Min(float64(ht[i]), float64(ht[j]))) * (j - i)\n        res = int(math.Max(float64(res), float64(capacity)))\n        // Сдвигать внутрь более короткую сторону\n        if ht[i] < ht[j] {\n            i++\n        } else {\n            j--\n        }\n    }\n    return res\n}\n
        max_capacity.swift
        /* Максимальная вместимость: жадный алгоритм */\nfunc maxCapacity(ht: [Int]) -> Int {\n    // Инициализировать i и j так, чтобы они располагались по двум концам массива\n    var i = ht.startIndex, j = ht.endIndex - 1\n    // Начальная максимальная вместимость равна 0\n    var res = 0\n    // Выполнять жадный выбор в цикле, пока две доски не встретятся\n    while i < j {\n        // Обновить максимальную вместимость\n        let cap = min(ht[i], ht[j]) * (j - i)\n        res = max(res, cap)\n        // Сдвигать внутрь более короткую сторону\n        if ht[i] < ht[j] {\n            i += 1\n        } else {\n            j -= 1\n        }\n    }\n    return res\n}\n
        max_capacity.js
        /* Максимальная вместимость: жадный алгоритм */\nfunction maxCapacity(ht) {\n    // Инициализировать i и j так, чтобы они располагались по двум концам массива\n    let i = 0,\n        j = ht.length - 1;\n    // Начальная максимальная вместимость равна 0\n    let res = 0;\n    // Выполнять жадный выбор в цикле, пока две доски не встретятся\n    while (i < j) {\n        // Обновить максимальную вместимость\n        const cap = Math.min(ht[i], ht[j]) * (j - i);\n        res = Math.max(res, cap);\n        // Сдвигать внутрь более короткую сторону\n        if (ht[i] < ht[j]) {\n            i += 1;\n        } else {\n            j -= 1;\n        }\n    }\n    return res;\n}\n
        max_capacity.ts
        /* Максимальная вместимость: жадный алгоритм */\nfunction maxCapacity(ht: number[]): number {\n    // Инициализировать i и j так, чтобы они располагались по двум концам массива\n    let i = 0,\n        j = ht.length - 1;\n    // Начальная максимальная вместимость равна 0\n    let res = 0;\n    // Выполнять жадный выбор в цикле, пока две доски не встретятся\n    while (i < j) {\n        // Обновить максимальную вместимость\n        const cap: number = Math.min(ht[i], ht[j]) * (j - i);\n        res = Math.max(res, cap);\n        // Сдвигать внутрь более короткую сторону\n        if (ht[i] < ht[j]) {\n            i += 1;\n        } else {\n            j -= 1;\n        }\n    }\n    return res;\n}\n
        max_capacity.dart
        /* Максимальная вместимость: жадный алгоритм */\nint maxCapacity(List<int> ht) {\n  // Инициализировать i и j так, чтобы они располагались по двум концам массива\n  int i = 0, j = ht.length - 1;\n  // Начальная максимальная вместимость равна 0\n  int res = 0;\n  // Выполнять жадный выбор в цикле, пока две доски не встретятся\n  while (i < j) {\n    // Обновить максимальную вместимость\n    int cap = min(ht[i], ht[j]) * (j - i);\n    res = max(res, cap);\n    // Сдвигать внутрь более короткую сторону\n    if (ht[i] < ht[j]) {\n      i++;\n    } else {\n      j--;\n    }\n  }\n  return res;\n}\n
        max_capacity.rs
        /* Максимальная вместимость: жадный алгоритм */\nfn max_capacity(ht: &[i32]) -> i32 {\n    // Инициализировать i и j так, чтобы они располагались по двум концам массива\n    let mut i = 0;\n    let mut j = ht.len() - 1;\n    // Начальная максимальная вместимость равна 0\n    let mut res = 0;\n    // Выполнять жадный выбор в цикле, пока две доски не встретятся\n    while i < j {\n        // Обновить максимальную вместимость\n        let cap = std::cmp::min(ht[i], ht[j]) * (j - i) as i32;\n        res = std::cmp::max(res, cap);\n        // Сдвигать внутрь более короткую сторону\n        if ht[i] < ht[j] {\n            i += 1;\n        } else {\n            j -= 1;\n        }\n    }\n    res\n}\n
        max_capacity.c
        /* Максимальная вместимость: жадный алгоритм */\nint maxCapacity(int ht[], int htLength) {\n    // Инициализировать i и j так, чтобы они располагались по двум концам массива\n    int i = 0;\n    int j = htLength - 1;\n    // Начальная максимальная вместимость равна 0\n    int res = 0;\n    // Выполнять жадный выбор в цикле, пока две доски не встретятся\n    while (i < j) {\n        // Обновить максимальную вместимость\n        int capacity = myMin(ht[i], ht[j]) * (j - i);\n        res = myMax(res, capacity);\n        // Сдвигать внутрь более короткую сторону\n        if (ht[i] < ht[j]) {\n            i++;\n        } else {\n            j--;\n        }\n    }\n    return res;\n}\n
        max_capacity.kt
        /* Максимальная вместимость: жадный алгоритм */\nfun maxCapacity(ht: IntArray): Int {\n    // Инициализировать i и j так, чтобы они располагались по двум концам массива\n    var i = 0\n    var j = ht.size - 1\n    // Начальная максимальная вместимость равна 0\n    var res = 0\n    // Выполнять жадный выбор в цикле, пока две доски не встретятся\n    while (i < j) {\n        // Обновить максимальную вместимость\n        val cap = min(ht[i], ht[j]) * (j - i)\n        res = max(res, cap)\n        // Сдвигать внутрь более короткую сторону\n        if (ht[i] < ht[j]) {\n            i++\n        } else {\n            j--\n        }\n    }\n    return res\n}\n
        max_capacity.rb
        ### Максимальная вместимость: жадный алгоритм ###\ndef max_capacity(ht)\n  # Инициализировать i и j так, чтобы они располагались по двум концам массива\n  i, j = 0, ht.length - 1\n  # Начальная максимальная вместимость равна 0\n  res = 0\n\n  # Выполнять жадный выбор в цикле, пока две доски не встретятся\n  while i < j\n    # Обновить максимальную вместимость\n    cap = [ht[i], ht[j]].min * (j - i)\n    res = [res, cap].max\n    # Сдвигать внутрь более короткую сторону\n    if ht[i] < ht[j]\n      i += 1\n    else\n      j -= 1\n    end\n  end\n\n  res\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 15. Жадность","15.3   Задача о максимальной вместимости"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/#3","level":3,"title":"3.   Доказательство корректности","text":"

        Жадный алгоритм быстрее полного перебора именно потому, что каждый жадный шаг «пропускает» часть состояний.

        Например, в состоянии \\(cap[i, j]\\) перегородка \\(i\\) является короткой, а \\(j\\) - длинной. Если жадно сдвинуть короткую перегородку \\(i\\) на одну позицию внутрь, то состояния, показанные на рисунке 15-12, будут «пропущены». Это означает, что позже мы уже не сможем проверить вместимость этих состояний.

        \\[ cap[i, i+1], cap[i, i+2], \\dots, cap[i, j-2], cap[i, j-1] \\]

        Рисунок 15-12   Состояния, пропущенные из-за смещения короткой перегородки

        Нетрудно заметить, что эти пропущенные состояния на самом деле и есть все состояния, в которых длинная перегородка \\(j\\) сдвигается внутрь. Ранее мы уже доказали, что перемещение длинной перегородки внутрь обязательно уменьшает вместимость. Иными словами, пропущенные состояния не могут быть оптимальным решением, поэтому их пропуск не приводит к потере оптимума.

        Приведенный анализ показывает, что операция перемещения короткой перегородки является «безопасной», а жадная стратегия действительно корректна.

        ","path":["Глава 15. Жадность","15.3   Задача о максимальной вместимости"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/","level":1,"title":"15.4   Задача о максимальном произведении разбиения","text":"

        Question

        Дан положительный целый \\(n\\). Требуется разложить его в сумму как минимум двух положительных целых чисел и найти максимально возможное произведение всех полученных чисел, как показано на рисунке 15-13.

        Рисунок 15-13   Определение задачи о максимальном произведении разбиения

        Предположим, что мы разбили \\(n\\) на \\(m\\) целочисленных множителей, где \\(i\\)-й множитель обозначим через \\(n_i\\), то есть

        \\[ n = \\sum_{i=1}^{m}n_i \\]

        Цель задачи - найти максимальное произведение всех целочисленных множителей, то есть

        \\[ \\max(\\prod_{i=1}^{m}n_i) \\]

        Нужно понять: каким должно быть число частей \\(m\\) и какими должны быть значения каждого \\(n_i\\)?

        ","path":["Глава 15. Жадность","15.4   Задача о максимальном произведении разбиения"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/#1","level":3,"title":"1.   Определение жадной стратегии","text":"

        Из опыта известно, что произведение двух целых чисел часто больше их суммы. Предположим, что мы выделяем из \\(n\\) множитель \\(2\\), тогда произведение равно \\(2(n-2)\\). Сравним это выражение с \\(n\\):

        \\[ \\begin{aligned} 2(n-2) & \\geq n \\newline 2n - n - 4 & \\geq 0 \\newline n & \\geq 4 \\end{aligned} \\]

        Как показано на рисунке 15-14, когда \\(n \\geq 4\\), выделение множителя \\(2\\) увеличивает произведение. Это означает, что все целые числа, большие либо равные \\(4\\), следует продолжать разбивать.

        Жадная стратегия 1: если в схеме разбиения присутствует множитель \\(\\geq 4\\), то его нужно дальше разбивать. В конечной схеме разбиения должны остаться только множители \\(1\\), \\(2\\), \\(3\\).

        Рисунок 15-14   Разбиение увеличивает произведение

        Теперь подумаем, какой множитель является наилучшим. Среди \\(1\\), \\(2\\), \\(3\\) очевидно худшим является \\(1\\), потому что всегда выполняется \\(1 \\times (n-1) < n\\), то есть выделение \\(1\\) уменьшает произведение.

        Как показано на рисунке 15-15, при \\(n = 6\\) имеем \\(3 \\times 3 > 2 \\times 2 \\times 2\\). Это означает, что выделять \\(3\\) выгоднее, чем выделять \\(2\\).

        Жадная стратегия 2: в схеме разбиения должно быть не более двух множителей \\(2\\). Потому что три двойки всегда можно заменить двумя тройками и получить большее произведение.

        Рисунок 15-15   Оптимальные множители разбиения

        Итак, получаем следующую жадную стратегию.

        1. Для заданного целого \\(n\\) непрерывно выделять из него множитель \\(3\\), пока остаток не станет равным \\(0\\), \\(1\\) или \\(2\\).
        2. Если остаток равен \\(0\\), это означает, что \\(n\\) кратно \\(3\\), и больше ничего делать не нужно.
        3. Если остаток равен \\(2\\), дальнейшее разбиение не требуется, его нужно сохранить.
        4. Если остаток равен \\(1\\), то поскольку \\(2 \\times 2 > 1 \\times 3\\), последний множитель \\(3\\) следует заменить на \\(2\\).
        ","path":["Глава 15. Жадность","15.4   Задача о максимальном произведении разбиения"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/#2","level":3,"title":"2.   Код реализации","text":"

        Как показано на рисунке 15-16, нам не нужен цикл, чтобы выполнять разбиение числа. Можно использовать целочисленное деление, чтобы получить число троек \\(a\\), и операцию взятия остатка, чтобы получить остаток \\(b\\). Тогда имеем:

        \\[ n = 3 a + b \\]

        Обратите внимание, что для граничного случая \\(n \\leq 3\\) необходимо выделить множитель \\(1\\), и тогда произведение равно \\(1 \\times (n - 1)\\).

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby max_product_cutting.py
        def max_product_cutting(n: int) -> int:\n    \"\"\"Максимальное произведение разрезания: жадный алгоритм\"\"\"\n    # Когда n <= 3, обязательно нужно выделить одну 1\n    if n <= 3:\n        return 1 * (n - 1)\n    # Жадно выделить множители 3, где a — число троек, а b — остаток\n    a, b = n // 3, n % 3\n    if b == 1:\n        # Если остаток равен 1, преобразовать одну пару 1 * 3 в 2 * 2\n        return int(math.pow(3, a - 1)) * 2 * 2\n    if b == 2:\n        # Если остаток равен 2, ничего не делать\n        return int(math.pow(3, a)) * 2\n    # Если остаток равен 0, ничего не делать\n    return int(math.pow(3, a))\n
        max_product_cutting.cpp
        /* Максимальное произведение разрезания: жадный алгоритм */\nint maxProductCutting(int n) {\n    // Когда n <= 3, обязательно нужно выделить одну 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // Жадно выделить множители 3, где a — число троек, а b — остаток\n    int a = n / 3;\n    int b = n % 3;\n    if (b == 1) {\n        // Если остаток равен 1, преобразовать одну пару 1 * 3 в 2 * 2\n        return (int)pow(3, a - 1) * 2 * 2;\n    }\n    if (b == 2) {\n        // Если остаток равен 2, ничего не делать\n        return (int)pow(3, a) * 2;\n    }\n    // Если остаток равен 0, ничего не делать\n    return (int)pow(3, a);\n}\n
        max_product_cutting.java
        /* Максимальное произведение разрезания: жадный алгоритм */\nint maxProductCutting(int n) {\n    // Когда n <= 3, обязательно нужно выделить одну 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // Жадно выделить множители 3, где a — число троек, а b — остаток\n    int a = n / 3;\n    int b = n % 3;\n    if (b == 1) {\n        // Если остаток равен 1, преобразовать одну пару 1 * 3 в 2 * 2\n        return (int) Math.pow(3, a - 1) * 2 * 2;\n    }\n    if (b == 2) {\n        // Если остаток равен 2, ничего не делать\n        return (int) Math.pow(3, a) * 2;\n    }\n    // Если остаток равен 0, ничего не делать\n    return (int) Math.pow(3, a);\n}\n
        max_product_cutting.cs
        /* Максимальное произведение разрезания: жадный алгоритм */\nint MaxProductCutting(int n) {\n    // Когда n <= 3, обязательно нужно выделить одну 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // Жадно выделить множители 3, где a — число троек, а b — остаток\n    int a = n / 3;\n    int b = n % 3;\n    if (b == 1) {\n        // Если остаток равен 1, преобразовать одну пару 1 * 3 в 2 * 2\n        return (int)Math.Pow(3, a - 1) * 2 * 2;\n    }\n    if (b == 2) {\n        // Если остаток равен 2, ничего не делать\n        return (int)Math.Pow(3, a) * 2;\n    }\n    // Если остаток равен 0, ничего не делать\n    return (int)Math.Pow(3, a);\n}\n
        max_product_cutting.go
        /* Максимальное произведение разрезания: жадный алгоритм */\nfunc maxProductCutting(n int) int {\n    // Когда n <= 3, обязательно нужно выделить одну 1\n    if n <= 3 {\n        return 1 * (n - 1)\n    }\n    // Жадно выделить множители 3, где a — число троек, а b — остаток\n    a := n / 3\n    b := n % 3\n    if b == 1 {\n        // Если остаток равен 1, преобразовать одну пару 1 * 3 в 2 * 2\n        return int(math.Pow(3, float64(a-1))) * 2 * 2\n    }\n    if b == 2 {\n        // Если остаток равен 2, ничего не делать\n        return int(math.Pow(3, float64(a))) * 2\n    }\n    // Если остаток равен 0, ничего не делать\n    return int(math.Pow(3, float64(a)))\n}\n
        max_product_cutting.swift
        /* Максимальное произведение разрезания: жадный алгоритм */\nfunc maxProductCutting(n: Int) -> Int {\n    // Когда n <= 3, обязательно нужно выделить одну 1\n    if n <= 3 {\n        return 1 * (n - 1)\n    }\n    // Жадно выделить множители 3, где a — число троек, а b — остаток\n    let a = n / 3\n    let b = n % 3\n    if b == 1 {\n        // Если остаток равен 1, преобразовать одну пару 1 * 3 в 2 * 2\n        return pow(3, a - 1) * 2 * 2\n    }\n    if b == 2 {\n        // Если остаток равен 2, ничего не делать\n        return pow(3, a) * 2\n    }\n    // Если остаток равен 0, ничего не делать\n    return pow(3, a)\n}\n
        max_product_cutting.js
        /* Максимальное произведение разрезания: жадный алгоритм */\nfunction maxProductCutting(n) {\n    // Когда n <= 3, обязательно нужно выделить одну 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // Жадно выделить множители 3, где a — число троек, а b — остаток\n    let a = Math.floor(n / 3);\n    let b = n % 3;\n    if (b === 1) {\n        // Если остаток равен 1, преобразовать одну пару 1 * 3 в 2 * 2\n        return Math.pow(3, a - 1) * 2 * 2;\n    }\n    if (b === 2) {\n        // Если остаток равен 2, ничего не делать\n        return Math.pow(3, a) * 2;\n    }\n    // Если остаток равен 0, ничего не делать\n    return Math.pow(3, a);\n}\n
        max_product_cutting.ts
        /* Максимальное произведение разрезания: жадный алгоритм */\nfunction maxProductCutting(n: number): number {\n    // Когда n <= 3, обязательно нужно выделить одну 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // Жадно выделить множители 3, где a — число троек, а b — остаток\n    let a: number = Math.floor(n / 3);\n    let b: number = n % 3;\n    if (b === 1) {\n        // Если остаток равен 1, преобразовать одну пару 1 * 3 в 2 * 2\n        return Math.pow(3, a - 1) * 2 * 2;\n    }\n    if (b === 2) {\n        // Если остаток равен 2, ничего не делать\n        return Math.pow(3, a) * 2;\n    }\n    // Если остаток равен 0, ничего не делать\n    return Math.pow(3, a);\n}\n
        max_product_cutting.dart
        /* Максимальное произведение разрезания: жадный алгоритм */\nint maxProductCutting(int n) {\n  // Когда n <= 3, обязательно нужно выделить одну 1\n  if (n <= 3) {\n    return 1 * (n - 1);\n  }\n  // Жадно выделить множители 3, где a — число троек, а b — остаток\n  int a = n ~/ 3;\n  int b = n % 3;\n  if (b == 1) {\n    // Если остаток равен 1, преобразовать одну пару 1 * 3 в 2 * 2\n    return (pow(3, a - 1) * 2 * 2).toInt();\n  }\n  if (b == 2) {\n    // Если остаток равен 2, ничего не делать\n    return (pow(3, a) * 2).toInt();\n  }\n  // Если остаток равен 0, ничего не делать\n  return pow(3, a).toInt();\n}\n
        max_product_cutting.rs
        /* Максимальное произведение разрезания: жадный алгоритм */\nfn max_product_cutting(n: i32) -> i32 {\n    // Когда n <= 3, обязательно нужно выделить одну 1\n    if n <= 3 {\n        return 1 * (n - 1);\n    }\n    // Жадно выделить множители 3, где a — число троек, а b — остаток\n    let a = n / 3;\n    let b = n % 3;\n    if b == 1 {\n        // Если остаток равен 1, преобразовать одну пару 1 * 3 в 2 * 2\n        3_i32.pow(a as u32 - 1) * 2 * 2\n    } else if b == 2 {\n        // Если остаток равен 2, ничего не делать\n        3_i32.pow(a as u32) * 2\n    } else {\n        // Если остаток равен 0, ничего не делать\n        3_i32.pow(a as u32)\n    }\n}\n
        max_product_cutting.c
        /* Максимальное произведение разрезания: жадный алгоритм */\nint maxProductCutting(int n) {\n    // Когда n <= 3, обязательно нужно выделить одну 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // Жадно выделить множители 3, где a — число троек, а b — остаток\n    int a = n / 3;\n    int b = n % 3;\n    if (b == 1) {\n        // Если остаток равен 1, преобразовать одну пару 1 * 3 в 2 * 2\n        return pow(3, a - 1) * 2 * 2;\n    }\n    if (b == 2) {\n        // Если остаток равен 2, ничего не делать\n        return pow(3, a) * 2;\n    }\n    // Если остаток равен 0, ничего не делать\n    return pow(3, a);\n}\n
        max_product_cutting.kt
        /* Максимальное произведение разрезания: жадный алгоритм */\nfun maxProductCutting(n: Int): Int {\n    // Когда n <= 3, обязательно нужно выделить одну 1\n    if (n <= 3) {\n        return 1 * (n - 1)\n    }\n    // Жадно выделить множители 3, где a — число троек, а b — остаток\n    val a = n / 3\n    val b = n % 3\n    if (b == 1) {\n        // Если остаток равен 1, преобразовать одну пару 1 * 3 в 2 * 2\n        return 3.0.pow((a - 1)).toInt() * 2 * 2\n    }\n    if (b == 2) {\n        // Если остаток равен 2, ничего не делать\n        return 3.0.pow(a).toInt() * 2 * 2\n    }\n    // Если остаток равен 0, ничего не делать\n    return 3.0.pow(a).toInt()\n}\n
        max_product_cutting.rb
        ### Максимальное произведение разрезания: жадный алгоритм ###\ndef max_product_cutting(n)\n  # Когда n <= 3, обязательно нужно выделить одну 1\n  return 1 * (n - 1) if n <= 3\n  # Жадно выделить множители 3, где a — число троек, а b — остаток\n  a, b = n / 3, n % 3\n  # Если остаток равен 1, преобразовать одну пару 1 * 3 в 2 * 2\n  return (3.pow(a - 1) * 2 * 2).to_i if b == 1\n  # Если остаток равен 2, ничего не делать\n  return (3.pow(a) * 2).to_i if b == 2\n  # Если остаток равен 0, ничего не делать\n  3.pow(a).to_i\nend\n
        Визуализация кода

        Во весь экран >

        Рисунок 15-16   Метод вычисления максимального произведения разбиения

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

        • Оператор ** и функция pow() имеют временную сложность \\(O(\\log⁡ a)\\).
        • Функция math.pow() внутри вызывает функцию pow() из библиотеки C, выполняющую возведение в степень с плавающей точкой, и ее временная сложность равна \\(O(1)\\).

        Переменные \\(a\\) и \\(b\\) занимают дополнительную память постоянного размера, поэтому пространственная сложность равна \\(O(1)\\).

        ","path":["Глава 15. Жадность","15.4   Задача о максимальном произведении разбиения"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/#3","level":3,"title":"3.   Доказательство корректности","text":"

        Используем доказательство от противного и рассмотрим только случай \\(n \\geq 4\\).

        1. Все множители \\(\\leq 3\\): предположим, что в оптимальной схеме разбиения существует множитель \\(x \\geq 4\\). Тогда его можно дальше разложить в \\(2(x-2)\\) и получить большее или равное произведение. Это противоречит предположению.
        2. Схема разбиения не содержит \\(1\\): предположим, что в оптимальной схеме присутствует множитель \\(1\\). Тогда его можно объединить с другим множителем и получить большее произведение. Это противоречит предположению.
        3. Схема разбиения содержит не более двух \\(2\\): предположим, что в оптимальной схеме присутствуют три двойки. Тогда их можно заменить двумя тройками и получить большее произведение. Это противоречит предположению.
        ","path":["Глава 15. Жадность","15.4   Задача о максимальном произведении разбиения"],"tags":[]},{"location":"chapter_greedy/summary/","level":1,"title":"15.5   Резюме","text":"","path":["Глава 15. Жадность","15.5   Резюме"],"tags":[]},{"location":"chapter_greedy/summary/#1","level":3,"title":"1.   Ключевые моменты","text":"
        • Жадный алгоритм обычно используется для решения задач оптимизации. Его принцип состоит в том, чтобы на каждом этапе принятия решения делать локально оптимальный выбор в надежде получить глобально оптимальный ответ.
        • Жадный алгоритм итеративно делает один жадный выбор за другим, на каждом шаге превращая задачу в подзадачу меньшего размера, пока задача не будет полностью решена.
        • Жадный алгоритм не только прост в реализации, но и часто обладает высокой эффективностью. По сравнению с динамическим программированием его временная сложность обычно ниже.
        • В задаче о размене монет для некоторых наборов монет жадный алгоритм способен гарантировать оптимальный ответ, а для других наборов - нет: он может дать очень плохое решение.
        • Задачи, подходящие для жадного алгоритма, обладают двумя ключевыми свойствами: свойством жадного выбора и оптимальной подструктурой. Свойство жадного выбора отражает корректность жадной стратегии.
        • Для некоторых сложных задач доказать свойство жадного выбора непросто. Относительно легче найти контрпример и опровергнуть его, как это видно на примере задачи о размене монет.
        • Решение жадной задачи обычно состоит из трех шагов: анализ задачи, определение жадной стратегии и доказательство корректности. Из них ключевым является выбор жадной стратегии, а доказательство корректности часто оказывается самым трудным.
        • В задаче о дробном рюкзаке, в отличие от задачи о рюкзаке 0-1, разрешено брать часть предмета, поэтому ее можно решать жадным алгоритмом. Корректность жадной стратегии доказывается методом от противного.
        • Задачу о максимальной вместимости можно решать полным перебором со временной сложностью \\(O(n^2)\\). Разработав жадную стратегию со сдвигом короткой перегородки внутрь на каждом шаге, временную сложность можно оптимизировать до \\(O(n)\\).
        • В задаче о максимальном произведении разбиения мы последовательно выводим две жадные стратегии: все целые числа \\(\\geq 4\\) следует дальше разбивать, а оптимальным множителем разбиения является \\(3\\). В коде присутствуют операции возведения в степень, поэтому временная сложность зависит от способа их реализации и обычно равна \\(O(1)\\) или \\(O(\\log n)\\).
        ","path":["Глава 15. Жадность","15.5   Резюме"],"tags":[]},{"location":"chapter_hashing/","level":1,"title":"Глава 6.   Хеш-таблицы","text":"

        Abstract

        Хеш-таблица устанавливает соответствие между ключом и значением.

        Благодаря этому она позволяет получать нужное значение по ключу за очень короткое время.

        ","path":["Глава 6. Хеш-таблицы","Глава 6.   Хеш-таблицы"],"tags":[]},{"location":"chapter_hashing/#_1","level":2,"title":"Содержание главы","text":"
        • 6.1   Хеш-таблица
        • 6.2   Хеш-коллизии
        • 6.3   Алгоритмы хеширования
        • 6.4   Резюме
        ","path":["Глава 6. Хеш-таблицы","Глава 6.   Хеш-таблицы"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/","level":1,"title":"6.3   Алгоритмы хеширования","text":"

        В двух предыдущих разделах мы рассмотрели принципы работы хеш-таблицы и способы обработки хеш-коллизий. Однако и открытая адресация, и метод цепочек лишь позволяют хеш-таблице корректно работать при возникновении коллизий, но не уменьшают вероятность появления самих коллизий.

        Если хеш-коллизии происходят слишком часто, производительность хеш-таблицы резко деградирует. Как показано на рисунке 6-8, для хеш-таблицы с методом цепочек в идеальном случае пары ключ-значение равномерно распределены по всем бакетам, и это дает наилучшую эффективность поиска; в худшем же случае все пары ключ-значение оказываются в одном бакете, и временная сложность вырождается до \\(O(n)\\) .

        Рисунок 6-8   Лучший и худший случаи хеш-коллизий

        Распределение пар ключ-значение определяется хеш-функцией. Вспомним этапы вычисления хеш-функции: сначала вычисляется хеш-значение, затем оно берется по модулю длины массива:

        index = hash(key) % capacity\n

        Из этой формулы видно: при фиксированной емкости хеш-таблицы capacity **выходное значение определяет именно хеш-алгоритм hash() **, а значит, именно он определяет распределение пар ключ-значение в хеш-таблице.

        Это означает, что для уменьшения вероятности хеш-коллизий нам следует сосредоточиться на проектировании хеш-алгоритма hash() .

        ","path":["Глава 6. Хеш-таблицы","6.3   Алгоритмы хеширования"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#631-","level":2,"title":"6.3.1   Цели хеш-алгоритма","text":"

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

        • Детерминированность: для одинакового входа хеш-алгоритм всегда должен выдавать одинаковый результат. Только так хеш-таблица остается надежной.
        • Высокая эффективность: вычисление хеш-значения должно быть достаточно быстрым. Чем меньше вычислительные затраты, тем выше практическая ценность хеш-таблицы.
        • Равномерное распределение: хеш-алгоритм должен стараться распределять пары ключ-значение в хеш-таблице равномерно. Чем равномернее распределение, тем ниже вероятность хеш-коллизий.

        На практике хеш-алгоритмы используются не только для реализации хеш-таблиц, но и во многих других областях.

        • Хранение паролей: чтобы защищать пароли пользователей, система обычно хранит не сами пароли в открытом виде, а их хеш-значения. Когда пользователь вводит пароль, система вычисляет хеш-значение введенного пароля и сравнивает его с сохраненным значением. Если они совпадают, пароль считается правильным.
        • Проверка целостности данных: отправитель может вычислить хеш-значение данных и отправить его вместе с самими данными; получатель затем вычисляет хеш-значение повторно и сравнивает его с полученным. Если они совпадают, данные считаются целостными.

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

        • Односторонность: по хеш-значению нельзя восстановить какую-либо информацию о входных данных.
        • Устойчивость к коллизиям: должно быть крайне трудно найти два разных входа, имеющих одинаковое хеш-значение.
        • Эффект лавины: даже небольшое изменение во входных данных должно приводить к заметному и непредсказуемому изменению результата.

        Обрати внимание: \"равномерное распределение\" и \"устойчивость к коллизиям\" - это два независимых понятия , и выполнение первого не означает автоматического выполнения второго. Например, при случайном распределении входных key хеш-функция key % 100 может выдавать достаточно равномерное распределение. Однако этот хеш-алгоритм слишком прост: все key с одинаковыми двумя последними цифрами будут иметь одинаковый результат, а значит, по хеш-значению можно легко подобрать подходящие key и, например, взломать пароль.

        ","path":["Глава 6. Хеш-таблицы","6.3   Алгоритмы хеширования"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#632-","level":2,"title":"6.3.2   Проектирование хеш-алгоритма","text":"

        Разработка хеш-алгоритма - это сложная задача, в которой нужно учитывать множество факторов. Однако для некоторых нетребовательных сценариев мы можем спроектировать и несколько простых хеш-алгоритмов.

        • Аддитивный хеш: складываем ASCII-коды всех символов входной строки и используем полученную сумму как хеш-значение.
        • Мультипликативный хеш: используем \"некоррелированность\" умножения; на каждом шаге умножаем текущее значение на константу и добавляем ASCII-код очередного символа.
        • XOR-хеш: последовательно накапливаем элементы входных данных в одном хеш-значении через операцию XOR.
        • Ротационный хеш: последовательно накапливаем ASCII-коды символов, причем перед каждым накоплением выполняем циклический сдвиг хеш-значения.
        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby simple_hash.py
        def add_hash(key: str) -> int:\n    \"\"\"Аддитивное хеширование\"\"\"\n    hash = 0\n    modulus = 1000000007\n    for c in key:\n        hash += ord(c)\n    return hash % modulus\n\ndef mul_hash(key: str) -> int:\n    \"\"\"Мультипликативное хеширование\"\"\"\n    hash = 0\n    modulus = 1000000007\n    for c in key:\n        hash = 31 * hash + ord(c)\n    return hash % modulus\n\ndef xor_hash(key: str) -> int:\n    \"\"\"XOR-хеширование\"\"\"\n    hash = 0\n    modulus = 1000000007\n    for c in key:\n        hash ^= ord(c)\n    return hash % modulus\n\ndef rot_hash(key: str) -> int:\n    \"\"\"Хеширование с циклическим сдвигом\"\"\"\n    hash = 0\n    modulus = 1000000007\n    for c in key:\n        hash = (hash << 4) ^ (hash >> 28) ^ ord(c)\n    return hash % modulus\n
        simple_hash.cpp
        /* Аддитивное хеширование */\nint addHash(string key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (unsigned char c : key) {\n        hash = (hash + (int)c) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* Мультипликативное хеширование */\nint mulHash(string key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (unsigned char c : key) {\n        hash = (31 * hash + (int)c) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* XOR-хеширование */\nint xorHash(string key) {\n    int hash = 0;\n    const int MODULUS = 1000000007;\n    for (unsigned char c : key) {\n        hash ^= (int)c;\n    }\n    return hash & MODULUS;\n}\n\n/* Хеширование с циклическим сдвигом */\nint rotHash(string key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (unsigned char c : key) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ (int)c) % MODULUS;\n    }\n    return (int)hash;\n}\n
        simple_hash.java
        /* Аддитивное хеширование */\nint addHash(String key) {\n    long hash = 0;\n    final int MODULUS = 1000000007;\n    for (char c : key.toCharArray()) {\n        hash = (hash + (int) c) % MODULUS;\n    }\n    return (int) hash;\n}\n\n/* Мультипликативное хеширование */\nint mulHash(String key) {\n    long hash = 0;\n    final int MODULUS = 1000000007;\n    for (char c : key.toCharArray()) {\n        hash = (31 * hash + (int) c) % MODULUS;\n    }\n    return (int) hash;\n}\n\n/* XOR-хеширование */\nint xorHash(String key) {\n    int hash = 0;\n    final int MODULUS = 1000000007;\n    for (char c : key.toCharArray()) {\n        hash ^= (int) c;\n    }\n    return hash & MODULUS;\n}\n\n/* Хеширование с циклическим сдвигом */\nint rotHash(String key) {\n    long hash = 0;\n    final int MODULUS = 1000000007;\n    for (char c : key.toCharArray()) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ (int) c) % MODULUS;\n    }\n    return (int) hash;\n}\n
        simple_hash.cs
        /* Аддитивное хеширование */\nint AddHash(string key) {\n    long hash = 0;\n    const int MODULUS = 1000000007;\n    foreach (char c in key) {\n        hash = (hash + c) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* Мультипликативное хеширование */\nint MulHash(string key) {\n    long hash = 0;\n    const int MODULUS = 1000000007;\n    foreach (char c in key) {\n        hash = (31 * hash + c) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* XOR-хеширование */\nint XorHash(string key) {\n    int hash = 0;\n    const int MODULUS = 1000000007;\n    foreach (char c in key) {\n        hash ^= c;\n    }\n    return hash & MODULUS;\n}\n\n/* Хеширование с циклическим сдвигом */\nint RotHash(string key) {\n    long hash = 0;\n    const int MODULUS = 1000000007;\n    foreach (char c in key) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ c) % MODULUS;\n    }\n    return (int)hash;\n}\n
        simple_hash.go
        /* Аддитивное хеширование */\nfunc addHash(key string) int {\n    var hash int64\n    var modulus int64\n\n    modulus = 1000000007\n    for _, b := range []byte(key) {\n        hash = (hash + int64(b)) % modulus\n    }\n    return int(hash)\n}\n\n/* Мультипликативное хеширование */\nfunc mulHash(key string) int {\n    var hash int64\n    var modulus int64\n\n    modulus = 1000000007\n    for _, b := range []byte(key) {\n        hash = (31*hash + int64(b)) % modulus\n    }\n    return int(hash)\n}\n\n/* XOR-хеширование */\nfunc xorHash(key string) int {\n    hash := 0\n    modulus := 1000000007\n    for _, b := range []byte(key) {\n        fmt.Println(int(b))\n        hash ^= int(b)\n        hash = (31*hash + int(b)) % modulus\n    }\n    return hash & modulus\n}\n\n/* Хеширование с циклическим сдвигом */\nfunc rotHash(key string) int {\n    var hash int64\n    var modulus int64\n\n    modulus = 1000000007\n    for _, b := range []byte(key) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ int64(b)) % modulus\n    }\n    return int(hash)\n}\n
        simple_hash.swift
        /* Аддитивное хеширование */\nfunc addHash(key: String) -> Int {\n    var hash = 0\n    let MODULUS = 1_000_000_007\n    for c in key {\n        for scalar in c.unicodeScalars {\n            hash = (hash + Int(scalar.value)) % MODULUS\n        }\n    }\n    return hash\n}\n\n/* Мультипликативное хеширование */\nfunc mulHash(key: String) -> Int {\n    var hash = 0\n    let MODULUS = 1_000_000_007\n    for c in key {\n        for scalar in c.unicodeScalars {\n            hash = (31 * hash + Int(scalar.value)) % MODULUS\n        }\n    }\n    return hash\n}\n\n/* XOR-хеширование */\nfunc xorHash(key: String) -> Int {\n    var hash = 0\n    let MODULUS = 1_000_000_007\n    for c in key {\n        for scalar in c.unicodeScalars {\n            hash ^= Int(scalar.value)\n        }\n    }\n    return hash & MODULUS\n}\n\n/* Хеширование с циклическим сдвигом */\nfunc rotHash(key: String) -> Int {\n    var hash = 0\n    let MODULUS = 1_000_000_007\n    for c in key {\n        for scalar in c.unicodeScalars {\n            hash = ((hash << 4) ^ (hash >> 28) ^ Int(scalar.value)) % MODULUS\n        }\n    }\n    return hash\n}\n
        simple_hash.js
        /* Аддитивное хеширование */\nfunction addHash(key) {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = (hash + c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n\n/* Мультипликативное хеширование */\nfunction mulHash(key) {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = (31 * hash + c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n\n/* XOR-хеширование */\nfunction xorHash(key) {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash ^= c.charCodeAt(0);\n    }\n    return hash % MODULUS;\n}\n\n/* Хеширование с циклическим сдвигом */\nfunction rotHash(key) {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n
        simple_hash.ts
        /* Аддитивное хеширование */\nfunction addHash(key: string): number {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = (hash + c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n\n/* Мультипликативное хеширование */\nfunction mulHash(key: string): number {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = (31 * hash + c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n\n/* XOR-хеширование */\nfunction xorHash(key: string): number {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash ^= c.charCodeAt(0);\n    }\n    return hash % MODULUS;\n}\n\n/* Хеширование с циклическим сдвигом */\nfunction rotHash(key: string): number {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n
        simple_hash.dart
        /* Аддитивное хеширование */\nint addHash(String key) {\n  int hash = 0;\n  final int MODULUS = 1000000007;\n  for (int i = 0; i < key.length; i++) {\n    hash = (hash + key.codeUnitAt(i)) % MODULUS;\n  }\n  return hash;\n}\n\n/* Мультипликативное хеширование */\nint mulHash(String key) {\n  int hash = 0;\n  final int MODULUS = 1000000007;\n  for (int i = 0; i < key.length; i++) {\n    hash = (31 * hash + key.codeUnitAt(i)) % MODULUS;\n  }\n  return hash;\n}\n\n/* XOR-хеширование */\nint xorHash(String key) {\n  int hash = 0;\n  final int MODULUS = 1000000007;\n  for (int i = 0; i < key.length; i++) {\n    hash ^= key.codeUnitAt(i);\n  }\n  return hash & MODULUS;\n}\n\n/* Хеширование с циклическим сдвигом */\nint rotHash(String key) {\n  int hash = 0;\n  final int MODULUS = 1000000007;\n  for (int i = 0; i < key.length; i++) {\n    hash = ((hash << 4) ^ (hash >> 28) ^ key.codeUnitAt(i)) % MODULUS;\n  }\n  return hash;\n}\n
        simple_hash.rs
        /* Аддитивное хеширование */\nfn add_hash(key: &str) -> i32 {\n    let mut hash = 0_i64;\n    const MODULUS: i64 = 1000000007;\n\n    for c in key.chars() {\n        hash = (hash + c as i64) % MODULUS;\n    }\n\n    hash as i32\n}\n\n/* Мультипликативное хеширование */\nfn mul_hash(key: &str) -> i32 {\n    let mut hash = 0_i64;\n    const MODULUS: i64 = 1000000007;\n\n    for c in key.chars() {\n        hash = (31 * hash + c as i64) % MODULUS;\n    }\n\n    hash as i32\n}\n\n/* XOR-хеширование */\nfn xor_hash(key: &str) -> i32 {\n    let mut hash = 0_i64;\n    const MODULUS: i64 = 1000000007;\n\n    for c in key.chars() {\n        hash ^= c as i64;\n    }\n\n    (hash & MODULUS) as i32\n}\n\n/* Хеширование с циклическим сдвигом */\nfn rot_hash(key: &str) -> i32 {\n    let mut hash = 0_i64;\n    const MODULUS: i64 = 1000000007;\n\n    for c in key.chars() {\n        hash = ((hash << 4) ^ (hash >> 28) ^ c as i64) % MODULUS;\n    }\n\n    hash as i32\n}\n
        simple_hash.c
        /* Аддитивное хеширование */\nint addHash(char *key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (int i = 0; i < strlen(key); i++) {\n        hash = (hash + (unsigned char)key[i]) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* Мультипликативное хеширование */\nint mulHash(char *key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (int i = 0; i < strlen(key); i++) {\n        hash = (31 * hash + (unsigned char)key[i]) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* XOR-хеширование */\nint xorHash(char *key) {\n    int hash = 0;\n    const int MODULUS = 1000000007;\n\n    for (int i = 0; i < strlen(key); i++) {\n        hash ^= (unsigned char)key[i];\n    }\n    return hash & MODULUS;\n}\n\n/* Хеширование с циклическим сдвигом */\nint rotHash(char *key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (int i = 0; i < strlen(key); i++) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ (unsigned char)key[i]) % MODULUS;\n    }\n\n    return (int)hash;\n}\n
        simple_hash.kt
        /* Аддитивное хеширование */\nfun addHash(key: String): Int {\n    var hash = 0L\n    val MODULUS = 1000000007\n    for (c in key.toCharArray()) {\n        hash = (hash + c.code) % MODULUS\n    }\n    return hash.toInt()\n}\n\n/* Мультипликативное хеширование */\nfun mulHash(key: String): Int {\n    var hash = 0L\n    val MODULUS = 1000000007\n    for (c in key.toCharArray()) {\n        hash = (31 * hash + c.code) % MODULUS\n    }\n    return hash.toInt()\n}\n\n/* XOR-хеширование */\nfun xorHash(key: String): Int {\n    var hash = 0\n    val MODULUS = 1000000007\n    for (c in key.toCharArray()) {\n        hash = hash xor c.code\n    }\n    return hash and MODULUS\n}\n\n/* Хеширование с циклическим сдвигом */\nfun rotHash(key: String): Int {\n    var hash = 0L\n    val MODULUS = 1000000007\n    for (c in key.toCharArray()) {\n        hash = ((hash shl 4) xor (hash shr 28) xor c.code.toLong()) % MODULUS\n    }\n    return hash.toInt()\n}\n
        simple_hash.rb
        ### Аддитивное хеширование ###\ndef add_hash(key)\n  hash = 0\n  modulus = 1_000_000_007\n\n  key.each_char { |c| hash += c.ord }\n\n  hash % modulus\nend\n\n### Мультипликативное хеширование ###\ndef mul_hash(key)\n  hash = 0\n  modulus = 1_000_000_007\n\n  key.each_char { |c| hash = 31 * hash + c.ord }\n\n  hash % modulus\nend\n\n### XOR-хеширование ###\ndef xor_hash(key)\n  hash = 0\n  modulus = 1_000_000_007\n\n  key.each_char { |c| hash ^= c.ord }\n\n  hash % modulus\nend\n\n### Хеширование с циклическим сдвигом ###\ndef rot_hash(key)\n  hash = 0\n  modulus = 1_000_000_007\n\n  key.each_char { |c| hash = (hash << 4) ^ (hash >> 28) ^ c.ord }\n\n  hash % modulus\nend\n
        Визуализация кода

        Во весь экран >

        Нетрудно заметить, что последний шаг каждого из этих хеш-алгоритмов - взятие по модулю большого простого числа \\(1000000007\\) , чтобы гарантировать, что хеш-значение остается в разумных границах. Стоит задуматься: почему подчеркивается именно взятие по модулю простого числа, и какие недостатки возникают при использовании составного модуля? Это интересный вопрос.

        Сначала дадим вывод: использование большого простого числа в качестве модуля позволяет в максимальной степени обеспечивать равномерное распределение хеш-значений. Поскольку простое число не имеет общих делителей с другими числами, это помогает уменьшить периодические закономерности, возникающие из-за операции взятия остатка, и тем самым снизить число хеш-коллизий.

        Рассмотрим пример. Предположим, мы выбрали составное число \\(9\\) в качестве модуля. Оно делится на \\(3\\) , поэтому все key , которые делятся на \\(3\\) , будут отображаться только в три хеш-значения: \\(0\\) , \\(3\\) , \\(6\\) .

        \\[ \\begin{aligned} \\text{modulus} & = 9 \\newline \\text{key} & = \\{ 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, \\dots \\} \\newline \\text{hash} & = \\{ 0, 3, 6, 0, 3, 6, 0, 3, 6, 0, 3, 6,\\dots \\} \\end{aligned} \\]

        Если входные key как раз удовлетворяют такому распределению в виде арифметической прогрессии, то хеш-значения начнут скучиваться, а это усугубит хеш-коллизии. Теперь предположим, что мы заменили modulus на простое число \\(13\\) ; поскольку между key и modulus нет общих делителей, равномерность распределения хеш-значений заметно улучшится.

        \\[ \\begin{aligned} \\text{modulus} & = 13 \\newline \\text{key} & = \\{ 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, \\dots \\} \\newline \\text{hash} & = \\{ 0, 3, 6, 9, 12, 2, 5, 8, 11, 1, 4, 7, \\dots \\} \\end{aligned} \\]

        Следует отметить: если можно гарантировать, что key распределены случайно и равномерно, то выбор простого или составного числа в качестве модуля не так важен - оба варианта способны дать равномерное распределение хеш-значений. Но если в распределении key присутствует периодичность, то взятие по модулю составного числа гораздо легче приводит к кластеризации.

        Итак, на практике мы обычно выбираем простое число в качестве модуля, причем это простое число желательно брать достаточно большим, чтобы по возможности убрать периодические закономерности и повысить устойчивость хеш-алгоритма.

        ","path":["Глава 6. Хеш-таблицы","6.3   Алгоритмы хеширования"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#633-","level":2,"title":"6.3.3   Распространенные хеш-алгоритмы","text":"

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

        На практике мы обычно используем стандартные хеш-алгоритмы, такие как MD5, SHA-1, SHA-2 и SHA-3. Они могут отображать входные данные произвольной длины в хеш-значения фиксированной длины.

        На протяжении почти ста лет хеш-алгоритмы непрерывно развивались и оптимизировались. Одни исследователи старались повысить их производительность, а другие исследователи и хакеры сосредоточивались на поиске уязвимостей в их безопасности. В таблице 6-2 приведены распространенные хеш-алгоритмы, которые часто встречаются в реальных приложениях.

        • MD5 и SHA-1 уже многократно были успешно атакованы, поэтому они выведены из большинства сценариев, где требуется безопасность.
        • SHA-256 из семейства SHA-2 является одним из самых надежных хеш-алгоритмов; на сегодняшний день не известно успешных практических атак, поэтому он широко используется в самых разных протоколах и системах безопасности.
        • SHA-3 по сравнению с SHA-2 требует меньших затрат на реализацию и обеспечивает более высокую вычислительную эффективность, но на данный момент распространен слабее, чем семейство SHA-2.

        Таблица 6-2   Распространенные хеш-алгоритмы

        MD5 SHA-1 SHA-2 SHA-3 Год появления 1992 1995 2002 2008 Длина вывода 128 bit 160 bit 256/512 bit 224/256/384/512 bit Хеш-коллизии Частые Частые Редкие Редкие Уровень безопасности Низкий, успешно атакован Низкий, успешно атакован Высокий Высокий Применение Устарел, но еще используется для проверки целостности данных Устарел Проверка криптовалютных транзакций, цифровые подписи и т. д. Может использоваться как замена SHA-2","path":["Глава 6. Хеш-таблицы","6.3   Алгоритмы хеширования"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#634-","level":2,"title":"6.3.4   Хеш-значения структур данных","text":"

        Мы знаем, что key в хеш-таблице могут быть целыми числами, вещественными числами, строками и другими типами данных. Языки программирования обычно предоставляют встроенные хеш-алгоритмы для этих типов, чтобы вычислять индексы бакетов в хеш-таблице. Возьмем Python: в нем можно вызвать функцию hash() , чтобы вычислить хеш-значения для различных типов данных.

        • Хеш-значение целого числа и булева значения совпадает с самим значением.
        • Вычисление хеш-значений для вещественных чисел и строк устроено сложнее; интересующиеся читатели могут изучить это самостоятельно.
        • Хеш-значение кортежа получается путем хеширования каждого элемента, а затем объединения этих хеш-значений в одно итоговое значение.
        • Хеш-значение объекта обычно строится на основе его адреса в памяти. Если переопределить метод хеширования объекта, можно реализовать вычисление хеша по содержимому.

        Tip

        Обрати внимание: определения и способы вычисления встроенных хеш-значений в разных языках программирования отличаются.

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby built_in_hash.py
        num = 3\nhash_num = hash(num)\n# Хеш-значение целого числа 3 равно 3\n\nbol = True\nhash_bol = hash(bol)\n# Хеш-значение булевого значения True равно 1\n\ndec = 3.14159\nhash_dec = hash(dec)\n# Хеш-значение числа 3.14159 равно 326484311674566659\n\nstr = \"Hello Algo\"\nhash_str = hash(str)\n# Хеш-значение строки \"Hello Algo\" равно 4617003410720528961\n\ntup = (12836, \"Сяо Ха\")\nhash_tup = hash(tup)\n# Хеш-значение кортежа (12836, \"Сяо Ха\") равно 1029005403108185979\n\nobj = ListNode(0)\nhash_obj = hash(obj)\n# Хеш-значение объекта узла <ListNode object at 0x1058fd810> равно 274267521\n
        built_in_hash.cpp
        int num = 3;\nsize_t hashNum = hash<int>()(num);\n// Хеш-значение целого числа 3 равно 3\n\nbool bol = true;\nsize_t hashBol = hash<bool>()(bol);\n// Хеш-значение булевого значения 1 равно 1\n\ndouble dec = 3.14159;\nsize_t hashDec = hash<double>()(dec);\n// Хеш-значение числа 3.14159 равно 4614256650576692846\n\nstring str = \"Hello Algo\";\nsize_t hashStr = hash<string>()(str);\n// Хеш-значение строки \"Hello Algo\" равно 15466937326284535026\n\n// В C++ встроенный std::hash() предоставляет вычисление хеша только для базовых типов данных\n// Для массивов и объектов хеш-значение обычно приходится реализовывать самостоятельно\n
        built_in_hash.java
        int num = 3;\nint hashNum = Integer.hashCode(num);\n// Хеш-значение целого числа 3 равно 3\n\nboolean bol = true;\nint hashBol = Boolean.hashCode(bol);\n// Хеш-значение булевого значения true равно 1231\n\ndouble dec = 3.14159;\nint hashDec = Double.hashCode(dec);\n// Хеш-значение числа 3.14159 равно -1340954729\n\nString str = \"Hello Algo\";\nint hashStr = str.hashCode();\n// Хеш-значение строки \"Hello Algo\" равно -727081396\n\nObject[] arr = { 12836, \"Сяо Ха\" };\nint hashTup = Arrays.hashCode(arr);\n// Хеш-значение массива [12836, Сяо Ха] равно 1151158\n\nListNode obj = new ListNode(0);\nint hashObj = obj.hashCode();\n// Хеш-значение объекта узла utils.ListNode@7dc5e7b4 равно 2110121908\n
        built_in_hash.cs
        int num = 3;\nint hashNum = num.GetHashCode();\n// Хеш-значение целого числа 3 равно 3;\n\nbool bol = true;\nint hashBol = bol.GetHashCode();\n// Хеш-значение булевого значения true равно 1;\n\ndouble dec = 3.14159;\nint hashDec = dec.GetHashCode();\n// Хеш-значение числа 3.14159 равно -1340954729;\n\nstring str = \"Hello Algo\";\nint hashStr = str.GetHashCode();\n// Хеш-значение строки \"Hello Algo\" равно -586107568;\n\nobject[] arr = [12836, \"Сяо Ха\"];\nint hashTup = arr.GetHashCode();\n// Хеш-значение массива [12836, Сяо Ха] равно 42931033;\n\nListNode obj = new(0);\nint hashObj = obj.GetHashCode();\n// Хеш-значение объекта узла 0 равно 39053774;\n
        built_in_hash.go
        // В Go нет встроенной функции hash code\n
        built_in_hash.swift
        let num = 3\nlet hashNum = num.hashValue\n// Хеш-значение целого числа 3 равно 9047044699613009734\n\nlet bol = true\nlet hashBol = bol.hashValue\n// Хеш-значение булевого значения true равно -4431640247352757451\n\nlet dec = 3.14159\nlet hashDec = dec.hashValue\n// Хеш-значение числа 3.14159 равно -2465384235396674631\n\nlet str = \"Hello Algo\"\nlet hashStr = str.hashValue\n// Хеш-значение строки \"Hello Algo\" равно -7850626797806988787\n\nlet arr = [AnyHashable(12836), AnyHashable(\"Сяо Ха\")]\nlet hashTup = arr.hashValue\n// Хеш-значение массива [AnyHashable(12836), AnyHashable(\"Сяо Ха\")] равно -2308633508154532996\n\nlet obj = ListNode(x: 0)\nlet hashObj = obj.hashValue\n// Хеш-значение объекта узла utils.ListNode равно -2434780518035996159\n
        built_in_hash.js
        // В JavaScript нет встроенной функции hash code\n
        built_in_hash.ts
        // В TypeScript нет встроенной функции hash code\n
        built_in_hash.dart
        int num = 3;\nint hashNum = num.hashCode;\n// Хеш-значение целого числа 3 равно 34803\n\nbool bol = true;\nint hashBol = bol.hashCode;\n// Хеш-значение булевого значения true равно 1231\n\ndouble dec = 3.14159;\nint hashDec = dec.hashCode;\n// Хеш-значение числа 3.14159 равно 2570631074981783\n\nString str = \"Hello Algo\";\nint hashStr = str.hashCode;\n// Хеш-значение строки \"Hello Algo\" равно 468167534\n\nList arr = [12836, \"Сяо Ха\"];\nint hashArr = arr.hashCode;\n// Хеш-значение массива [12836, Сяо Ха] равно 976512528\n\nListNode obj = new ListNode(0);\nint hashObj = obj.hashCode;\n// Хеш-значение объекта Instance of 'ListNode' равно 1033450432\n
        built_in_hash.rs
        use std::collections::hash_map::DefaultHasher;\nuse std::hash::{Hash, Hasher};\n\nlet num = 3;\nlet mut num_hasher = DefaultHasher::new();\nnum.hash(&mut num_hasher);\nlet hash_num = num_hasher.finish();\n// Хеш-значение целого числа 3 равно 568126464209439262\n\nlet bol = true;\nlet mut bol_hasher = DefaultHasher::new();\nbol.hash(&mut bol_hasher);\nlet hash_bol = bol_hasher.finish();\n// Хеш-значение булевого значения true равно 4952851536318644461\n\nlet dec: f32 = 3.14159;\nlet mut dec_hasher = DefaultHasher::new();\ndec.to_bits().hash(&mut dec_hasher);\nlet hash_dec = dec_hasher.finish();\n// Хеш-значение числа 3.14159 равно 2566941990314602357\n\nlet str = \"Hello Algo\";\nlet mut str_hasher = DefaultHasher::new();\nstr.hash(&mut str_hasher);\nlet hash_str = str_hasher.finish();\n// Хеш-значение строки \"Hello Algo\" равно 16092673739211250988\n\nlet arr = (&12836, &\"Сяо Ха\");\nlet mut tup_hasher = DefaultHasher::new();\narr.hash(&mut tup_hasher);\nlet hash_tup = tup_hasher.finish();\n// Хеш-значение кортежа (12836, \"Сяо Ха\") равно 1885128010422702749\n\nlet node = ListNode::new(42);\nlet mut hasher = DefaultHasher::new();\nnode.borrow().val.hash(&mut hasher);\nlet hash = hasher.finish();\n// Хеш-значение объекта RefCell { value: ListNode { val: 42, next: None } } равно 15387811073369036852\n
        built_in_hash.c
        // В C нет встроенной функции hash code\n
        built_in_hash.kt
        val num = 3\nval hashNum = num.hashCode()\n// Хеш-значение целого числа 3 равно 3\n\nval bol = true\nval hashBol = bol.hashCode()\n// Хеш-значение булевого значения true равно 1231\n\nval dec = 3.14159\nval hashDec = dec.hashCode()\n// Хеш-значение числа 3.14159 равно -1340954729\n\nval str = \"Hello Algo\"\nval hashStr = str.hashCode()\n// Хеш-значение строки \"Hello Algo\" равно -727081396\n\nval arr = arrayOf<Any>(12836, \"Сяо Ха\")\nval hashTup = arr.hashCode()\n// Хеш-значение массива [12836, Сяо Ха] равно 189568618\n\nval obj = ListNode(0)\nval hashObj = obj.hashCode()\n// Хеш-значение объекта узла utils.ListNode@1d81eb93 равно 495053715\n
        built_in_hash.rb
        num = 3\nhash_num = num.hash\n# Хеш-значение целого числа 3 равно -4385856518450339636\n\nbol = true\nhash_bol = bol.hash\n# Хеш-значение булевого значения true равно -1617938112149317027\n\ndec = 3.14159\nhash_dec = dec.hash\n# Хеш-значение числа 3.14159 равно -1479186995943067893\n\nstr = \"Hello Algo\"\nhash_str = str.hash\n# Хеш-значение строки \"Hello Algo\" равно -4075943250025831763\n\ntup = [12836, 'Сяо Ха']\nhash_tup = tup.hash\n# Хеш-значение кортежа (12836, 'Сяо Ха') равно 1999544809202288822\n\nobj = ListNode.new(0)\nhash_obj = obj.hash\n# Хеш-значение объекта #<ListNode:0x000078133140ab70> равно 4302940560806366381\n
        Визуализация выполнения

        https://pythontutor.com/render.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%D1%81%D0%B2%D1%8F%D0%B7%D0%BD%D1%8B%D0%B9%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%D1%83%D0%B7%D0%B5%D0%BB%D0%BA%D0%BB%D0%B0%D1%81%D1%81%22%22%22%0A%20%20%20%20def%20__init__%28self%2C%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%D0%97%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%D1%83%D0%B7%D0%BB%D0%B0%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%D0%A1%D1%81%D1%8B%D0%BB%D0%BA%D0%B0%20%D0%BD%D0%B0%20%D1%81%D0%BB%D0%B5%D0%B4%D1%83%D1%8E%D1%89%D0%B8%D0%B9%20%D1%83%D0%B7%D0%B5%D0%BB%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20num%20%3D%203%0A%20%20%20%20hash_num%20%3D%20hash%28num%29%0A%20%20%20%20%23%20%D0%A5%D0%B5%D1%88-%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%D1%86%D0%B5%D0%BB%D0%BE%D0%B3%D0%BE%20%D1%87%D0%B8%D1%81%D0%BB%D0%B0%203%20%D1%80%D0%B0%D0%B2%D0%BD%D0%BE%203%0A%0A%20%20%20%20bol%20%3D%20True%0A%20%20%20%20hash_bol%20%3D%20hash%28bol%29%0A%20%20%20%20%23%20%D0%A5%D0%B5%D1%88-%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%D0%B1%D1%83%D0%BB%D0%B5%D0%B2%D0%B0%20%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D1%8F%20True%20%D1%80%D0%B0%D0%B2%D0%BD%D0%BE%201%0A%0A%20%20%20%20dec%20%3D%203.14159%0A%20%20%20%20hash_dec%20%3D%20hash%28dec%29%0A%20%20%20%20%23%20%D0%A5%D0%B5%D1%88-%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%D1%87%D0%B8%D1%81%D0%BB%D0%B0%203.14159%20%D1%80%D0%B0%D0%B2%D0%BD%D0%BE%20326484311674566659%0A%0A%20%20%20%20str%20%3D%20%22Hello%20Algo%22%0A%20%20%20%20hash_str%20%3D%20hash%28str%29%0A%20%20%20%20%23%20%D0%A5%D0%B5%D1%88-%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%D1%81%D1%82%D1%80%D0%BE%D0%BA%D0%B8%20%22Hello%20Algo%22%20%D1%80%D0%B0%D0%B2%D0%BD%D0%BE%204617003410720528961%0A%0A%20%20%20%20tup%20%3D%20%2812836%2C%20%22%D0%A1%D1%8F%D0%BE%20%D0%A5%D0%B0%22%29%0A%20%20%20%20hash_tup%20%3D%20hash%28tup%29%0A%20%20%20%20%23%20%D0%A5%D0%B5%D1%88-%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%D0%BA%D0%BE%D1%80%D1%82%D0%B5%D0%B6%D0%B0%20%2812836%2C%20%27%D0%A1%D1%8F%D0%BE%20%D0%A5%D0%B0%27%29%20%D1%80%D0%B0%D0%B2%D0%BD%D0%BE%201029005403108185979%0A%0A%20%20%20%20obj%20%3D%20ListNode%280%29%0A%20%20%20%20hash_obj%20%3D%20hash%28obj%29%0A%20%20%20%20%23%20%D0%A5%D0%B5%D1%88-%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%D0%BE%D0%B1%D1%8A%D0%B5%D0%BA%D1%82%D0%B0%20%D1%83%D0%B7%D0%BB%D0%B0%20%3CListNode%20object%20at%200x1058fd810%3E%20%D1%80%D0%B0%D0%B2%D0%BD%D0%BE%20274267521&cumulative=false&curInstr=19&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

        Во многих языках программирования в качестве key хеш-таблицы можно использовать только неизменяемые объекты . Если, например, использовать список (динамический массив) как key , то после изменения содержимого списка изменится и его хеш-значение, из-за чего мы уже не сможем найти прежнее value в хеш-таблице.

        Хотя у пользовательских объектов (например, у узла связного списка) поля являются изменяемыми, сам объект все же может быть хешируемым. Причина в том, что хеш-значение объекта обычно строится на основе адреса в памяти : даже если содержимое объекта меняется, его адрес памяти остается прежним, а значит, и хеш-значение не меняется.

        Внимательный читатель мог заметить, что при запуске программы в разных консолях выводимые хеш-значения отличаются. Это связано с тем, что интерпретатор Python при каждом запуске добавляет в хеш-функцию строк случайную соль (salt). Такой подход эффективно защищает от атак типа HashDoS и повышает безопасность хеш-алгоритма.

        ","path":["Глава 6. Хеш-таблицы","6.3   Алгоритмы хеширования"],"tags":[]},{"location":"chapter_hashing/hash_collision/","level":1,"title":"6.2   Хеш-коллизии","text":"

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

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

        1. Улучшить структуру данных хеш-таблицы, чтобы она могла корректно работать даже при возникновении хеш-коллизий.
        2. Выполнять расширение только тогда, когда это действительно необходимо, то есть когда хеш-коллизии становятся достаточно серьезными.

        Основные способы улучшения структуры хеш-таблицы включают метод цепочек и открытую адресацию.

        ","path":["Глава 6. Хеш-таблицы","6.2   Хеш-коллизии"],"tags":[]},{"location":"chapter_hashing/hash_collision/#621","level":2,"title":"6.2.1   Метод цепочек","text":"

        В исходной хеш-таблице каждый бакет может хранить только одну пару ключ-значение. Метод цепочек (separate chaining) превращает отдельный элемент в связный список: пары ключ-значение становятся узлами списка, и все конфликтующие пары ключ-значение хранятся в одном и том же списке. На рисунке 6-5 показан пример хеш-таблицы, реализованной методом цепочек.

        Рисунок 6-5   Хеш-таблица с методом цепочек

        Методы работы с хеш-таблицей, построенной на основе метода цепочек, меняются следующим образом.

        • Поиск элемента: передаем key , по хеш-функции получаем индекс бакета, после чего обращаемся к голове списка и обходим список, сравнивая key , пока не найдем целевую пару ключ-значение.
        • Добавление элемента: сначала через хеш-функцию получаем голову списка, затем добавляем узел (пару ключ-значение) в этот список.
        • Удаление элемента: по результату хеш-функции обращаемся к голове списка, затем обходим список, находим целевой узел и удаляем его.

        Метод цепочек имеет следующие ограничения.

        • Рост потребления памяти: связный список содержит указатели на узлы, поэтому по сравнению с массивом он требует больше памяти.
        • Снижение эффективности поиска: для нахождения нужного элемента нужно линейно обходить связный список.

        Ниже приведена простая реализация хеш-таблицы методом цепочек. Следует обратить внимание на два момента.

        • Для упрощения кода вместо связного списка используется список (динамический массив). В этой реализации хеш-таблица (массив) содержит несколько бакетов, и каждый бакет представляет собой список.
        • Ниже включен метод расширения хеш-таблицы. Когда коэффициент загрузки превышает \\(\\frac{2}{3}\\) , мы расширяем хеш-таблицу до \\(2\\) раз от прежней емкости.
        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map_chaining.py
        class HashMapChaining:\n    \"\"\"Хеш-таблица с цепочками\"\"\"\n\n    def __init__(self):\n        \"\"\"Конструктор\"\"\"\n        self.size = 0  # Число пар ключ-значение\n        self.capacity = 4  # Вместимость хеш-таблицы\n        self.load_thres = 2.0 / 3.0  # Порог коэффициента загрузки для запуска расширения\n        self.extend_ratio = 2  # Коэффициент расширения\n        self.buckets = [[] for _ in range(self.capacity)]  # Массив корзин\n\n    def hash_func(self, key: int) -> int:\n        \"\"\"Хеш-функция\"\"\"\n        return key % self.capacity\n\n    def load_factor(self) -> float:\n        \"\"\"Коэффициент загрузки\"\"\"\n        return self.size / self.capacity\n\n    def get(self, key: int) -> str | None:\n        \"\"\"Операция поиска\"\"\"\n        index = self.hash_func(key)\n        bucket = self.buckets[index]\n        # Обойти корзину; если найден key, вернуть соответствующее val\n        for pair in bucket:\n            if pair.key == key:\n                return pair.val\n        # Если key не найден, вернуть None\n        return None\n\n    def put(self, key: int, val: str):\n        \"\"\"Операция добавления\"\"\"\n        # Когда коэффициент загрузки превышает порог, выполнить расширение\n        if self.load_factor() > self.load_thres:\n            self.extend()\n        index = self.hash_func(key)\n        bucket = self.buckets[index]\n        # Обойти корзину; если встретился указанный key, обновить соответствующее val и вернуть\n        for pair in bucket:\n            if pair.key == key:\n                pair.val = val\n                return\n        # Если такого key нет, добавить пару ключ-значение в конец\n        pair = Pair(key, val)\n        bucket.append(pair)\n        self.size += 1\n\n    def remove(self, key: int):\n        \"\"\"Операция удаления\"\"\"\n        index = self.hash_func(key)\n        bucket = self.buckets[index]\n        # Обойти корзину и удалить из нее пару ключ-значение\n        for pair in bucket:\n            if pair.key == key:\n                bucket.remove(pair)\n                self.size -= 1\n                break\n\n    def extend(self):\n        \"\"\"Расширить хеш-таблицу\"\"\"\n        # Временно сохранить исходную хеш-таблицу\n        buckets = self.buckets\n        # Инициализация новой хеш-таблицы после расширения\n        self.capacity *= self.extend_ratio\n        self.buckets = [[] for _ in range(self.capacity)]\n        self.size = 0\n        # Перенести пары ключ-значение из исходной хеш-таблицы в новую\n        for bucket in buckets:\n            for pair in bucket:\n                self.put(pair.key, pair.val)\n\n    def print(self):\n        \"\"\"Вывести хеш-таблицу\"\"\"\n        for bucket in self.buckets:\n            res = []\n            for pair in bucket:\n                res.append(str(pair.key) + \" -> \" + pair.val)\n            print(res)\n
        hash_map_chaining.cpp
        /* Хеш-таблица с цепочками */\nclass HashMapChaining {\n  private:\n    int size;                       // Число пар ключ-значение\n    int capacity;                   // Вместимость хеш-таблицы\n    double loadThres;               // Порог коэффициента загрузки для запуска расширения\n    int extendRatio;                // Коэффициент расширения\n    vector<vector<Pair *>> buckets; // Массив корзин\n\n  public:\n    /* Конструктор */\n    HashMapChaining() : size(0), capacity(4), loadThres(2.0 / 3.0), extendRatio(2) {\n        buckets.resize(capacity);\n    }\n\n    /* Метод-деструктор */\n    ~HashMapChaining() {\n        for (auto &bucket : buckets) {\n            for (Pair *pair : bucket) {\n                // Освободить память\n                delete pair;\n            }\n        }\n    }\n\n    /* Хеш-функция */\n    int hashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* Коэффициент загрузки */\n    double loadFactor() {\n        return (double)size / (double)capacity;\n    }\n\n    /* Операция поиска */\n    string get(int key) {\n        int index = hashFunc(key);\n        // Обойти корзину; если найден key, вернуть соответствующее val\n        for (Pair *pair : buckets[index]) {\n            if (pair->key == key) {\n                return pair->val;\n            }\n        }\n        // Если key не найден, вернуть пустую строку\n        return \"\";\n    }\n\n    /* Операция добавления */\n    void put(int key, string val) {\n        // Когда коэффициент загрузки превышает порог, выполнить расширение\n        if (loadFactor() > loadThres) {\n            extend();\n        }\n        int index = hashFunc(key);\n        // Обойти корзину; если встретился указанный key, обновить соответствующее val и вернуть\n        for (Pair *pair : buckets[index]) {\n            if (pair->key == key) {\n                pair->val = val;\n                return;\n            }\n        }\n        // Если такого key нет, добавить пару ключ-значение в конец\n        buckets[index].push_back(new Pair(key, val));\n        size++;\n    }\n\n    /* Операция удаления */\n    void remove(int key) {\n        int index = hashFunc(key);\n        auto &bucket = buckets[index];\n        // Обойти корзину и удалить из нее пару ключ-значение\n        for (int i = 0; i < bucket.size(); i++) {\n            if (bucket[i]->key == key) {\n                Pair *tmp = bucket[i];\n                bucket.erase(bucket.begin() + i); // Удалить из него пару ключ-значение\n                delete tmp;                       // Освободить память\n                size--;\n                return;\n            }\n        }\n    }\n\n    /* Расширить хеш-таблицу */\n    void extend() {\n        // Временно сохранить исходную хеш-таблицу\n        vector<vector<Pair *>> bucketsTmp = buckets;\n        // Инициализация новой хеш-таблицы после расширения\n        capacity *= extendRatio;\n        buckets.clear();\n        buckets.resize(capacity);\n        size = 0;\n        // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n        for (auto &bucket : bucketsTmp) {\n            for (Pair *pair : bucket) {\n                put(pair->key, pair->val);\n                // Освободить память\n                delete pair;\n            }\n        }\n    }\n\n    /* Вывести хеш-таблицу */\n    void print() {\n        for (auto &bucket : buckets) {\n            cout << \"[\";\n            for (Pair *pair : bucket) {\n                cout << pair->key << \" -> \" << pair->val << \", \";\n            }\n            cout << \"]\\n\";\n        }\n    }\n};\n
        hash_map_chaining.java
        /* Хеш-таблица с цепочками */\nclass HashMapChaining {\n    int size; // Число пар ключ-значение\n    int capacity; // Вместимость хеш-таблицы\n    double loadThres; // Порог коэффициента загрузки для запуска расширения\n    int extendRatio; // Коэффициент расширения\n    List<List<Pair>> buckets; // Массив корзин\n\n    /* Конструктор */\n    public HashMapChaining() {\n        size = 0;\n        capacity = 4;\n        loadThres = 2.0 / 3.0;\n        extendRatio = 2;\n        buckets = new ArrayList<>(capacity);\n        for (int i = 0; i < capacity; i++) {\n            buckets.add(new ArrayList<>());\n        }\n    }\n\n    /* Хеш-функция */\n    int hashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* Коэффициент загрузки */\n    double loadFactor() {\n        return (double) size / capacity;\n    }\n\n    /* Операция поиска */\n    String get(int key) {\n        int index = hashFunc(key);\n        List<Pair> bucket = buckets.get(index);\n        // Обойти корзину; если найден key, вернуть соответствующее val\n        for (Pair pair : bucket) {\n            if (pair.key == key) {\n                return pair.val;\n            }\n        }\n        // Если key не найден, вернуть null\n        return null;\n    }\n\n    /* Операция добавления */\n    void put(int key, String val) {\n        // Когда коэффициент загрузки превышает порог, выполнить расширение\n        if (loadFactor() > loadThres) {\n            extend();\n        }\n        int index = hashFunc(key);\n        List<Pair> bucket = buckets.get(index);\n        // Обойти корзину; если встретился указанный key, обновить соответствующее val и вернуть\n        for (Pair pair : bucket) {\n            if (pair.key == key) {\n                pair.val = val;\n                return;\n            }\n        }\n        // Если такого key нет, добавить пару ключ-значение в конец\n        Pair pair = new Pair(key, val);\n        bucket.add(pair);\n        size++;\n    }\n\n    /* Операция удаления */\n    void remove(int key) {\n        int index = hashFunc(key);\n        List<Pair> bucket = buckets.get(index);\n        // Обойти корзину и удалить из нее пару ключ-значение\n        for (Pair pair : bucket) {\n            if (pair.key == key) {\n                bucket.remove(pair);\n                size--;\n                break;\n            }\n        }\n    }\n\n    /* Расширить хеш-таблицу */\n    void extend() {\n        // Временно сохранить исходную хеш-таблицу\n        List<List<Pair>> bucketsTmp = buckets;\n        // Инициализация новой хеш-таблицы после расширения\n        capacity *= extendRatio;\n        buckets = new ArrayList<>(capacity);\n        for (int i = 0; i < capacity; i++) {\n            buckets.add(new ArrayList<>());\n        }\n        size = 0;\n        // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n        for (List<Pair> bucket : bucketsTmp) {\n            for (Pair pair : bucket) {\n                put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Вывести хеш-таблицу */\n    void print() {\n        for (List<Pair> bucket : buckets) {\n            List<String> res = new ArrayList<>();\n            for (Pair pair : bucket) {\n                res.add(pair.key + \" -> \" + pair.val);\n            }\n            System.out.println(res);\n        }\n    }\n}\n
        hash_map_chaining.cs
        /* Хеш-таблица с цепочками */\nclass HashMapChaining {\n    int size; // Число пар ключ-значение\n    int capacity; // Вместимость хеш-таблицы\n    double loadThres; // Порог коэффициента загрузки для запуска расширения\n    int extendRatio; // Коэффициент расширения\n    List<List<Pair>> buckets; // Массив корзин\n\n    /* Конструктор */\n    public HashMapChaining() {\n        size = 0;\n        capacity = 4;\n        loadThres = 2.0 / 3.0;\n        extendRatio = 2;\n        buckets = new List<List<Pair>>(capacity);\n        for (int i = 0; i < capacity; i++) {\n            buckets.Add([]);\n        }\n    }\n\n    /* Хеш-функция */\n    int HashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* Коэффициент загрузки */\n    double LoadFactor() {\n        return (double)size / capacity;\n    }\n\n    /* Операция поиска */\n    public string? Get(int key) {\n        int index = HashFunc(key);\n        // Обойти корзину; если найден key, вернуть соответствующее val\n        foreach (Pair pair in buckets[index]) {\n            if (pair.key == key) {\n                return pair.val;\n            }\n        }\n        // Если key не найден, вернуть null\n        return null;\n    }\n\n    /* Операция добавления */\n    public void Put(int key, string val) {\n        // Когда коэффициент загрузки превышает порог, выполнить расширение\n        if (LoadFactor() > loadThres) {\n            Extend();\n        }\n        int index = HashFunc(key);\n        // Обойти корзину; если встретился указанный key, обновить соответствующее val и вернуть\n        foreach (Pair pair in buckets[index]) {\n            if (pair.key == key) {\n                pair.val = val;\n                return;\n            }\n        }\n        // Если такого key нет, добавить пару ключ-значение в конец\n        buckets[index].Add(new Pair(key, val));\n        size++;\n    }\n\n    /* Операция удаления */\n    public void Remove(int key) {\n        int index = HashFunc(key);\n        // Обойти корзину и удалить из нее пару ключ-значение\n        foreach (Pair pair in buckets[index].ToList()) {\n            if (pair.key == key) {\n                buckets[index].Remove(pair);\n                size--;\n                break;\n            }\n        }\n    }\n\n    /* Расширить хеш-таблицу */\n    void Extend() {\n        // Временно сохранить исходную хеш-таблицу\n        List<List<Pair>> bucketsTmp = buckets;\n        // Инициализация новой хеш-таблицы после расширения\n        capacity *= extendRatio;\n        buckets = new List<List<Pair>>(capacity);\n        for (int i = 0; i < capacity; i++) {\n            buckets.Add([]);\n        }\n        size = 0;\n        // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n        foreach (List<Pair> bucket in bucketsTmp) {\n            foreach (Pair pair in bucket) {\n                Put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Вывести хеш-таблицу */\n    public void Print() {\n        foreach (List<Pair> bucket in buckets) {\n            List<string> res = [];\n            foreach (Pair pair in bucket) {\n                res.Add(pair.key + \" -> \" + pair.val);\n            }\n            foreach (string kv in res) {\n                Console.WriteLine(kv);\n            }\n        }\n    }\n}\n
        hash_map_chaining.go
        /* Хеш-таблица с цепочками */\ntype hashMapChaining struct {\n    size        int      // Число пар ключ-значение\n    capacity    int      // Вместимость хеш-таблицы\n    loadThres   float64  // Порог коэффициента загрузки для запуска расширения\n    extendRatio int      // Коэффициент расширения\n    buckets     [][]pair // Массив корзин\n}\n\n/* Конструктор */\nfunc newHashMapChaining() *hashMapChaining {\n    buckets := make([][]pair, 4)\n    for i := 0; i < 4; i++ {\n        buckets[i] = make([]pair, 0)\n    }\n    return &hashMapChaining{\n        size:        0,\n        capacity:    4,\n        loadThres:   2.0 / 3.0,\n        extendRatio: 2,\n        buckets:     buckets,\n    }\n}\n\n/* Хеш-функция */\nfunc (m *hashMapChaining) hashFunc(key int) int {\n    return key % m.capacity\n}\n\n/* Коэффициент загрузки */\nfunc (m *hashMapChaining) loadFactor() float64 {\n    return float64(m.size) / float64(m.capacity)\n}\n\n/* Операция поиска */\nfunc (m *hashMapChaining) get(key int) string {\n    idx := m.hashFunc(key)\n    bucket := m.buckets[idx]\n    // Обойти корзину; если найден key, вернуть соответствующее val\n    for _, p := range bucket {\n        if p.key == key {\n            return p.val\n        }\n    }\n    // Если key не найден, вернуть пустую строку\n    return \"\"\n}\n\n/* Операция добавления */\nfunc (m *hashMapChaining) put(key int, val string) {\n    // Когда коэффициент загрузки превышает порог, выполнить расширение\n    if m.loadFactor() > m.loadThres {\n        m.extend()\n    }\n    idx := m.hashFunc(key)\n    // Обойти корзину; если встретился указанный key, обновить соответствующее val и вернуть\n    for i := range m.buckets[idx] {\n        if m.buckets[idx][i].key == key {\n            m.buckets[idx][i].val = val\n            return\n        }\n    }\n    // Если такого key нет, добавить пару ключ-значение в конец\n    p := pair{\n        key: key,\n        val: val,\n    }\n    m.buckets[idx] = append(m.buckets[idx], p)\n    m.size += 1\n}\n\n/* Операция удаления */\nfunc (m *hashMapChaining) remove(key int) {\n    idx := m.hashFunc(key)\n    // Обойти корзину и удалить из нее пару ключ-значение\n    for i, p := range m.buckets[idx] {\n        if p.key == key {\n            // Удаление из среза\n            m.buckets[idx] = append(m.buckets[idx][:i], m.buckets[idx][i+1:]...)\n            m.size -= 1\n            break\n        }\n    }\n}\n\n/* Расширить хеш-таблицу */\nfunc (m *hashMapChaining) extend() {\n    // Временно сохранить исходную хеш-таблицу\n    tmpBuckets := make([][]pair, len(m.buckets))\n    for i := 0; i < len(m.buckets); i++ {\n        tmpBuckets[i] = make([]pair, len(m.buckets[i]))\n        copy(tmpBuckets[i], m.buckets[i])\n    }\n    // Инициализация новой хеш-таблицы после расширения\n    m.capacity *= m.extendRatio\n    m.buckets = make([][]pair, m.capacity)\n    for i := 0; i < m.capacity; i++ {\n        m.buckets[i] = make([]pair, 0)\n    }\n    m.size = 0\n    // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n    for _, bucket := range tmpBuckets {\n        for _, p := range bucket {\n            m.put(p.key, p.val)\n        }\n    }\n}\n\n/* Вывести хеш-таблицу */\nfunc (m *hashMapChaining) print() {\n    var builder strings.Builder\n\n    for _, bucket := range m.buckets {\n        builder.WriteString(\"[\")\n        for _, p := range bucket {\n            builder.WriteString(strconv.Itoa(p.key) + \" -> \" + p.val + \" \")\n        }\n        builder.WriteString(\"]\")\n        fmt.Println(builder.String())\n        builder.Reset()\n    }\n}\n
        hash_map_chaining.swift
        /* Хеш-таблица с цепочками */\nclass HashMapChaining {\n    var size: Int // Число пар ключ-значение\n    var capacity: Int // Вместимость хеш-таблицы\n    var loadThres: Double // Порог коэффициента загрузки для запуска расширения\n    var extendRatio: Int // Коэффициент расширения\n    var buckets: [[Pair]] // Массив корзин\n\n    /* Конструктор */\n    init() {\n        size = 0\n        capacity = 4\n        loadThres = 2.0 / 3.0\n        extendRatio = 2\n        buckets = Array(repeating: [], count: capacity)\n    }\n\n    /* Хеш-функция */\n    func hashFunc(key: Int) -> Int {\n        key % capacity\n    }\n\n    /* Коэффициент загрузки */\n    func loadFactor() -> Double {\n        Double(size) / Double(capacity)\n    }\n\n    /* Операция поиска */\n    func get(key: Int) -> String? {\n        let index = hashFunc(key: key)\n        let bucket = buckets[index]\n        // Обойти корзину; если найден key, вернуть соответствующее val\n        for pair in bucket {\n            if pair.key == key {\n                return pair.val\n            }\n        }\n        // Если key не найден, вернуть nil\n        return nil\n    }\n\n    /* Операция добавления */\n    func put(key: Int, val: String) {\n        // Когда коэффициент загрузки превышает порог, выполнить расширение\n        if loadFactor() > loadThres {\n            extend()\n        }\n        let index = hashFunc(key: key)\n        let bucket = buckets[index]\n        // Обойти корзину; если встретился указанный key, обновить соответствующее val и вернуть\n        for pair in bucket {\n            if pair.key == key {\n                pair.val = val\n                return\n            }\n        }\n        // Если такого key нет, добавить пару ключ-значение в конец\n        let pair = Pair(key: key, val: val)\n        buckets[index].append(pair)\n        size += 1\n    }\n\n    /* Операция удаления */\n    func remove(key: Int) {\n        let index = hashFunc(key: key)\n        let bucket = buckets[index]\n        // Обойти корзину и удалить из нее пару ключ-значение\n        for (pairIndex, pair) in bucket.enumerated() {\n            if pair.key == key {\n                buckets[index].remove(at: pairIndex)\n                size -= 1\n                break\n            }\n        }\n    }\n\n    /* Расширить хеш-таблицу */\n    func extend() {\n        // Временно сохранить исходную хеш-таблицу\n        let bucketsTmp = buckets\n        // Инициализация новой хеш-таблицы после расширения\n        capacity *= extendRatio\n        buckets = Array(repeating: [], count: capacity)\n        size = 0\n        // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n        for bucket in bucketsTmp {\n            for pair in bucket {\n                put(key: pair.key, val: pair.val)\n            }\n        }\n    }\n\n    /* Вывести хеш-таблицу */\n    func print() {\n        for bucket in buckets {\n            let res = bucket.map { \"\\($0.key) -> \\($0.val)\" }\n            Swift.print(res)\n        }\n    }\n}\n
        hash_map_chaining.js
        /* Хеш-таблица с цепочками */\nclass HashMapChaining {\n    #size; // Число пар ключ-значение\n    #capacity; // Вместимость хеш-таблицы\n    #loadThres; // Порог коэффициента загрузки для запуска расширения\n    #extendRatio; // Коэффициент расширения\n    #buckets; // Массив корзин\n\n    /* Конструктор */\n    constructor() {\n        this.#size = 0;\n        this.#capacity = 4;\n        this.#loadThres = 2.0 / 3.0;\n        this.#extendRatio = 2;\n        this.#buckets = new Array(this.#capacity).fill(null).map((x) => []);\n    }\n\n    /* Хеш-функция */\n    #hashFunc(key) {\n        return key % this.#capacity;\n    }\n\n    /* Коэффициент загрузки */\n    #loadFactor() {\n        return this.#size / this.#capacity;\n    }\n\n    /* Операция поиска */\n    get(key) {\n        const index = this.#hashFunc(key);\n        const bucket = this.#buckets[index];\n        // Обойти корзину; если найден key, вернуть соответствующее val\n        for (const pair of bucket) {\n            if (pair.key === key) {\n                return pair.val;\n            }\n        }\n        // Если key не найден, вернуть null\n        return null;\n    }\n\n    /* Операция добавления */\n    put(key, val) {\n        // Когда коэффициент загрузки превышает порог, выполнить расширение\n        if (this.#loadFactor() > this.#loadThres) {\n            this.#extend();\n        }\n        const index = this.#hashFunc(key);\n        const bucket = this.#buckets[index];\n        // Обойти корзину; если встретился указанный key, обновить соответствующее val и вернуть\n        for (const pair of bucket) {\n            if (pair.key === key) {\n                pair.val = val;\n                return;\n            }\n        }\n        // Если такого key нет, добавить пару ключ-значение в конец\n        const pair = new Pair(key, val);\n        bucket.push(pair);\n        this.#size++;\n    }\n\n    /* Операция удаления */\n    remove(key) {\n        const index = this.#hashFunc(key);\n        let bucket = this.#buckets[index];\n        // Обойти корзину и удалить из нее пару ключ-значение\n        for (let i = 0; i < bucket.length; i++) {\n            if (bucket[i].key === key) {\n                bucket.splice(i, 1);\n                this.#size--;\n                break;\n            }\n        }\n    }\n\n    /* Расширить хеш-таблицу */\n    #extend() {\n        // Временно сохранить исходную хеш-таблицу\n        const bucketsTmp = this.#buckets;\n        // Инициализация новой хеш-таблицы после расширения\n        this.#capacity *= this.#extendRatio;\n        this.#buckets = new Array(this.#capacity).fill(null).map((x) => []);\n        this.#size = 0;\n        // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n        for (const bucket of bucketsTmp) {\n            for (const pair of bucket) {\n                this.put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Вывести хеш-таблицу */\n    print() {\n        for (const bucket of this.#buckets) {\n            let res = [];\n            for (const pair of bucket) {\n                res.push(pair.key + ' -> ' + pair.val);\n            }\n            console.log(res);\n        }\n    }\n}\n
        hash_map_chaining.ts
        /* Хеш-таблица с цепочками */\nclass HashMapChaining {\n    #size: number; // Число пар ключ-значение\n    #capacity: number; // Вместимость хеш-таблицы\n    #loadThres: number; // Порог коэффициента загрузки для запуска расширения\n    #extendRatio: number; // Коэффициент расширения\n    #buckets: Pair[][]; // Массив корзин\n\n    /* Конструктор */\n    constructor() {\n        this.#size = 0;\n        this.#capacity = 4;\n        this.#loadThres = 2.0 / 3.0;\n        this.#extendRatio = 2;\n        this.#buckets = new Array(this.#capacity).fill(null).map((x) => []);\n    }\n\n    /* Хеш-функция */\n    #hashFunc(key: number): number {\n        return key % this.#capacity;\n    }\n\n    /* Коэффициент загрузки */\n    #loadFactor(): number {\n        return this.#size / this.#capacity;\n    }\n\n    /* Операция поиска */\n    get(key: number): string | null {\n        const index = this.#hashFunc(key);\n        const bucket = this.#buckets[index];\n        // Обойти корзину; если найден key, вернуть соответствующее val\n        for (const pair of bucket) {\n            if (pair.key === key) {\n                return pair.val;\n            }\n        }\n        // Если key не найден, вернуть null\n        return null;\n    }\n\n    /* Операция добавления */\n    put(key: number, val: string): void {\n        // Когда коэффициент загрузки превышает порог, выполнить расширение\n        if (this.#loadFactor() > this.#loadThres) {\n            this.#extend();\n        }\n        const index = this.#hashFunc(key);\n        const bucket = this.#buckets[index];\n        // Обойти корзину; если встретился указанный key, обновить соответствующее val и вернуть\n        for (const pair of bucket) {\n            if (pair.key === key) {\n                pair.val = val;\n                return;\n            }\n        }\n        // Если такого key нет, добавить пару ключ-значение в конец\n        const pair = new Pair(key, val);\n        bucket.push(pair);\n        this.#size++;\n    }\n\n    /* Операция удаления */\n    remove(key: number): void {\n        const index = this.#hashFunc(key);\n        let bucket = this.#buckets[index];\n        // Обойти корзину и удалить из нее пару ключ-значение\n        for (let i = 0; i < bucket.length; i++) {\n            if (bucket[i].key === key) {\n                bucket.splice(i, 1);\n                this.#size--;\n                break;\n            }\n        }\n    }\n\n    /* Расширить хеш-таблицу */\n    #extend(): void {\n        // Временно сохранить исходную хеш-таблицу\n        const bucketsTmp = this.#buckets;\n        // Инициализация новой хеш-таблицы после расширения\n        this.#capacity *= this.#extendRatio;\n        this.#buckets = new Array(this.#capacity).fill(null).map((x) => []);\n        this.#size = 0;\n        // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n        for (const bucket of bucketsTmp) {\n            for (const pair of bucket) {\n                this.put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Вывести хеш-таблицу */\n    print(): void {\n        for (const bucket of this.#buckets) {\n            let res = [];\n            for (const pair of bucket) {\n                res.push(pair.key + ' -> ' + pair.val);\n            }\n            console.log(res);\n        }\n    }\n}\n
        hash_map_chaining.dart
        /* Хеш-таблица с цепочками */\nclass HashMapChaining {\n  late int size; // Число пар ключ-значение\n  late int capacity; // Вместимость хеш-таблицы\n  late double loadThres; // Порог коэффициента загрузки для запуска расширения\n  late int extendRatio; // Коэффициент расширения\n  late List<List<Pair>> buckets; // Массив корзин\n\n  /* Конструктор */\n  HashMapChaining() {\n    size = 0;\n    capacity = 4;\n    loadThres = 2.0 / 3.0;\n    extendRatio = 2;\n    buckets = List.generate(capacity, (_) => []);\n  }\n\n  /* Хеш-функция */\n  int hashFunc(int key) {\n    return key % capacity;\n  }\n\n  /* Коэффициент загрузки */\n  double loadFactor() {\n    return size / capacity;\n  }\n\n  /* Операция поиска */\n  String? get(int key) {\n    int index = hashFunc(key);\n    List<Pair> bucket = buckets[index];\n    // Обойти корзину; если найден key, вернуть соответствующее val\n    for (Pair pair in bucket) {\n      if (pair.key == key) {\n        return pair.val;\n      }\n    }\n    // Если key не найден, вернуть null\n    return null;\n  }\n\n  /* Операция добавления */\n  void put(int key, String val) {\n    // Когда коэффициент загрузки превышает порог, выполнить расширение\n    if (loadFactor() > loadThres) {\n      extend();\n    }\n    int index = hashFunc(key);\n    List<Pair> bucket = buckets[index];\n    // Обойти корзину; если встретился указанный key, обновить соответствующее val и вернуть\n    for (Pair pair in bucket) {\n      if (pair.key == key) {\n        pair.val = val;\n        return;\n      }\n    }\n    // Если такого key нет, добавить пару ключ-значение в конец\n    Pair pair = Pair(key, val);\n    bucket.add(pair);\n    size++;\n  }\n\n  /* Операция удаления */\n  void remove(int key) {\n    int index = hashFunc(key);\n    List<Pair> bucket = buckets[index];\n    // Обойти корзину и удалить из нее пару ключ-значение\n    for (Pair pair in bucket) {\n      if (pair.key == key) {\n        bucket.remove(pair);\n        size--;\n        break;\n      }\n    }\n  }\n\n  /* Расширить хеш-таблицу */\n  void extend() {\n    // Временно сохранить исходную хеш-таблицу\n    List<List<Pair>> bucketsTmp = buckets;\n    // Инициализация новой хеш-таблицы после расширения\n    capacity *= extendRatio;\n    buckets = List.generate(capacity, (_) => []);\n    size = 0;\n    // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n    for (List<Pair> bucket in bucketsTmp) {\n      for (Pair pair in bucket) {\n        put(pair.key, pair.val);\n      }\n    }\n  }\n\n  /* Вывести хеш-таблицу */\n  void printHashMap() {\n    for (List<Pair> bucket in buckets) {\n      List<String> res = [];\n      for (Pair pair in bucket) {\n        res.add(\"${pair.key} -> ${pair.val}\");\n      }\n      print(res);\n    }\n  }\n}\n
        hash_map_chaining.rs
        /* Хеш-таблица с цепочками */\nstruct HashMapChaining {\n    size: usize,\n    capacity: usize,\n    load_thres: f32,\n    extend_ratio: usize,\n    buckets: Vec<Vec<Pair>>,\n}\n\nimpl HashMapChaining {\n    /* Конструктор */\n    fn new() -> Self {\n        Self {\n            size: 0,\n            capacity: 4,\n            load_thres: 2.0 / 3.0,\n            extend_ratio: 2,\n            buckets: vec![vec![]; 4],\n        }\n    }\n\n    /* Хеш-функция */\n    fn hash_func(&self, key: i32) -> usize {\n        key as usize % self.capacity\n    }\n\n    /* Коэффициент загрузки */\n    fn load_factor(&self) -> f32 {\n        self.size as f32 / self.capacity as f32\n    }\n\n    /* Операция удаления */\n    fn remove(&mut self, key: i32) -> Option<String> {\n        let index = self.hash_func(key);\n\n        // Обойти корзину и удалить из нее пару ключ-значение\n        for (i, p) in self.buckets[index].iter_mut().enumerate() {\n            if p.key == key {\n                let pair = self.buckets[index].remove(i);\n                self.size -= 1;\n                return Some(pair.val);\n            }\n        }\n\n        // Если key не найден, вернуть None\n        None\n    }\n\n    /* Расширить хеш-таблицу */\n    fn extend(&mut self) {\n        // Временно сохранить исходную хеш-таблицу\n        let buckets_tmp = std::mem::take(&mut self.buckets);\n\n        // Инициализация новой хеш-таблицы после расширения\n        self.capacity *= self.extend_ratio;\n        self.buckets = vec![Vec::new(); self.capacity as usize];\n        self.size = 0;\n\n        // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n        for bucket in buckets_tmp {\n            for pair in bucket {\n                self.put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Вывести хеш-таблицу */\n    fn print(&self) {\n        for bucket in &self.buckets {\n            let mut res = Vec::new();\n            for pair in bucket {\n                res.push(format!(\"{} -> {}\", pair.key, pair.val));\n            }\n            println!(\"{:?}\", res);\n        }\n    }\n\n    /* Операция добавления */\n    fn put(&mut self, key: i32, val: String) {\n        // Когда коэффициент загрузки превышает порог, выполнить расширение\n        if self.load_factor() > self.load_thres {\n            self.extend();\n        }\n\n        let index = self.hash_func(key);\n\n        // Обойти корзину; если встретился указанный key, обновить соответствующее val и вернуть\n        for pair in self.buckets[index].iter_mut() {\n            if pair.key == key {\n                pair.val = val;\n                return;\n            }\n        }\n\n        // Если такого key нет, добавить пару ключ-значение в конец\n        let pair = Pair { key, val };\n        self.buckets[index].push(pair);\n        self.size += 1;\n    }\n\n    /* Операция поиска */\n    fn get(&self, key: i32) -> Option<&str> {\n        let index = self.hash_func(key);\n\n        // Обойти корзину; если найден key, вернуть соответствующее val\n        for pair in self.buckets[index].iter() {\n            if pair.key == key {\n                return Some(&pair.val);\n            }\n        }\n\n        // Если key не найден, вернуть None\n        None\n    }\n}\n
        hash_map_chaining.c
        /* Узел связного списка */\ntypedef struct Node {\n    Pair *pair;\n    struct Node *next;\n} Node;\n\n/* Хеш-таблица с цепочками */\ntypedef struct {\n    int size;         // Число пар ключ-значение\n    int capacity;     // Вместимость хеш-таблицы\n    double loadThres; // Порог коэффициента загрузки для запуска расширения\n    int extendRatio;  // Коэффициент расширения\n    Node **buckets;   // Массив корзин\n} HashMapChaining;\n\n/* Конструктор */\nHashMapChaining *newHashMapChaining() {\n    HashMapChaining *hashMap = (HashMapChaining *)malloc(sizeof(HashMapChaining));\n    hashMap->size = 0;\n    hashMap->capacity = 4;\n    hashMap->loadThres = 2.0 / 3.0;\n    hashMap->extendRatio = 2;\n    hashMap->buckets = (Node **)malloc(hashMap->capacity * sizeof(Node *));\n    for (int i = 0; i < hashMap->capacity; i++) {\n        hashMap->buckets[i] = NULL;\n    }\n    return hashMap;\n}\n\n/* Деструктор */\nvoid delHashMapChaining(HashMapChaining *hashMap) {\n    for (int i = 0; i < hashMap->capacity; i++) {\n        Node *cur = hashMap->buckets[i];\n        while (cur) {\n            Node *tmp = cur;\n            cur = cur->next;\n            free(tmp->pair);\n            free(tmp);\n        }\n    }\n    free(hashMap->buckets);\n    free(hashMap);\n}\n\n/* Хеш-функция */\nint hashFunc(HashMapChaining *hashMap, int key) {\n    return key % hashMap->capacity;\n}\n\n/* Коэффициент загрузки */\ndouble loadFactor(HashMapChaining *hashMap) {\n    return (double)hashMap->size / (double)hashMap->capacity;\n}\n\n/* Операция поиска */\nchar *get(HashMapChaining *hashMap, int key) {\n    int index = hashFunc(hashMap, key);\n    // Обойти корзину; если найден key, вернуть соответствующее val\n    Node *cur = hashMap->buckets[index];\n    while (cur) {\n        if (cur->pair->key == key) {\n            return cur->pair->val;\n        }\n        cur = cur->next;\n    }\n    return \"\"; // Если key не найден, вернуть пустую строку\n}\n\n/* Операция добавления */\nvoid put(HashMapChaining *hashMap, int key, const char *val) {\n    // Когда коэффициент загрузки превышает порог, выполнить расширение\n    if (loadFactor(hashMap) > hashMap->loadThres) {\n        extend(hashMap);\n    }\n    int index = hashFunc(hashMap, key);\n    // Обойти корзину; если встретился указанный key, обновить соответствующее val и вернуть\n    Node *cur = hashMap->buckets[index];\n    while (cur) {\n        if (cur->pair->key == key) {\n            strcpy(cur->pair->val, val); // Если встретился указанный key, обновить соответствующий val и вернуть\n            return;\n        }\n        cur = cur->next;\n    }\n    // Если такого key нет, добавить пару ключ-значение в голову связного списка\n    Pair *newPair = (Pair *)malloc(sizeof(Pair));\n    newPair->key = key;\n    strcpy(newPair->val, val);\n    Node *newNode = (Node *)malloc(sizeof(Node));\n    newNode->pair = newPair;\n    newNode->next = hashMap->buckets[index];\n    hashMap->buckets[index] = newNode;\n    hashMap->size++;\n}\n\n/* Расширить хеш-таблицу */\nvoid extend(HashMapChaining *hashMap) {\n    // Временно сохранить исходную хеш-таблицу\n    int oldCapacity = hashMap->capacity;\n    Node **oldBuckets = hashMap->buckets;\n    // Инициализация новой хеш-таблицы после расширения\n    hashMap->capacity *= hashMap->extendRatio;\n    hashMap->buckets = (Node **)malloc(hashMap->capacity * sizeof(Node *));\n    for (int i = 0; i < hashMap->capacity; i++) {\n        hashMap->buckets[i] = NULL;\n    }\n    hashMap->size = 0;\n    // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n    for (int i = 0; i < oldCapacity; i++) {\n        Node *cur = oldBuckets[i];\n        while (cur) {\n            put(hashMap, cur->pair->key, cur->pair->val);\n            Node *temp = cur;\n            cur = cur->next;\n            // Освободить память\n            free(temp->pair);\n            free(temp);\n        }\n    }\n\n    free(oldBuckets);\n}\n\n/* Операция удаления */\nvoid removeItem(HashMapChaining *hashMap, int key) {\n    int index = hashFunc(hashMap, key);\n    Node *cur = hashMap->buckets[index];\n    Node *pre = NULL;\n    while (cur) {\n        if (cur->pair->key == key) {\n            // Удалить из него пару ключ-значение\n            if (pre) {\n                pre->next = cur->next;\n            } else {\n                hashMap->buckets[index] = cur->next;\n            }\n            // Освободить память\n            free(cur->pair);\n            free(cur);\n            hashMap->size--;\n            return;\n        }\n        pre = cur;\n        cur = cur->next;\n    }\n}\n\n/* Вывести хеш-таблицу */\nvoid print(HashMapChaining *hashMap) {\n    for (int i = 0; i < hashMap->capacity; i++) {\n        Node *cur = hashMap->buckets[i];\n        printf(\"[\");\n        while (cur) {\n            printf(\"%d -> %s, \", cur->pair->key, cur->pair->val);\n            cur = cur->next;\n        }\n        printf(\"]\\n\");\n    }\n}\n
        hash_map_chaining.kt
        /* Хеш-таблица с цепочками */\nclass HashMapChaining {\n    var size: Int // Число пар ключ-значение\n    var capacity: Int // Вместимость хеш-таблицы\n    val loadThres: Double // Порог коэффициента загрузки для запуска расширения\n    val extendRatio: Int // Коэффициент расширения\n    var buckets: MutableList<MutableList<Pair>> // Массив корзин\n\n    /* Конструктор */\n    init {\n        size = 0\n        capacity = 4\n        loadThres = 2.0 / 3.0\n        extendRatio = 2\n        buckets = mutableListOf()\n        for (i in 0..<capacity) {\n            buckets.add(mutableListOf())\n        }\n    }\n\n    /* Хеш-функция */\n    fun hashFunc(key: Int): Int {\n        return key % capacity\n    }\n\n    /* Коэффициент загрузки */\n    fun loadFactor(): Double {\n        return (size / capacity).toDouble()\n    }\n\n    /* Операция поиска */\n    fun get(key: Int): String? {\n        val index = hashFunc(key)\n        val bucket = buckets[index]\n        // Обойти корзину; если найден key, вернуть соответствующее val\n        for (pair in bucket) {\n            if (pair.key == key) return pair._val\n        }\n        // Если key не найден, вернуть null\n        return null\n    }\n\n    /* Операция добавления */\n    fun put(key: Int, _val: String) {\n        // Когда коэффициент загрузки превышает порог, выполнить расширение\n        if (loadFactor() > loadThres) {\n            extend()\n        }\n        val index = hashFunc(key)\n        val bucket = buckets[index]\n        // Обойти корзину; если встретился указанный key, обновить соответствующее val и вернуть\n        for (pair in bucket) {\n            if (pair.key == key) {\n                pair._val = _val\n                return\n            }\n        }\n        // Если такого key нет, добавить пару ключ-значение в конец\n        val pair = Pair(key, _val)\n        bucket.add(pair)\n        size++\n    }\n\n    /* Операция удаления */\n    fun remove(key: Int) {\n        val index = hashFunc(key)\n        val bucket = buckets[index]\n        // Обойти корзину и удалить из нее пару ключ-значение\n        for (pair in bucket) {\n            if (pair.key == key) {\n                bucket.remove(pair)\n                size--\n                break\n            }\n        }\n    }\n\n    /* Расширить хеш-таблицу */\n    fun extend() {\n        // Временно сохранить исходную хеш-таблицу\n        val bucketsTmp = buckets\n        // Инициализация новой хеш-таблицы после расширения\n        capacity *= extendRatio\n        // mutablelist не имеет фиксированного размера\n        buckets = mutableListOf()\n        for (i in 0..<capacity) {\n            buckets.add(mutableListOf())\n        }\n        size = 0\n        // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n        for (bucket in bucketsTmp) {\n            for (pair in bucket) {\n                put(pair.key, pair._val)\n            }\n        }\n    }\n\n    /* Вывести хеш-таблицу */\n    fun print() {\n        for (bucket in buckets) {\n            val res = mutableListOf<String>()\n            for (pair in bucket) {\n                val k = pair.key\n                val v = pair._val\n                res.add(\"$k -> $v\")\n            }\n            println(res)\n        }\n    }\n}\n
        hash_map_chaining.rb
        ### Хеш-таблица с цепочками ###\nclass HashMapChaining\n  ### Конструктор ###\n  def initialize\n    @size = 0 # Число пар ключ-значение\n    @capacity = 4 # Вместимость хеш-таблицы\n    @load_thres = 2.0 / 3.0 # Порог коэффициента загрузки для запуска расширения\n    @extend_ratio = 2 # Коэффициент расширения\n    @buckets = Array.new(@capacity) { [] } # Массив корзин\n  end\n\n  ### Хеш-функция ###\n  def hash_func(key)\n    key % @capacity\n  end\n\n  ### Коэффициент загрузки ###\n  def load_factor\n    @size / @capacity\n  end\n\n  ### Операция поиска ###\n  def get(key)\n    index = hash_func(key)\n    bucket = @buckets[index]\n    # Обойти корзину; если найден key, вернуть соответствующее val\n    for pair in bucket\n      return pair.val if pair.key == key\n    end\n    # Если key не найден, вернуть nil\n    nil\n  end\n\n  ### Операция добавления ###\n  def put(key, val)\n    # Когда коэффициент загрузки превышает порог, выполнить расширение\n    extend if load_factor > @load_thres\n    index = hash_func(key)\n    bucket = @buckets[index]\n    # Обойти корзину; если встретился указанный key, обновить соответствующее val и вернуть\n    for pair in bucket\n      if pair.key == key\n        pair.val = val\n        return\n      end\n    end\n    # Если такого key нет, добавить пару ключ-значение в конец\n    pair = Pair.new(key, val)\n    bucket << pair\n    @size += 1\n  end\n\n  ### Операция удаления ###\n  def remove(key)\n    index = hash_func(key)\n    bucket = @buckets[index]\n    # Обойти корзину и удалить из нее пару ключ-значение\n    for pair in bucket\n      if pair.key == key\n        bucket.delete(pair)\n        @size -= 1\n        break\n      end\n    end\n  end\n\n  ### Расширение хеш-таблицы ###\n  def extend\n    # Временно сохранить исходную хеш-таблицу\n    buckets = @buckets\n    # Инициализация новой хеш-таблицы после расширения\n    @capacity *= @extend_ratio\n    @buckets = Array.new(@capacity) { [] }\n    @size = 0\n    # Перенести пары ключ-значение из исходной хеш-таблицы в новую\n    for bucket in buckets\n      for pair in bucket\n        put(pair.key, pair.val)\n      end\n    end\n  end\n\n  ### Вывести хеш-таблицу ###\n  def print\n    for bucket in @buckets\n      res = []\n      for pair in bucket\n        res << \"#{pair.key} -> #{pair.val}\"\n      end\n      pp res\n    end\n  end\nend\n
        Визуализация кода

        Во весь экран >

        Следует отметить, что когда связный список становится очень длинным, эффективность поиска \\(O(n)\\) оказывается низкой. В этом случае список можно преобразовать в AVL-дерево или красно-черное дерево , чтобы оптимизировать временную сложность поиска до \\(O(\\log n)\\) .

        ","path":["Глава 6. Хеш-таблицы","6.2   Хеш-коллизии"],"tags":[]},{"location":"chapter_hashing/hash_collision/#622","level":2,"title":"6.2.2   Открытая адресация","text":"

        Открытая адресация (open addressing) не вводит дополнительных структур данных, а обрабатывает хеш-коллизии с помощью многократного пробирования; основные варианты пробирования включают линейное пробирование, квадратичное пробирование и повторное хеширование.

        Ниже на примере линейного пробирования рассмотрим механизм работы хеш-таблицы с открытой адресацией.

        ","path":["Глава 6. Хеш-таблицы","6.2   Хеш-коллизии"],"tags":[]},{"location":"chapter_hashing/hash_collision/#1","level":3,"title":"1.   Линейное пробирование","text":"

        Линейное пробирование использует линейный поиск с фиксированным шагом. Его методы работы отличаются от обычной хеш-таблицы.

        • Вставка элемента: по хеш-функции вычисляется индекс бакета; если бакет уже занят, то от места конфликта выполняется линейный обход вперед (шаг обычно равен \\(1\\) ), пока не будет найден пустой бакет, после чего элемент вставляется туда.
        • Поиск элемента: если возник конфликт, то с тем же шагом продолжается линейный обход вперед, пока не будет найден целевой элемент и возвращено value ; если встречается пустой бакет, это означает, что искомого элемента в хеш-таблице нет, и возвращается None .

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

        Рисунок 6-6   Распределение пар ключ-значение в хеш-таблице с открытой адресацией (линейное пробирование)

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

        Стоит заметить, что мы не можем напрямую удалять элементы из хеш-таблицы с открытой адресацией. Причина в том, что удаление создаст внутри массива пустой бакет None , а при поиске элемента линейное пробирование остановится на этом пустом бакете и вернет результат, из-за чего элементы ниже этого бакета уже не смогут быть найдены, и программа может ошибочно посчитать, что их не существует, как показано на рисунке 6-7.

        Рисунок 6-7   Проблема поиска после удаления элемента в открытой адресации

        Чтобы решить эту проблему, можно использовать механизм ленивого удаления (lazy deletion): он не удаляет элемент из хеш-таблицы напрямую, **а помечает этот бакет специальной константой TOMBSTONE **. В этом механизме и None , и TOMBSTONE означают пустой бакет, и оба могут быть использованы для размещения пары ключ-значение. Но есть важное различие: при линейном пробировании, встретив TOMBSTONE , нужно продолжать обход, потому что ниже него все еще могут существовать пары ключ-значение.

        Однако ленивое удаление может ускорять деградацию производительности хеш-таблицы. Это связано с тем, что каждая операция удаления создает новую метку удаления; по мере роста числа TOMBSTONE время поиска тоже увеличивается, потому что линейное пробирование может быть вынуждено перескакивать через множество TOMBSTONE , прежде чем найдет целевой элемент.

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

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

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map_open_addressing.py
        class HashMapOpenAddressing:\n    \"\"\"Хеш-таблица с открытой адресацией\"\"\"\n\n    def __init__(self):\n        \"\"\"Конструктор\"\"\"\n        self.size = 0  # Число пар ключ-значение\n        self.capacity = 4  # Вместимость хеш-таблицы\n        self.load_thres = 2.0 / 3.0  # Порог коэффициента загрузки для запуска расширения\n        self.extend_ratio = 2  # Коэффициент расширения\n        self.buckets: list[Pair | None] = [None] * self.capacity  # Массив корзин\n        self.TOMBSTONE = Pair(-1, \"-1\")  # Удалить метку\n\n    def hash_func(self, key: int) -> int:\n        \"\"\"Хеш-функция\"\"\"\n        return key % self.capacity\n\n    def load_factor(self) -> float:\n        \"\"\"Коэффициент загрузки\"\"\"\n        return self.size / self.capacity\n\n    def find_bucket(self, key: int) -> int:\n        \"\"\"Найти индекс корзины, соответствующий key\"\"\"\n        index = self.hash_func(key)\n        first_tombstone = -1\n        # Выполнять линейное пробирование и завершить при встрече с пустой корзиной\n        while self.buckets[index] is not None:\n            # Если встретился key, вернуть соответствующий индекс корзины\n            if self.buckets[index].key == key:\n                # Если ранее встретилась метка удаления, переместить пару ключ-значение на этот индекс\n                if first_tombstone != -1:\n                    self.buckets[first_tombstone] = self.buckets[index]\n                    self.buckets[index] = self.TOMBSTONE\n                    return first_tombstone  # Вернуть индекс корзины после перемещения\n                return index  # Вернуть индекс корзины\n            # Записать первую встретившуюся метку удаления\n            if first_tombstone == -1 and self.buckets[index] is self.TOMBSTONE:\n                first_tombstone = index\n            # Вычислить индекс корзины; при выходе за конец вернуться к началу\n            index = (index + 1) % self.capacity\n        # Если key не существует, вернуть индекс точки добавления\n        return index if first_tombstone == -1 else first_tombstone\n\n    def get(self, key: int) -> str:\n        \"\"\"Операция поиска\"\"\"\n        # Найти индекс корзины, соответствующий key\n        index = self.find_bucket(key)\n        # Если пара ключ-значение найдена, вернуть соответствующее val\n        if self.buckets[index] not in [None, self.TOMBSTONE]:\n            return self.buckets[index].val\n        # Если пара ключ-значение не существует, вернуть None\n        return None\n\n    def put(self, key: int, val: str):\n        \"\"\"Операция добавления\"\"\"\n        # Когда коэффициент загрузки превышает порог, выполнить расширение\n        if self.load_factor() > self.load_thres:\n            self.extend()\n        # Найти индекс корзины, соответствующий key\n        index = self.find_bucket(key)\n        # Если пара ключ-значение найдена, перезаписать val и вернуть\n        if self.buckets[index] not in [None, self.TOMBSTONE]:\n            self.buckets[index].val = val\n            return\n        # Если пары ключ-значение нет, добавить ее\n        self.buckets[index] = Pair(key, val)\n        self.size += 1\n\n    def remove(self, key: int):\n        \"\"\"Операция удаления\"\"\"\n        # Найти индекс корзины, соответствующий key\n        index = self.find_bucket(key)\n        # Если пара ключ-значение найдена, заменить ее меткой удаления\n        if self.buckets[index] not in [None, self.TOMBSTONE]:\n            self.buckets[index] = self.TOMBSTONE\n            self.size -= 1\n\n    def extend(self):\n        \"\"\"Расширить хеш-таблицу\"\"\"\n        # Временно сохранить исходную хеш-таблицу\n        buckets_tmp = self.buckets\n        # Инициализация новой хеш-таблицы после расширения\n        self.capacity *= self.extend_ratio\n        self.buckets = [None] * self.capacity\n        self.size = 0\n        # Перенести пары ключ-значение из исходной хеш-таблицы в новую\n        for pair in buckets_tmp:\n            if pair not in [None, self.TOMBSTONE]:\n                self.put(pair.key, pair.val)\n\n    def print(self):\n        \"\"\"Вывести хеш-таблицу\"\"\"\n        for pair in self.buckets:\n            if pair is None:\n                print(\"None\")\n            elif pair is self.TOMBSTONE:\n                print(\"TOMBSTONE\")\n            else:\n                print(pair.key, \"->\", pair.val)\n
        hash_map_open_addressing.cpp
        /* Хеш-таблица с открытой адресацией */\nclass HashMapOpenAddressing {\n  private:\n    int size;                             // Число пар ключ-значение\n    int capacity = 4;                     // Вместимость хеш-таблицы\n    const double loadThres = 2.0 / 3.0;     // Порог коэффициента загрузки для запуска расширения\n    const int extendRatio = 2;            // Коэффициент расширения\n    vector<Pair *> buckets;               // Массив корзин\n    Pair *TOMBSTONE = new Pair(-1, \"-1\"); // Удалить метку\n\n  public:\n    /* Конструктор */\n    HashMapOpenAddressing() : size(0), buckets(capacity, nullptr) {\n    }\n\n    /* Метод-деструктор */\n    ~HashMapOpenAddressing() {\n        for (Pair *pair : buckets) {\n            if (pair != nullptr && pair != TOMBSTONE) {\n                delete pair;\n            }\n        }\n        delete TOMBSTONE;\n    }\n\n    /* Хеш-функция */\n    int hashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* Коэффициент загрузки */\n    double loadFactor() {\n        return (double)size / capacity;\n    }\n\n    /* Найти индекс корзины, соответствующий key */\n    int findBucket(int key) {\n        int index = hashFunc(key);\n        int firstTombstone = -1;\n        // Выполнять линейное пробирование и завершить при встрече с пустой корзиной\n        while (buckets[index] != nullptr) {\n            // Если встретился key, вернуть соответствующий индекс корзины\n            if (buckets[index]->key == key) {\n                // Если ранее встретилась метка удаления, переместить пару ключ-значение на этот индекс\n                if (firstTombstone != -1) {\n                    buckets[firstTombstone] = buckets[index];\n                    buckets[index] = TOMBSTONE;\n                    return firstTombstone; // Вернуть индекс корзины после перемещения\n                }\n                return index; // Вернуть индекс корзины\n            }\n            // Записать первую встретившуюся метку удаления\n            if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {\n                firstTombstone = index;\n            }\n            // Вычислить индекс корзины; при выходе за конец вернуться к началу\n            index = (index + 1) % capacity;\n        }\n        // Если key не существует, вернуть индекс точки добавления\n        return firstTombstone == -1 ? index : firstTombstone;\n    }\n\n    /* Операция поиска */\n    string get(int key) {\n        // Найти индекс корзины, соответствующий key\n        int index = findBucket(key);\n        // Если пара ключ-значение найдена, вернуть соответствующее val\n        if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {\n            return buckets[index]->val;\n        }\n        // Если пары ключ-значение не существует, вернуть пустую строку\n        return \"\";\n    }\n\n    /* Операция добавления */\n    void put(int key, string val) {\n        // Когда коэффициент загрузки превышает порог, выполнить расширение\n        if (loadFactor() > loadThres) {\n            extend();\n        }\n        // Найти индекс корзины, соответствующий key\n        int index = findBucket(key);\n        // Если пара ключ-значение найдена, перезаписать val и вернуть\n        if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {\n            buckets[index]->val = val;\n            return;\n        }\n        // Если пары ключ-значение нет, добавить ее\n        buckets[index] = new Pair(key, val);\n        size++;\n    }\n\n    /* Операция удаления */\n    void remove(int key) {\n        // Найти индекс корзины, соответствующий key\n        int index = findBucket(key);\n        // Если пара ключ-значение найдена, заменить ее меткой удаления\n        if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {\n            delete buckets[index];\n            buckets[index] = TOMBSTONE;\n            size--;\n        }\n    }\n\n    /* Расширить хеш-таблицу */\n    void extend() {\n        // Временно сохранить исходную хеш-таблицу\n        vector<Pair *> bucketsTmp = buckets;\n        // Инициализация новой хеш-таблицы после расширения\n        capacity *= extendRatio;\n        buckets = vector<Pair *>(capacity, nullptr);\n        size = 0;\n        // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n        for (Pair *pair : bucketsTmp) {\n            if (pair != nullptr && pair != TOMBSTONE) {\n                put(pair->key, pair->val);\n                delete pair;\n            }\n        }\n    }\n\n    /* Вывести хеш-таблицу */\n    void print() {\n        for (Pair *pair : buckets) {\n            if (pair == nullptr) {\n                cout << \"nullptr\" << endl;\n            } else if (pair == TOMBSTONE) {\n                cout << \"TOMBSTONE\" << endl;\n            } else {\n                cout << pair->key << \" -> \" << pair->val << endl;\n            }\n        }\n    }\n};\n
        hash_map_open_addressing.java
        /* Хеш-таблица с открытой адресацией */\nclass HashMapOpenAddressing {\n    private int size; // Число пар ключ-значение\n    private int capacity = 4; // Вместимость хеш-таблицы\n    private final double loadThres = 2.0 / 3.0; // Порог коэффициента загрузки для запуска расширения\n    private final int extendRatio = 2; // Коэффициент расширения\n    private Pair[] buckets; // Массив корзин\n    private final Pair TOMBSTONE = new Pair(-1, \"-1\"); // Удалить метку\n\n    /* Конструктор */\n    public HashMapOpenAddressing() {\n        size = 0;\n        buckets = new Pair[capacity];\n    }\n\n    /* Хеш-функция */\n    private int hashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* Коэффициент загрузки */\n    private double loadFactor() {\n        return (double) size / capacity;\n    }\n\n    /* Найти индекс корзины, соответствующий key */\n    private int findBucket(int key) {\n        int index = hashFunc(key);\n        int firstTombstone = -1;\n        // Выполнять линейное пробирование и завершить при встрече с пустой корзиной\n        while (buckets[index] != null) {\n            // Если встретился key, вернуть соответствующий индекс корзины\n            if (buckets[index].key == key) {\n                // Если ранее встретилась метка удаления, переместить пару ключ-значение на этот индекс\n                if (firstTombstone != -1) {\n                    buckets[firstTombstone] = buckets[index];\n                    buckets[index] = TOMBSTONE;\n                    return firstTombstone; // Вернуть индекс корзины после перемещения\n                }\n                return index; // Вернуть индекс корзины\n            }\n            // Записать первую встретившуюся метку удаления\n            if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {\n                firstTombstone = index;\n            }\n            // Вычислить индекс корзины; при выходе за конец вернуться к началу\n            index = (index + 1) % capacity;\n        }\n        // Если key не существует, вернуть индекс точки добавления\n        return firstTombstone == -1 ? index : firstTombstone;\n    }\n\n    /* Операция поиска */\n    public String get(int key) {\n        // Найти индекс корзины, соответствующий key\n        int index = findBucket(key);\n        // Если пара ключ-значение найдена, вернуть соответствующее val\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            return buckets[index].val;\n        }\n        // Если пары ключ-значение не существует, вернуть null\n        return null;\n    }\n\n    /* Операция добавления */\n    public void put(int key, String val) {\n        // Когда коэффициент загрузки превышает порог, выполнить расширение\n        if (loadFactor() > loadThres) {\n            extend();\n        }\n        // Найти индекс корзины, соответствующий key\n        int index = findBucket(key);\n        // Если пара ключ-значение найдена, перезаписать val и вернуть\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index].val = val;\n            return;\n        }\n        // Если пары ключ-значение нет, добавить ее\n        buckets[index] = new Pair(key, val);\n        size++;\n    }\n\n    /* Операция удаления */\n    public void remove(int key) {\n        // Найти индекс корзины, соответствующий key\n        int index = findBucket(key);\n        // Если пара ключ-значение найдена, заменить ее меткой удаления\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index] = TOMBSTONE;\n            size--;\n        }\n    }\n\n    /* Расширить хеш-таблицу */\n    private void extend() {\n        // Временно сохранить исходную хеш-таблицу\n        Pair[] bucketsTmp = buckets;\n        // Инициализация новой хеш-таблицы после расширения\n        capacity *= extendRatio;\n        buckets = new Pair[capacity];\n        size = 0;\n        // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n        for (Pair pair : bucketsTmp) {\n            if (pair != null && pair != TOMBSTONE) {\n                put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Вывести хеш-таблицу */\n    public void print() {\n        for (Pair pair : buckets) {\n            if (pair == null) {\n                System.out.println(\"null\");\n            } else if (pair == TOMBSTONE) {\n                System.out.println(\"TOMBSTONE\");\n            } else {\n                System.out.println(pair.key + \" -> \" + pair.val);\n            }\n        }\n    }\n}\n
        hash_map_open_addressing.cs
        /* Хеш-таблица с открытой адресацией */\nclass HashMapOpenAddressing {\n    int size; // Число пар ключ-значение\n    int capacity = 4; // Вместимость хеш-таблицы\n    double loadThres = 2.0 / 3.0; // Порог коэффициента загрузки для запуска расширения\n    int extendRatio = 2; // Коэффициент расширения\n    Pair[] buckets; // Массив корзин\n    Pair TOMBSTONE = new(-1, \"-1\"); // Удалить метку\n\n    /* Конструктор */\n    public HashMapOpenAddressing() {\n        size = 0;\n        buckets = new Pair[capacity];\n    }\n\n    /* Хеш-функция */\n    int HashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* Коэффициент загрузки */\n    double LoadFactor() {\n        return (double)size / capacity;\n    }\n\n    /* Найти индекс корзины, соответствующий key */\n    int FindBucket(int key) {\n        int index = HashFunc(key);\n        int firstTombstone = -1;\n        // Выполнять линейное пробирование и завершить при встрече с пустой корзиной\n        while (buckets[index] != null) {\n            // Если встретился key, вернуть соответствующий индекс корзины\n            if (buckets[index].key == key) {\n                // Если ранее встретилась метка удаления, переместить пару ключ-значение на этот индекс\n                if (firstTombstone != -1) {\n                    buckets[firstTombstone] = buckets[index];\n                    buckets[index] = TOMBSTONE;\n                    return firstTombstone; // Вернуть индекс корзины после перемещения\n                }\n                return index; // Вернуть индекс корзины\n            }\n            // Записать первую встретившуюся метку удаления\n            if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {\n                firstTombstone = index;\n            }\n            // Вычислить индекс корзины; при выходе за конец вернуться к началу\n            index = (index + 1) % capacity;\n        }\n        // Если key не существует, вернуть индекс точки добавления\n        return firstTombstone == -1 ? index : firstTombstone;\n    }\n\n    /* Операция поиска */\n    public string? Get(int key) {\n        // Найти индекс корзины, соответствующий key\n        int index = FindBucket(key);\n        // Если пара ключ-значение найдена, вернуть соответствующее val\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            return buckets[index].val;\n        }\n        // Если пары ключ-значение не существует, вернуть null\n        return null;\n    }\n\n    /* Операция добавления */\n    public void Put(int key, string val) {\n        // Когда коэффициент загрузки превышает порог, выполнить расширение\n        if (LoadFactor() > loadThres) {\n            Extend();\n        }\n        // Найти индекс корзины, соответствующий key\n        int index = FindBucket(key);\n        // Если пара ключ-значение найдена, перезаписать val и вернуть\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index].val = val;\n            return;\n        }\n        // Если пары ключ-значение нет, добавить ее\n        buckets[index] = new Pair(key, val);\n        size++;\n    }\n\n    /* Операция удаления */\n    public void Remove(int key) {\n        // Найти индекс корзины, соответствующий key\n        int index = FindBucket(key);\n        // Если пара ключ-значение найдена, заменить ее меткой удаления\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index] = TOMBSTONE;\n            size--;\n        }\n    }\n\n    /* Расширить хеш-таблицу */\n    void Extend() {\n        // Временно сохранить исходную хеш-таблицу\n        Pair[] bucketsTmp = buckets;\n        // Инициализация новой хеш-таблицы после расширения\n        capacity *= extendRatio;\n        buckets = new Pair[capacity];\n        size = 0;\n        // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n        foreach (Pair pair in bucketsTmp) {\n            if (pair != null && pair != TOMBSTONE) {\n                Put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Вывести хеш-таблицу */\n    public void Print() {\n        foreach (Pair pair in buckets) {\n            if (pair == null) {\n                Console.WriteLine(\"null\");\n            } else if (pair == TOMBSTONE) {\n                Console.WriteLine(\"TOMBSTONE\");\n            } else {\n                Console.WriteLine(pair.key + \" -> \" + pair.val);\n            }\n        }\n    }\n}\n
        hash_map_open_addressing.go
        /* Хеш-таблица с открытой адресацией */\ntype hashMapOpenAddressing struct {\n    size        int     // Число пар ключ-значение\n    capacity    int     // Вместимость хеш-таблицы\n    loadThres   float64 // Порог коэффициента загрузки для запуска расширения\n    extendRatio int     // Коэффициент расширения\n    buckets     []*pair // Массив корзин\n    TOMBSTONE   *pair   // Удалить метку\n}\n\n/* Конструктор */\nfunc newHashMapOpenAddressing() *hashMapOpenAddressing {\n    return &hashMapOpenAddressing{\n        size:        0,\n        capacity:    4,\n        loadThres:   2.0 / 3.0,\n        extendRatio: 2,\n        buckets:     make([]*pair, 4),\n        TOMBSTONE:   &pair{-1, \"-1\"},\n    }\n}\n\n/* Хеш-функция */\nfunc (h *hashMapOpenAddressing) hashFunc(key int) int {\n    return key % h.capacity // Вычислить хеш-значение по ключу\n}\n\n/* Коэффициент загрузки */\nfunc (h *hashMapOpenAddressing) loadFactor() float64 {\n    return float64(h.size) / float64(h.capacity) // Вычислить текущий коэффициент загрузки\n}\n\n/* Найти индекс корзины, соответствующий key */\nfunc (h *hashMapOpenAddressing) findBucket(key int) int {\n    index := h.hashFunc(key) // Получить начальный индекс\n    firstTombstone := -1     // Запомнить положение первого TOMBSTONE\n    for h.buckets[index] != nil {\n        if h.buckets[index].key == key {\n            if firstTombstone != -1 {\n                // Если ранее встретилась метка удаления, переместить пару ключ-значение на этот индекс\n                h.buckets[firstTombstone] = h.buckets[index]\n                h.buckets[index] = h.TOMBSTONE\n                return firstTombstone // Вернуть индекс корзины после перемещения\n            }\n            return index // Вернуть найденный индекс\n        }\n        if firstTombstone == -1 && h.buckets[index] == h.TOMBSTONE {\n            firstTombstone = index // Запомнить положение первой метки удаления\n        }\n        index = (index + 1) % h.capacity // Линейное пробирование: при выходе за хвост вернуться к началу\n    }\n    // Если key не существует, вернуть индекс точки добавления\n    if firstTombstone != -1 {\n        return firstTombstone\n    }\n    return index\n}\n\n/* Операция поиска */\nfunc (h *hashMapOpenAddressing) get(key int) string {\n    index := h.findBucket(key) // Найти индекс корзины, соответствующий key\n    if h.buckets[index] != nil && h.buckets[index] != h.TOMBSTONE {\n        return h.buckets[index].val // Если пара ключ-значение найдена, вернуть соответствующее val\n    }\n    return \"\" // Если пара ключ-значение не существует, вернуть \"\"\n}\n\n/* Операция добавления */\nfunc (h *hashMapOpenAddressing) put(key int, val string) {\n    if h.loadFactor() > h.loadThres {\n        h.extend() // Когда коэффициент загрузки превышает порог, выполнить расширение\n    }\n    index := h.findBucket(key) // Найти индекс корзины, соответствующий key\n    if h.buckets[index] == nil || h.buckets[index] == h.TOMBSTONE {\n        h.buckets[index] = &pair{key, val} // Если пары ключ-значение нет, добавить ее\n        h.size++\n    } else {\n        h.buckets[index].val = val // Если пара ключ-значение найдена, перезаписать val\n    }\n}\n\n/* Операция удаления */\nfunc (h *hashMapOpenAddressing) remove(key int) {\n    index := h.findBucket(key) // Найти индекс корзины, соответствующий key\n    if h.buckets[index] != nil && h.buckets[index] != h.TOMBSTONE {\n        h.buckets[index] = h.TOMBSTONE // Если пара ключ-значение найдена, заменить ее меткой удаления\n        h.size--\n    }\n}\n\n/* Расширить хеш-таблицу */\nfunc (h *hashMapOpenAddressing) extend() {\n    oldBuckets := h.buckets               // Временно сохранить исходную хеш-таблицу\n    h.capacity *= h.extendRatio           // Обновить емкость\n    h.buckets = make([]*pair, h.capacity) // Инициализация новой хеш-таблицы после расширения\n    h.size = 0                            // Сбросить размер\n    // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n    for _, pair := range oldBuckets {\n        if pair != nil && pair != h.TOMBSTONE {\n            h.put(pair.key, pair.val)\n        }\n    }\n}\n\n/* Вывести хеш-таблицу */\nfunc (h *hashMapOpenAddressing) print() {\n    for _, pair := range h.buckets {\n        if pair == nil {\n            fmt.Println(\"nil\")\n        } else if pair == h.TOMBSTONE {\n            fmt.Println(\"TOMBSTONE\")\n        } else {\n            fmt.Printf(\"%d -> %s\\n\", pair.key, pair.val)\n        }\n    }\n}\n
        hash_map_open_addressing.swift
        /* Хеш-таблица с открытой адресацией */\nclass HashMapOpenAddressing {\n    var size: Int // Число пар ключ-значение\n    var capacity: Int // Вместимость хеш-таблицы\n    var loadThres: Double // Порог коэффициента загрузки для запуска расширения\n    var extendRatio: Int // Коэффициент расширения\n    var buckets: [Pair?] // Массив корзин\n    var TOMBSTONE: Pair // Удалить метку\n\n    /* Конструктор */\n    init() {\n        size = 0\n        capacity = 4\n        loadThres = 2.0 / 3.0\n        extendRatio = 2\n        buckets = Array(repeating: nil, count: capacity)\n        TOMBSTONE = Pair(key: -1, val: \"-1\")\n    }\n\n    /* Хеш-функция */\n    func hashFunc(key: Int) -> Int {\n        key % capacity\n    }\n\n    /* Коэффициент загрузки */\n    func loadFactor() -> Double {\n        Double(size) / Double(capacity)\n    }\n\n    /* Найти индекс корзины, соответствующий key */\n    func findBucket(key: Int) -> Int {\n        var index = hashFunc(key: key)\n        var firstTombstone = -1\n        // Выполнять линейное пробирование и завершить при встрече с пустой корзиной\n        while buckets[index] != nil {\n            // Если встретился key, вернуть соответствующий индекс корзины\n            if buckets[index]!.key == key {\n                // Если ранее встретилась метка удаления, переместить пару ключ-значение на этот индекс\n                if firstTombstone != -1 {\n                    buckets[firstTombstone] = buckets[index]\n                    buckets[index] = TOMBSTONE\n                    return firstTombstone // Вернуть индекс корзины после перемещения\n                }\n                return index // Вернуть индекс корзины\n            }\n            // Записать первую встретившуюся метку удаления\n            if firstTombstone == -1 && buckets[index] == TOMBSTONE {\n                firstTombstone = index\n            }\n            // Вычислить индекс корзины; при выходе за конец вернуться к началу\n            index = (index + 1) % capacity\n        }\n        // Если key не существует, вернуть индекс точки добавления\n        return firstTombstone == -1 ? index : firstTombstone\n    }\n\n    /* Операция поиска */\n    func get(key: Int) -> String? {\n        // Найти индекс корзины, соответствующий key\n        let index = findBucket(key: key)\n        // Если пара ключ-значение найдена, вернуть соответствующее val\n        if buckets[index] != nil, buckets[index] != TOMBSTONE {\n            return buckets[index]!.val\n        }\n        // Если пары ключ-значение не существует, вернуть null\n        return nil\n    }\n\n    /* Операция добавления */\n    func put(key: Int, val: String) {\n        // Когда коэффициент загрузки превышает порог, выполнить расширение\n        if loadFactor() > loadThres {\n            extend()\n        }\n        // Найти индекс корзины, соответствующий key\n        let index = findBucket(key: key)\n        // Если пара ключ-значение найдена, перезаписать val и вернуть\n        if buckets[index] != nil, buckets[index] != TOMBSTONE {\n            buckets[index]!.val = val\n            return\n        }\n        // Если пары ключ-значение нет, добавить ее\n        buckets[index] = Pair(key: key, val: val)\n        size += 1\n    }\n\n    /* Операция удаления */\n    func remove(key: Int) {\n        // Найти индекс корзины, соответствующий key\n        let index = findBucket(key: key)\n        // Если пара ключ-значение найдена, заменить ее меткой удаления\n        if buckets[index] != nil, buckets[index] != TOMBSTONE {\n            buckets[index] = TOMBSTONE\n            size -= 1\n        }\n    }\n\n    /* Расширить хеш-таблицу */\n    func extend() {\n        // Временно сохранить исходную хеш-таблицу\n        let bucketsTmp = buckets\n        // Инициализация новой хеш-таблицы после расширения\n        capacity *= extendRatio\n        buckets = Array(repeating: nil, count: capacity)\n        size = 0\n        // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n        for pair in bucketsTmp {\n            if let pair, pair != TOMBSTONE {\n                put(key: pair.key, val: pair.val)\n            }\n        }\n    }\n\n    /* Вывести хеш-таблицу */\n    func print() {\n        for pair in buckets {\n            if pair == nil {\n                Swift.print(\"null\")\n            } else if pair == TOMBSTONE {\n                Swift.print(\"TOMBSTONE\")\n            } else {\n                Swift.print(\"\\(pair!.key) -> \\(pair!.val)\")\n            }\n        }\n    }\n}\n
        hash_map_open_addressing.js
        /* Хеш-таблица с открытой адресацией */\nclass HashMapOpenAddressing {\n    #size; // Число пар ключ-значение\n    #capacity; // Вместимость хеш-таблицы\n    #loadThres; // Порог коэффициента загрузки для запуска расширения\n    #extendRatio; // Коэффициент расширения\n    #buckets; // Массив корзин\n    #TOMBSTONE; // Удалить метку\n\n    /* Конструктор */\n    constructor() {\n        this.#size = 0; // Число пар ключ-значение\n        this.#capacity = 4; // Вместимость хеш-таблицы\n        this.#loadThres = 2.0 / 3.0; // Порог коэффициента загрузки для запуска расширения\n        this.#extendRatio = 2; // Коэффициент расширения\n        this.#buckets = Array(this.#capacity).fill(null); // Массив корзин\n        this.#TOMBSTONE = new Pair(-1, '-1'); // Удалить метку\n    }\n\n    /* Хеш-функция */\n    #hashFunc(key) {\n        return key % this.#capacity;\n    }\n\n    /* Коэффициент загрузки */\n    #loadFactor() {\n        return this.#size / this.#capacity;\n    }\n\n    /* Найти индекс корзины, соответствующий key */\n    #findBucket(key) {\n        let index = this.#hashFunc(key);\n        let firstTombstone = -1;\n        // Выполнять линейное пробирование и завершить при встрече с пустой корзиной\n        while (this.#buckets[index] !== null) {\n            // Если встретился key, вернуть соответствующий индекс корзины\n            if (this.#buckets[index].key === key) {\n                // Если ранее встретилась метка удаления, переместить пару ключ-значение на этот индекс\n                if (firstTombstone !== -1) {\n                    this.#buckets[firstTombstone] = this.#buckets[index];\n                    this.#buckets[index] = this.#TOMBSTONE;\n                    return firstTombstone; // Вернуть индекс корзины после перемещения\n                }\n                return index; // Вернуть индекс корзины\n            }\n            // Записать первую встретившуюся метку удаления\n            if (\n                firstTombstone === -1 &&\n                this.#buckets[index] === this.#TOMBSTONE\n            ) {\n                firstTombstone = index;\n            }\n            // Вычислить индекс корзины; при выходе за конец вернуться к началу\n            index = (index + 1) % this.#capacity;\n        }\n        // Если key не существует, вернуть индекс точки добавления\n        return firstTombstone === -1 ? index : firstTombstone;\n    }\n\n    /* Операция поиска */\n    get(key) {\n        // Найти индекс корзины, соответствующий key\n        const index = this.#findBucket(key);\n        // Если пара ключ-значение найдена, вернуть соответствующее val\n        if (\n            this.#buckets[index] !== null &&\n            this.#buckets[index] !== this.#TOMBSTONE\n        ) {\n            return this.#buckets[index].val;\n        }\n        // Если пары ключ-значение не существует, вернуть null\n        return null;\n    }\n\n    /* Операция добавления */\n    put(key, val) {\n        // Когда коэффициент загрузки превышает порог, выполнить расширение\n        if (this.#loadFactor() > this.#loadThres) {\n            this.#extend();\n        }\n        // Найти индекс корзины, соответствующий key\n        const index = this.#findBucket(key);\n        // Если пара ключ-значение найдена, перезаписать val и вернуть\n        if (\n            this.#buckets[index] !== null &&\n            this.#buckets[index] !== this.#TOMBSTONE\n        ) {\n            this.#buckets[index].val = val;\n            return;\n        }\n        // Если пары ключ-значение нет, добавить ее\n        this.#buckets[index] = new Pair(key, val);\n        this.#size++;\n    }\n\n    /* Операция удаления */\n    remove(key) {\n        // Найти индекс корзины, соответствующий key\n        const index = this.#findBucket(key);\n        // Если пара ключ-значение найдена, заменить ее меткой удаления\n        if (\n            this.#buckets[index] !== null &&\n            this.#buckets[index] !== this.#TOMBSTONE\n        ) {\n            this.#buckets[index] = this.#TOMBSTONE;\n            this.#size--;\n        }\n    }\n\n    /* Расширить хеш-таблицу */\n    #extend() {\n        // Временно сохранить исходную хеш-таблицу\n        const bucketsTmp = this.#buckets;\n        // Инициализация новой хеш-таблицы после расширения\n        this.#capacity *= this.#extendRatio;\n        this.#buckets = Array(this.#capacity).fill(null);\n        this.#size = 0;\n        // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n        for (const pair of bucketsTmp) {\n            if (pair !== null && pair !== this.#TOMBSTONE) {\n                this.put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Вывести хеш-таблицу */\n    print() {\n        for (const pair of this.#buckets) {\n            if (pair === null) {\n                console.log('null');\n            } else if (pair === this.#TOMBSTONE) {\n                console.log('TOMBSTONE');\n            } else {\n                console.log(pair.key + ' -> ' + pair.val);\n            }\n        }\n    }\n}\n
        hash_map_open_addressing.ts
        /* Хеш-таблица с открытой адресацией */\nclass HashMapOpenAddressing {\n    private size: number; // Число пар ключ-значение\n    private capacity: number; // Вместимость хеш-таблицы\n    private loadThres: number; // Порог коэффициента загрузки для запуска расширения\n    private extendRatio: number; // Коэффициент расширения\n    private buckets: Array<Pair | null>; // Массив корзин\n    private TOMBSTONE: Pair; // Удалить метку\n\n    /* Конструктор */\n    constructor() {\n        this.size = 0; // Число пар ключ-значение\n        this.capacity = 4; // Вместимость хеш-таблицы\n        this.loadThres = 2.0 / 3.0; // Порог коэффициента загрузки для запуска расширения\n        this.extendRatio = 2; // Коэффициент расширения\n        this.buckets = Array(this.capacity).fill(null); // Массив корзин\n        this.TOMBSTONE = new Pair(-1, '-1'); // Удалить метку\n    }\n\n    /* Хеш-функция */\n    private hashFunc(key: number): number {\n        return key % this.capacity;\n    }\n\n    /* Коэффициент загрузки */\n    private loadFactor(): number {\n        return this.size / this.capacity;\n    }\n\n    /* Найти индекс корзины, соответствующий key */\n    private findBucket(key: number): number {\n        let index = this.hashFunc(key);\n        let firstTombstone = -1;\n        // Выполнять линейное пробирование и завершить при встрече с пустой корзиной\n        while (this.buckets[index] !== null) {\n            // Если встретился key, вернуть соответствующий индекс корзины\n            if (this.buckets[index]!.key === key) {\n                // Если ранее встретилась метка удаления, переместить пару ключ-значение на этот индекс\n                if (firstTombstone !== -1) {\n                    this.buckets[firstTombstone] = this.buckets[index];\n                    this.buckets[index] = this.TOMBSTONE;\n                    return firstTombstone; // Вернуть индекс корзины после перемещения\n                }\n                return index; // Вернуть индекс корзины\n            }\n            // Записать первую встретившуюся метку удаления\n            if (\n                firstTombstone === -1 &&\n                this.buckets[index] === this.TOMBSTONE\n            ) {\n                firstTombstone = index;\n            }\n            // Вычислить индекс корзины; при выходе за конец вернуться к началу\n            index = (index + 1) % this.capacity;\n        }\n        // Если key не существует, вернуть индекс точки добавления\n        return firstTombstone === -1 ? index : firstTombstone;\n    }\n\n    /* Операция поиска */\n    get(key: number): string | null {\n        // Найти индекс корзины, соответствующий key\n        const index = this.findBucket(key);\n        // Если пара ключ-значение найдена, вернуть соответствующее val\n        if (\n            this.buckets[index] !== null &&\n            this.buckets[index] !== this.TOMBSTONE\n        ) {\n            return this.buckets[index]!.val;\n        }\n        // Если пары ключ-значение не существует, вернуть null\n        return null;\n    }\n\n    /* Операция добавления */\n    put(key: number, val: string): void {\n        // Когда коэффициент загрузки превышает порог, выполнить расширение\n        if (this.loadFactor() > this.loadThres) {\n            this.extend();\n        }\n        // Найти индекс корзины, соответствующий key\n        const index = this.findBucket(key);\n        // Если пара ключ-значение найдена, перезаписать val и вернуть\n        if (\n            this.buckets[index] !== null &&\n            this.buckets[index] !== this.TOMBSTONE\n        ) {\n            this.buckets[index]!.val = val;\n            return;\n        }\n        // Если пары ключ-значение нет, добавить ее\n        this.buckets[index] = new Pair(key, val);\n        this.size++;\n    }\n\n    /* Операция удаления */\n    remove(key: number): void {\n        // Найти индекс корзины, соответствующий key\n        const index = this.findBucket(key);\n        // Если пара ключ-значение найдена, заменить ее меткой удаления\n        if (\n            this.buckets[index] !== null &&\n            this.buckets[index] !== this.TOMBSTONE\n        ) {\n            this.buckets[index] = this.TOMBSTONE;\n            this.size--;\n        }\n    }\n\n    /* Расширить хеш-таблицу */\n    private extend(): void {\n        // Временно сохранить исходную хеш-таблицу\n        const bucketsTmp = this.buckets;\n        // Инициализация новой хеш-таблицы после расширения\n        this.capacity *= this.extendRatio;\n        this.buckets = Array(this.capacity).fill(null);\n        this.size = 0;\n        // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n        for (const pair of bucketsTmp) {\n            if (pair !== null && pair !== this.TOMBSTONE) {\n                this.put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Вывести хеш-таблицу */\n    print(): void {\n        for (const pair of this.buckets) {\n            if (pair === null) {\n                console.log('null');\n            } else if (pair === this.TOMBSTONE) {\n                console.log('TOMBSTONE');\n            } else {\n                console.log(pair.key + ' -> ' + pair.val);\n            }\n        }\n    }\n}\n
        hash_map_open_addressing.dart
        /* Хеш-таблица с открытой адресацией */\nclass HashMapOpenAddressing {\n  late int _size; // Число пар ключ-значение\n  int _capacity = 4; // Вместимость хеш-таблицы\n  double _loadThres = 2.0 / 3.0; // Порог коэффициента загрузки для запуска расширения\n  int _extendRatio = 2; // Коэффициент расширения\n  late List<Pair?> _buckets; // Массив корзин\n  Pair _TOMBSTONE = Pair(-1, \"-1\"); // Удалить метку\n\n  /* Конструктор */\n  HashMapOpenAddressing() {\n    _size = 0;\n    _buckets = List.generate(_capacity, (index) => null);\n  }\n\n  /* Хеш-функция */\n  int hashFunc(int key) {\n    return key % _capacity;\n  }\n\n  /* Коэффициент загрузки */\n  double loadFactor() {\n    return _size / _capacity;\n  }\n\n  /* Найти индекс корзины, соответствующий key */\n  int findBucket(int key) {\n    int index = hashFunc(key);\n    int firstTombstone = -1;\n    // Выполнять линейное пробирование и завершить при встрече с пустой корзиной\n    while (_buckets[index] != null) {\n      // Если встретился key, вернуть соответствующий индекс корзины\n      if (_buckets[index]!.key == key) {\n        // Если ранее встретилась метка удаления, переместить пару ключ-значение на этот индекс\n        if (firstTombstone != -1) {\n          _buckets[firstTombstone] = _buckets[index];\n          _buckets[index] = _TOMBSTONE;\n          return firstTombstone; // Вернуть индекс корзины после перемещения\n        }\n        return index; // Вернуть индекс корзины\n      }\n      // Записать первую встретившуюся метку удаления\n      if (firstTombstone == -1 && _buckets[index] == _TOMBSTONE) {\n        firstTombstone = index;\n      }\n      // Вычислить индекс корзины; при выходе за конец вернуться к началу\n      index = (index + 1) % _capacity;\n    }\n    // Если key не существует, вернуть индекс точки добавления\n    return firstTombstone == -1 ? index : firstTombstone;\n  }\n\n  /* Операция поиска */\n  String? get(int key) {\n    // Найти индекс корзины, соответствующий key\n    int index = findBucket(key);\n    // Если пара ключ-значение найдена, вернуть соответствующее val\n    if (_buckets[index] != null && _buckets[index] != _TOMBSTONE) {\n      return _buckets[index]!.val;\n    }\n    // Если пары ключ-значение не существует, вернуть null\n    return null;\n  }\n\n  /* Операция добавления */\n  void put(int key, String val) {\n    // Когда коэффициент загрузки превышает порог, выполнить расширение\n    if (loadFactor() > _loadThres) {\n      extend();\n    }\n    // Найти индекс корзины, соответствующий key\n    int index = findBucket(key);\n    // Если пара ключ-значение найдена, перезаписать val и вернуть\n    if (_buckets[index] != null && _buckets[index] != _TOMBSTONE) {\n      _buckets[index]!.val = val;\n      return;\n    }\n    // Если пары ключ-значение нет, добавить ее\n    _buckets[index] = new Pair(key, val);\n    _size++;\n  }\n\n  /* Операция удаления */\n  void remove(int key) {\n    // Найти индекс корзины, соответствующий key\n    int index = findBucket(key);\n    // Если пара ключ-значение найдена, заменить ее меткой удаления\n    if (_buckets[index] != null && _buckets[index] != _TOMBSTONE) {\n      _buckets[index] = _TOMBSTONE;\n      _size--;\n    }\n  }\n\n  /* Расширить хеш-таблицу */\n  void extend() {\n    // Временно сохранить исходную хеш-таблицу\n    List<Pair?> bucketsTmp = _buckets;\n    // Инициализация новой хеш-таблицы после расширения\n    _capacity *= _extendRatio;\n    _buckets = List.generate(_capacity, (index) => null);\n    _size = 0;\n    // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n    for (Pair? pair in bucketsTmp) {\n      if (pair != null && pair != _TOMBSTONE) {\n        put(pair.key, pair.val);\n      }\n    }\n  }\n\n  /* Вывести хеш-таблицу */\n  void printHashMap() {\n    for (Pair? pair in _buckets) {\n      if (pair == null) {\n        print(\"null\");\n      } else if (pair == _TOMBSTONE) {\n        print(\"TOMBSTONE\");\n      } else {\n        print(\"${pair.key} -> ${pair.val}\");\n      }\n    }\n  }\n}\n
        hash_map_open_addressing.rs
        /* Хеш-таблица с открытой адресацией */\nstruct HashMapOpenAddressing {\n    size: usize,                // Число пар ключ-значение\n    capacity: usize,            // Вместимость хеш-таблицы\n    load_thres: f64,            // Порог коэффициента загрузки для запуска расширения\n    extend_ratio: usize,        // Коэффициент расширения\n    buckets: Vec<Option<Pair>>, // Массив корзин\n    TOMBSTONE: Option<Pair>,    // Удалить метку\n}\n\nimpl HashMapOpenAddressing {\n    /* Конструктор */\n    fn new() -> Self {\n        Self {\n            size: 0,\n            capacity: 4,\n            load_thres: 2.0 / 3.0,\n            extend_ratio: 2,\n            buckets: vec![None; 4],\n            TOMBSTONE: Some(Pair {\n                key: -1,\n                val: \"-1\".to_string(),\n            }),\n        }\n    }\n\n    /* Хеш-функция */\n    fn hash_func(&self, key: i32) -> usize {\n        (key % self.capacity as i32) as usize\n    }\n\n    /* Коэффициент загрузки */\n    fn load_factor(&self) -> f64 {\n        self.size as f64 / self.capacity as f64\n    }\n\n    /* Найти индекс корзины, соответствующий key */\n    fn find_bucket(&mut self, key: i32) -> usize {\n        let mut index = self.hash_func(key);\n        let mut first_tombstone = -1;\n        // Выполнять линейное пробирование и завершить при встрече с пустой корзиной\n        while self.buckets[index].is_some() {\n            // Если встретился key, вернуть соответствующий индекс корзины\n            if self.buckets[index].as_ref().unwrap().key == key {\n                // Если ранее встретилась метка удаления, переместить пару ключ-значение в этот индекс\n                if first_tombstone != -1 {\n                    self.buckets[first_tombstone as usize] = self.buckets[index].take();\n                    self.buckets[index] = self.TOMBSTONE.clone();\n                    return first_tombstone as usize; // Вернуть индекс корзины после перемещения\n                }\n                return index; // Вернуть индекс корзины\n            }\n            // Записать первую встретившуюся метку удаления\n            if first_tombstone == -1 && self.buckets[index] == self.TOMBSTONE {\n                first_tombstone = index as i32;\n            }\n            // Вычислить индекс корзины; при выходе за конец вернуться к началу\n            index = (index + 1) % self.capacity;\n        }\n        // Если key не существует, вернуть индекс точки добавления\n        if first_tombstone == -1 {\n            index\n        } else {\n            first_tombstone as usize\n        }\n    }\n\n    /* Операция поиска */\n    fn get(&mut self, key: i32) -> Option<&str> {\n        // Найти индекс корзины, соответствующий key\n        let index = self.find_bucket(key);\n        // Если пара ключ-значение найдена, вернуть соответствующее val\n        if self.buckets[index].is_some() && self.buckets[index] != self.TOMBSTONE {\n            return self.buckets[index].as_ref().map(|pair| &pair.val as &str);\n        }\n        // Если пары ключ-значение не существует, вернуть null\n        None\n    }\n\n    /* Операция добавления */\n    fn put(&mut self, key: i32, val: String) {\n        // Когда коэффициент загрузки превышает порог, выполнить расширение\n        if self.load_factor() > self.load_thres {\n            self.extend();\n        }\n        // Найти индекс корзины, соответствующий key\n        let index = self.find_bucket(key);\n        // Если пара ключ-значение найдена, перезаписать val и вернуть\n        if self.buckets[index].is_some() && self.buckets[index] != self.TOMBSTONE {\n            self.buckets[index].as_mut().unwrap().val = val;\n            return;\n        }\n        // Если пары ключ-значение нет, добавить ее\n        self.buckets[index] = Some(Pair { key, val });\n        self.size += 1;\n    }\n\n    /* Операция удаления */\n    fn remove(&mut self, key: i32) {\n        // Найти индекс корзины, соответствующий key\n        let index = self.find_bucket(key);\n        // Если пара ключ-значение найдена, заменить ее меткой удаления\n        if self.buckets[index].is_some() && self.buckets[index] != self.TOMBSTONE {\n            self.buckets[index] = self.TOMBSTONE.clone();\n            self.size -= 1;\n        }\n    }\n\n    /* Расширить хеш-таблицу */\n    fn extend(&mut self) {\n        // Временно сохранить исходную хеш-таблицу\n        let buckets_tmp = self.buckets.clone();\n        // Инициализация новой хеш-таблицы после расширения\n        self.capacity *= self.extend_ratio;\n        self.buckets = vec![None; self.capacity];\n        self.size = 0;\n\n        // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n        for pair in buckets_tmp {\n            if pair.is_none() || pair == self.TOMBSTONE {\n                continue;\n            }\n            let pair = pair.unwrap();\n\n            self.put(pair.key, pair.val);\n        }\n    }\n    /* Вывести хеш-таблицу */\n    fn print(&self) {\n        for pair in &self.buckets {\n            if pair.is_none() {\n                println!(\"null\");\n            } else if pair == &self.TOMBSTONE {\n                println!(\"TOMBSTONE\");\n            } else {\n                let pair = pair.as_ref().unwrap();\n                println!(\"{} -> {}\", pair.key, pair.val);\n            }\n        }\n    }\n}\n
        hash_map_open_addressing.c
        /* Хеш-таблица с открытой адресацией */\ntypedef struct {\n    int size;         // Число пар ключ-значение\n    int capacity;     // Вместимость хеш-таблицы\n    double loadThres; // Порог коэффициента загрузки для запуска расширения\n    int extendRatio;  // Коэффициент расширения\n    Pair **buckets;   // Массив корзин\n    Pair *TOMBSTONE;  // Удалить метку\n} HashMapOpenAddressing;\n\n/* Конструктор */\nHashMapOpenAddressing *newHashMapOpenAddressing() {\n    HashMapOpenAddressing *hashMap = (HashMapOpenAddressing *)malloc(sizeof(HashMapOpenAddressing));\n    hashMap->size = 0;\n    hashMap->capacity = 4;\n    hashMap->loadThres = 2.0 / 3.0;\n    hashMap->extendRatio = 2;\n    hashMap->buckets = (Pair **)calloc(hashMap->capacity, sizeof(Pair *));\n    hashMap->TOMBSTONE = (Pair *)malloc(sizeof(Pair));\n    hashMap->TOMBSTONE->key = -1;\n    hashMap->TOMBSTONE->val = \"-1\";\n\n    return hashMap;\n}\n\n/* Деструктор */\nvoid delHashMapOpenAddressing(HashMapOpenAddressing *hashMap) {\n    for (int i = 0; i < hashMap->capacity; i++) {\n        Pair *pair = hashMap->buckets[i];\n        if (pair != NULL && pair != hashMap->TOMBSTONE) {\n            free(pair->val);\n            free(pair);\n        }\n    }\n    free(hashMap->buckets);\n    free(hashMap->TOMBSTONE);\n    free(hashMap);\n}\n\n/* Хеш-функция */\nint hashFunc(HashMapOpenAddressing *hashMap, int key) {\n    return key % hashMap->capacity;\n}\n\n/* Коэффициент загрузки */\ndouble loadFactor(HashMapOpenAddressing *hashMap) {\n    return (double)hashMap->size / (double)hashMap->capacity;\n}\n\n/* Найти индекс корзины, соответствующий key */\nint findBucket(HashMapOpenAddressing *hashMap, int key) {\n    int index = hashFunc(hashMap, key);\n    int firstTombstone = -1;\n    // Выполнять линейное пробирование и завершить при встрече с пустой корзиной\n    while (hashMap->buckets[index] != NULL) {\n        // Если встретился key, вернуть соответствующий индекс корзины\n        if (hashMap->buckets[index]->key == key) {\n            // Если ранее встретилась метка удаления, переместить пару ключ-значение на этот индекс\n            if (firstTombstone != -1) {\n                hashMap->buckets[firstTombstone] = hashMap->buckets[index];\n                hashMap->buckets[index] = hashMap->TOMBSTONE;\n                return firstTombstone; // Вернуть индекс корзины после перемещения\n            }\n            return index; // Вернуть индекс корзины\n        }\n        // Записать первую встретившуюся метку удаления\n        if (firstTombstone == -1 && hashMap->buckets[index] == hashMap->TOMBSTONE) {\n            firstTombstone = index;\n        }\n        // Вычислить индекс корзины; при выходе за конец вернуться к началу\n        index = (index + 1) % hashMap->capacity;\n    }\n    // Если key не существует, вернуть индекс точки добавления\n    return firstTombstone == -1 ? index : firstTombstone;\n}\n\n/* Операция поиска */\nchar *get(HashMapOpenAddressing *hashMap, int key) {\n    // Найти индекс корзины, соответствующий key\n    int index = findBucket(hashMap, key);\n    // Если пара ключ-значение найдена, вернуть соответствующее val\n    if (hashMap->buckets[index] != NULL && hashMap->buckets[index] != hashMap->TOMBSTONE) {\n        return hashMap->buckets[index]->val;\n    }\n    // Если пары ключ-значение не существует, вернуть пустую строку\n    return \"\";\n}\n\n/* Операция добавления */\nvoid put(HashMapOpenAddressing *hashMap, int key, char *val) {\n    // Когда коэффициент загрузки превышает порог, выполнить расширение\n    if (loadFactor(hashMap) > hashMap->loadThres) {\n        extend(hashMap);\n    }\n    // Найти индекс корзины, соответствующий key\n    int index = findBucket(hashMap, key);\n    // Если пара ключ-значение найдена, перезаписать val и вернуть\n    if (hashMap->buckets[index] != NULL && hashMap->buckets[index] != hashMap->TOMBSTONE) {\n        free(hashMap->buckets[index]->val);\n        hashMap->buckets[index]->val = (char *)malloc(sizeof(strlen(val) + 1));\n        strcpy(hashMap->buckets[index]->val, val);\n        hashMap->buckets[index]->val[strlen(val)] = '\\0';\n        return;\n    }\n    // Если пары ключ-значение нет, добавить ее\n    Pair *pair = (Pair *)malloc(sizeof(Pair));\n    pair->key = key;\n    pair->val = (char *)malloc(sizeof(strlen(val) + 1));\n    strcpy(pair->val, val);\n    pair->val[strlen(val)] = '\\0';\n\n    hashMap->buckets[index] = pair;\n    hashMap->size++;\n}\n\n/* Операция удаления */\nvoid removeItem(HashMapOpenAddressing *hashMap, int key) {\n    // Найти индекс корзины, соответствующий key\n    int index = findBucket(hashMap, key);\n    // Если пара ключ-значение найдена, заменить ее меткой удаления\n    if (hashMap->buckets[index] != NULL && hashMap->buckets[index] != hashMap->TOMBSTONE) {\n        Pair *pair = hashMap->buckets[index];\n        free(pair->val);\n        free(pair);\n        hashMap->buckets[index] = hashMap->TOMBSTONE;\n        hashMap->size--;\n    }\n}\n\n/* Расширить хеш-таблицу */\nvoid extend(HashMapOpenAddressing *hashMap) {\n    // Временно сохранить исходную хеш-таблицу\n    Pair **bucketsTmp = hashMap->buckets;\n    int oldCapacity = hashMap->capacity;\n    // Инициализация новой хеш-таблицы после расширения\n    hashMap->capacity *= hashMap->extendRatio;\n    hashMap->buckets = (Pair **)calloc(hashMap->capacity, sizeof(Pair *));\n    hashMap->size = 0;\n    // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n    for (int i = 0; i < oldCapacity; i++) {\n        Pair *pair = bucketsTmp[i];\n        if (pair != NULL && pair != hashMap->TOMBSTONE) {\n            put(hashMap, pair->key, pair->val);\n            free(pair->val);\n            free(pair);\n        }\n    }\n    free(bucketsTmp);\n}\n\n/* Вывести хеш-таблицу */\nvoid print(HashMapOpenAddressing *hashMap) {\n    for (int i = 0; i < hashMap->capacity; i++) {\n        Pair *pair = hashMap->buckets[i];\n        if (pair == NULL) {\n            printf(\"NULL\\n\");\n        } else if (pair == hashMap->TOMBSTONE) {\n            printf(\"TOMBSTONE\\n\");\n        } else {\n            printf(\"%d -> %s\\n\", pair->key, pair->val);\n        }\n    }\n}\n
        hash_map_open_addressing.kt
        /* Хеш-таблица с открытой адресацией */\nclass HashMapOpenAddressing {\n    private var size: Int               // Число пар ключ-значение\n    private var capacity: Int           // Вместимость хеш-таблицы\n    private val loadThres: Double       // Порог коэффициента загрузки для запуска расширения\n    private val extendRatio: Int        // Коэффициент расширения\n    private var buckets: Array<Pair?>   // Массив корзин\n    private val TOMBSTONE: Pair         // Удалить метку\n\n    /* Конструктор */\n    init {\n        size = 0\n        capacity = 4\n        loadThres = 2.0 / 3.0\n        extendRatio = 2\n        buckets = arrayOfNulls(capacity)\n        TOMBSTONE = Pair(-1, \"-1\")\n    }\n\n    /* Хеш-функция */\n    fun hashFunc(key: Int): Int {\n        return key % capacity\n    }\n\n    /* Коэффициент загрузки */\n    fun loadFactor(): Double {\n        return (size / capacity).toDouble()\n    }\n\n    /* Найти индекс корзины, соответствующий key */\n    fun findBucket(key: Int): Int {\n        var index = hashFunc(key)\n        var firstTombstone = -1\n        // Выполнять линейное пробирование и завершить при встрече с пустой корзиной\n        while (buckets[index] != null) {\n            // Если встретился key, вернуть соответствующий индекс корзины\n            if (buckets[index]?.key == key) {\n                // Если ранее встретилась метка удаления, переместить пару ключ-значение на этот индекс\n                if (firstTombstone != -1) {\n                    buckets[firstTombstone] = buckets[index]\n                    buckets[index] = TOMBSTONE\n                    return firstTombstone // Вернуть индекс корзины после перемещения\n                }\n                return index // Вернуть индекс корзины\n            }\n            // Записать первую встретившуюся метку удаления\n            if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {\n                firstTombstone = index\n            }\n            // Вычислить индекс корзины; при выходе за конец вернуться к началу\n            index = (index + 1) % capacity\n        }\n        // Если key не существует, вернуть индекс точки добавления\n        return if (firstTombstone == -1) index else firstTombstone\n    }\n\n    /* Операция поиска */\n    fun get(key: Int): String? {\n        // Найти индекс корзины, соответствующий key\n        val index = findBucket(key)\n        // Если пара ключ-значение найдена, вернуть соответствующее val\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            return buckets[index]?._val\n        }\n        // Если пары ключ-значение не существует, вернуть null\n        return null\n    }\n\n    /* Операция добавления */\n    fun put(key: Int, _val: String) {\n        // Когда коэффициент загрузки превышает порог, выполнить расширение\n        if (loadFactor() > loadThres) {\n            extend()\n        }\n        // Найти индекс корзины, соответствующий key\n        val index = findBucket(key)\n        // Если пара ключ-значение найдена, перезаписать val и вернуть\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index]!!._val = _val\n            return\n        }\n        // Если пары ключ-значение нет, добавить ее\n        buckets[index] = Pair(key, _val)\n        size++\n    }\n\n    /* Операция удаления */\n    fun remove(key: Int) {\n        // Найти индекс корзины, соответствующий key\n        val index = findBucket(key)\n        // Если пара ключ-значение найдена, заменить ее меткой удаления\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index] = TOMBSTONE\n            size--\n        }\n    }\n\n    /* Расширить хеш-таблицу */\n    fun extend() {\n        // Временно сохранить исходную хеш-таблицу\n        val bucketsTmp = buckets\n        // Инициализация новой хеш-таблицы после расширения\n        capacity *= extendRatio\n        buckets = arrayOfNulls(capacity)\n        size = 0\n        // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n        for (pair in bucketsTmp) {\n            if (pair != null && pair != TOMBSTONE) {\n                put(pair.key, pair._val)\n            }\n        }\n    }\n\n    /* Вывести хеш-таблицу */\n    fun print() {\n        for (pair in buckets) {\n            if (pair == null) {\n                println(\"null\")\n            } else if (pair == TOMBSTONE) {\n                println(\"TOMESTOME\")\n            } else {\n                println(\"${pair.key} -> ${pair._val}\")\n            }\n        }\n    }\n}\n
        hash_map_open_addressing.rb
        ### Хеш-таблица с открытой адресацией ###\nclass HashMapOpenAddressing\n  TOMBSTONE = Pair.new(-1, '-1') # Удалить метку\n\n  ### Конструктор ###\n  def initialize\n    @size = 0 # Число пар ключ-значение\n    @capacity = 4 # Вместимость хеш-таблицы\n    @load_thres = 2.0 / 3.0 # Порог коэффициента загрузки для запуска расширения\n    @extend_ratio = 2 # Коэффициент расширения\n    @buckets = Array.new(@capacity) # Массив корзин\n  end\n\n  ### Хеш-функция ###\n  def hash_func(key)\n    key % @capacity\n  end\n\n  ### Коэффициент загрузки ###\n  def load_factor\n    @size / @capacity\n  end\n\n  ### Найти индекс корзины, соответствующий key ###\n  def find_bucket(key)\n    index = hash_func(key)\n    first_tombstone = -1\n    # Выполнять линейное пробирование и завершить при встрече с пустой корзиной\n    while !@buckets[index].nil?\n      # Если встретился key, вернуть соответствующий индекс корзины\n      if @buckets[index].key == key\n        # Если ранее встретилась метка удаления, переместить пару ключ-значение на этот индекс\n        if first_tombstone != -1\n          @buckets[first_tombstone] = @buckets[index]\n          @buckets[index] = TOMBSTONE\n          return first_tombstone # Вернуть индекс корзины после перемещения\n        end\n        return index # Вернуть индекс корзины\n      end\n      # Записать первую встретившуюся метку удаления\n      first_tombstone = index if first_tombstone == -1 && @buckets[index] == TOMBSTONE\n      # Вычислить индекс корзины; при выходе за конец вернуться к началу\n      index = (index + 1) % @capacity\n    end\n    # Если key не существует, вернуть индекс точки добавления\n    first_tombstone == -1 ? index : first_tombstone\n  end\n\n  ### Операция поиска ###\n  def get(key)\n    # Найти индекс корзины, соответствующий key\n    index = find_bucket(key)\n    # Если пара ключ-значение найдена, вернуть соответствующее val\n    return @buckets[index].val unless [nil, TOMBSTONE].include?(@buckets[index])\n    # Если пара ключ-значение не существует, вернуть nil\n    nil\n  end\n\n  ### Операция добавления ###\n  def put(key, val)\n    # Когда коэффициент загрузки превышает порог, выполнить расширение\n    extend if load_factor > @load_thres\n    # Найти индекс корзины, соответствующий key\n    index = find_bucket(key)\n    # Если пара ключ-значение найдена, перезаписать val и вернуть\n    unless [nil, TOMBSTONE].include?(@buckets[index])\n      @buckets[index].val = val\n      return\n    end\n    # Если пары ключ-значение нет, добавить ее\n    @buckets[index] = Pair.new(key, val)\n    @size += 1\n  end\n\n  ### Операция удаления ###\n  def remove(key)\n    # Найти индекс корзины, соответствующий key\n    index = find_bucket(key)\n    # Если пара ключ-значение найдена, заменить ее меткой удаления\n    unless [nil, TOMBSTONE].include?(@buckets[index])\n      @buckets[index] = TOMBSTONE\n      @size -= 1\n    end\n  end\n\n  ### Расширение хеш-таблицы ###\n  def extend\n    # Временно сохранить исходную хеш-таблицу\n    buckets_tmp = @buckets\n    # Инициализация новой хеш-таблицы после расширения\n    @capacity *= @extend_ratio\n    @buckets = Array.new(@capacity)\n    @size = 0\n    # Перенести пары ключ-значение из исходной хеш-таблицы в новую\n    for pair in buckets_tmp\n      put(pair.key, pair.val) unless [nil, TOMBSTONE].include?(pair)\n    end\n  end\n\n  ### Вывести хеш-таблицу ###\n  def print\n    for pair in @buckets\n      if pair.nil?\n        puts \"Nil\"\n      elsif pair == TOMBSTONE\n        puts \"TOMBSTONE\"\n      else\n        puts \"#{pair.key} -> #{pair.val}\"\n      end\n    end\n  end\nend\n
        ","path":["Глава 6. Хеш-таблицы","6.2   Хеш-коллизии"],"tags":[]},{"location":"chapter_hashing/hash_collision/#2","level":3,"title":"2.   Квадратичное пробирование","text":"

        Квадратичное пробирование похоже на линейное пробирование и тоже является одной из распространенных стратегий открытой адресации. При возникновении конфликта оно не пропускает фиксированное число шагов, а переходит на расстояние, равное \"квадрату числа попыток\", то есть на \\(1, 4, 9, \\dots\\) шагов.

        Квадратичное пробирование имеет следующие основные преимущества.

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

        Однако квадратичное пробирование не является идеальным.

        • Кластеризация все равно существует: некоторые позиции по-прежнему занимают чаще других.
        • Из-за быстрого роста квадрата квадратичное пробирование может не охватить всю хеш-таблицу, а это означает, что даже при наличии пустых бакетов оно может так до них и не добраться.
        ","path":["Глава 6. Хеш-таблицы","6.2   Хеш-коллизии"],"tags":[]},{"location":"chapter_hashing/hash_collision/#3","level":3,"title":"3.   Повторное хеширование","text":"

        Как видно из названия, метод повторного хеширования использует для пробирования несколько хеш-функций \\(f_1(x)\\), \\(f_2(x)\\), \\(f_3(x)\\), \\(\\dots\\) .

        • Вставка элемента: если хеш-функция \\(f_1(x)\\) вызывает конфликт, то пробуем \\(f_2(x)\\) , и так далее, пока не будет найдено пустое место для вставки элемента.
        • Поиск элемента: поиск идет в том же порядке хеш-функций, пока не будет найден целевой элемент; если встречается пустая позиция или уже были опробованы все хеш-функции, это означает, что элемента в хеш-таблице нет, и возвращается None .

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

        Tip

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

        ","path":["Глава 6. Хеш-таблицы","6.2   Хеш-коллизии"],"tags":[]},{"location":"chapter_hashing/hash_collision/#623","level":2,"title":"6.2.3   Выбор в языках программирования","text":"

        Разные языки программирования используют разные стратегии реализации хеш-таблиц. Ниже приведено несколько примеров.

        • Python использует открытую адресацию. В словаре dict для пробирования применяются псевдослучайные числа.
        • Java использует метод цепочек. Начиная с JDK 1.8, когда длина массива внутри HashMap достигает 64, а длина списка достигает 8, этот список преобразуется в красно-черное дерево для повышения производительности поиска.
        • Go использует метод цепочек. В Go установлено, что каждый бакет может хранить не более 8 пар ключ-значение; при переполнении подключается overflow-бакет, а когда таких бакетов становится слишком много, выполняется специальное расширение того же масштаба, чтобы сохранить производительность.
        ","path":["Глава 6. Хеш-таблицы","6.2   Хеш-коллизии"],"tags":[]},{"location":"chapter_hashing/hash_map/","level":1,"title":"6.1   Хеш-таблица","text":"

        Хеш-таблица (hash table), также называемая таблицей рассеяния, реализует эффективный поиск элементов за счет установления соответствия между ключом key и значением value . Иначе говоря, если передать в хеш-таблицу ключ key , то можно за \\(O(1)\\) времени получить соответствующее значение value .

        Как показано на рисунке 6-1, пусть есть \\(n\\) студентов, и у каждого из них есть два поля данных: имя и номер студенческого билета. Если мы хотим реализовать запрос вида \"ввести номер студенческого билета и вернуть соответствующее имя\", то для этого можно использовать показанную ниже хеш-таблицу.

        Рисунок 6-1   Абстрактное представление хеш-таблицы

        Помимо хеш-таблицы, функцией поиска также обладают массив и связный список. Сравнение их эффективности приведено в таблице 6-1.

        • Добавление элемента: нужно лишь добавить элемент в конец массива (или списка), что занимает \\(O(1)\\) времени.
        • Поиск элемента: так как массив (или список) неупорядочен, приходится обходить все элементы, что занимает \\(O(n)\\) времени.
        • Удаление элемента: сначала нужно найти элемент, затем удалить его из массива (или списка), что занимает \\(O(n)\\) времени.

        Таблица 6-1   Сравнение эффективности поиска элементов

        Массив Связный список Хеш-таблица Поиск элемента \\(O(n)\\) \\(O(n)\\) \\(O(1)\\) Добавление элемента \\(O(1)\\) \\(O(1)\\) \\(O(1)\\) Удаление элемента \\(O(n)\\) \\(O(n)\\) \\(O(1)\\)

        Нетрудно заметить, что операции поиска, добавления и удаления в хеш-таблице имеют временную сложность \\(O(1)\\) , то есть выполняются очень эффективно.

        ","path":["Глава 6. Хеш-таблицы","6.1   Хеш-таблица"],"tags":[]},{"location":"chapter_hashing/hash_map/#611-","level":2,"title":"6.1.1   Основные операции с хеш-таблицей","text":"

        К базовым операциям хеш-таблицы относятся инициализация, поиск, добавление пар ключ-значение и удаление пар ключ-значение. Пример кода приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map.py
        # Инициализация хеш-таблицы\nhmap: dict = {}\n\n# Операция добавления\n# Добавить пару ключ-значение (key, value) в хеш-таблицу\nhmap[12836] = \"Сяо Ха\"\nhmap[15937] = \"Сяо Ло\"\nhmap[16750] = \"Сяо Суань\"\nhmap[13276] = \"Сяо Фа\"\nhmap[10583] = \"Сяо Я\"\n\n# Операция поиска\n# Передать в хеш-таблицу ключ key и получить значение value\nname: str = hmap[15937]\n\n# Операция удаления\n# Удалить пару ключ-значение (key, value) из хеш-таблицы\nhmap.pop(10583)\n
        hash_map.cpp
        /* Инициализация хеш-таблицы */\nunordered_map<int, string> map;\n\n/* Операция добавления */\n// Добавить пару ключ-значение (key, value) в хеш-таблицу\nmap[12836] = \"Сяо Ха\";\nmap[15937] = \"Сяо Ло\";\nmap[16750] = \"Сяо Суань\";\nmap[13276] = \"Сяо Фа\";\nmap[10583] = \"Сяо Я\";\n\n/* Операция поиска */\n// Передать в хеш-таблицу ключ key и получить значение value\nstring name = map[15937];\n\n/* Операция удаления */\n// Удалить пару ключ-значение (key, value) из хеш-таблицы\nmap.erase(10583);\n
        hash_map.java
        /* Инициализация хеш-таблицы */\nMap<Integer, String> map = new HashMap<>();\n\n/* Операция добавления */\n// Добавить пару ключ-значение (key, value) в хеш-таблицу\nmap.put(12836, \"Сяо Ха\");\nmap.put(15937, \"Сяо Ло\");\nmap.put(16750, \"Сяо Суань\");\nmap.put(13276, \"Сяо Фа\");\nmap.put(10583, \"Сяо Я\");\n\n/* Операция поиска */\n// Передать в хеш-таблицу ключ key и получить значение value\nString name = map.get(15937);\n\n/* Операция удаления */\n// Удалить пару ключ-значение (key, value) из хеш-таблицы\nmap.remove(10583);\n
        hash_map.cs
        /* Инициализация хеш-таблицы */\nDictionary<int, string> map = new() {\n    /* Операция добавления */\n    // Добавить пару ключ-значение (key, value) в хеш-таблицу\n    { 12836, \"Сяо Ха\" },\n    { 15937, \"Сяо Ло\" },\n    { 16750, \"Сяо Суань\" },\n    { 13276, \"Сяо Фа\" },\n    { 10583, \"Сяо Я\" }\n};\n\n/* Операция поиска */\n// Передать в хеш-таблицу ключ key и получить значение value\nstring name = map[15937];\n\n/* Операция удаления */\n// Удалить пару ключ-значение (key, value) из хеш-таблицы\nmap.Remove(10583);\n
        hash_map_test.go
        /* Инициализация хеш-таблицы */\nhmap := make(map[int]string)\n\n/* Операция добавления */\n// Добавить пару ключ-значение (key, value) в хеш-таблицу\nhmap[12836] = \"Сяо Ха\"\nhmap[15937] = \"Сяо Ло\"\nhmap[16750] = \"Сяо Суань\"\nhmap[13276] = \"Сяо Фа\"\nhmap[10583] = \"Сяо Я\"\n\n/* Операция поиска */\n// Передать в хеш-таблицу ключ key и получить значение value\nname := hmap[15937]\n\n/* Операция удаления */\n// Удалить пару ключ-значение (key, value) из хеш-таблицы\ndelete(hmap, 10583)\n
        hash_map.swift
        /* Инициализация хеш-таблицы */\nvar map: [Int: String] = [:]\n\n/* Операция добавления */\n// Добавить пару ключ-значение (key, value) в хеш-таблицу\nmap[12836] = \"Сяо Ха\"\nmap[15937] = \"Сяо Ло\"\nmap[16750] = \"Сяо Суань\"\nmap[13276] = \"Сяо Фа\"\nmap[10583] = \"Сяо Я\"\n\n/* Операция поиска */\n// Передать в хеш-таблицу ключ key и получить значение value\nlet name = map[15937]!\n\n/* Операция удаления */\n// Удалить пару ключ-значение (key, value) из хеш-таблицы\nmap.removeValue(forKey: 10583)\n
        hash_map.js
        /* Инициализация хеш-таблицы */\nconst map = new Map();\n/* Операция добавления */\n// Добавить пару ключ-значение (key, value) в хеш-таблицу\nmap.set(12836, 'Сяо Ха');\nmap.set(15937, 'Сяо Ло');\nmap.set(16750, 'Сяо Суань');\nmap.set(13276, 'Сяо Фа');\nmap.set(10583, 'Сяо Я');\n\n/* Операция поиска */\n// Передать в хеш-таблицу ключ key и получить значение value\nlet name = map.get(15937);\n\n/* Операция удаления */\n// Удалить пару ключ-значение (key, value) из хеш-таблицы\nmap.delete(10583);\n
        hash_map.ts
        /* Инициализация хеш-таблицы */\nconst map = new Map<number, string>();\n/* Операция добавления */\n// Добавить пару ключ-значение (key, value) в хеш-таблицу\nmap.set(12836, 'Сяо Ха');\nmap.set(15937, 'Сяо Ло');\nmap.set(16750, 'Сяо Суань');\nmap.set(13276, 'Сяо Фа');\nmap.set(10583, 'Сяо Я');\nconsole.info('\\nПосле добавления хеш-таблица имеет вид\\nKey -> Value');\nconsole.info(map);\n\n/* Операция поиска */\n// Передать в хеш-таблицу ключ key и получить значение value\nlet name = map.get(15937);\nconsole.info('\\nПо номеру 15937 найдено имя ' + name);\n\n/* Операция удаления */\n// Удалить пару ключ-значение (key, value) из хеш-таблицы\nmap.delete(10583);\nconsole.info('\\nПосле удаления 10583 хеш-таблица имеет вид\\nKey -> Value');\nconsole.info(map);\n
        hash_map.dart
        /* Инициализация хеш-таблицы */\nMap<int, String> map = {};\n\n/* Операция добавления */\n// Добавить пару ключ-значение (key, value) в хеш-таблицу\nmap[12836] = \"Сяо Ха\";\nmap[15937] = \"Сяо Ло\";\nmap[16750] = \"Сяо Суань\";\nmap[13276] = \"Сяо Фа\";\nmap[10583] = \"Сяо Я\";\n\n/* Операция поиска */\n// Передать в хеш-таблицу ключ key и получить значение value\nString name = map[15937];\n\n/* Операция удаления */\n// Удалить пару ключ-значение (key, value) из хеш-таблицы\nmap.remove(10583);\n
        hash_map.rs
        use std::collections::HashMap;\n\n/* Инициализация хеш-таблицы */\nlet mut map: HashMap<i32, String> = HashMap::new();\n\n/* Операция добавления */\n// Добавить пару ключ-значение (key, value) в хеш-таблицу\nmap.insert(12836, \"Сяо Ха\".to_string());\nmap.insert(15937, \"Сяо Ло\".to_string());\nmap.insert(16750, \"Сяо Суань\".to_string());\nmap.insert(13279, \"Сяо Фа\".to_string());\nmap.insert(10583, \"Сяо Я\".to_string());\n\n/* Операция поиска */\n// Передать в хеш-таблицу ключ key и получить значение value\nlet _name: Option<&String> = map.get(&15937);\n\n/* Операция удаления */\n// Удалить пару ключ-значение (key, value) из хеш-таблицы\nlet _removed_value: Option<String> = map.remove(&10583);\n
        hash_map.c
        // В C нет встроенной хеш-таблицы\n
        hash_map.kt
        /* Инициализация хеш-таблицы */\nval map = HashMap<Int,String>()\n\n/* Операция добавления */\n// Добавить пару ключ-значение (key, value) в хеш-таблицу\nmap[12836] = \"Сяо Ха\"\nmap[15937] = \"Сяо Ло\"\nmap[16750] = \"Сяо Суань\"\nmap[13276] = \"Сяо Фа\"\nmap[10583] = \"Сяо Я\"\n\n/* Операция поиска */\n// Передать в хеш-таблицу ключ key и получить значение value\nval name = map[15937]\n\n/* Операция удаления */\n// Удалить пару ключ-значение (key, value) из хеш-таблицы\nmap.remove(10583)\n
        hash_map.rb
        # Инициализация хеш-таблицы\nhmap = {}\n\n# Операция добавления\n# Добавить пару ключ-значение (key, value) в хеш-таблицу\nhmap[12836] = \"Сяо Ха\"\nhmap[15937] = \"Сяо Ло\"\nhmap[16750] = \"Сяо Суань\"\nhmap[13276] = \"Сяо Фа\"\nhmap[10583] = \"Сяо Я\"\n\n# Операция поиска\n# Передать в хеш-таблицу ключ key и получить значение value\nname = hmap[15937]\n\n# Операция удаления\n# Удалить пару ключ-значение (key, value) из хеш-таблицы\nhmap.delete(10583)\n
        Визуализация выполнения

        https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%85%D0%B5%D1%88-%D1%82%D0%B0%D0%B1%D0%BB%D0%B8%D1%86%D1%83%0A%20%20%20%20hmap%20%3D%20%7B%7D%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%9E%D0%BF%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D1%8F%20%D0%B4%D0%BE%D0%B1%D0%B0%D0%B2%D0%BB%D0%B5%D0%BD%D0%B8%D1%8F%0A%20%20%20%20%23%20%D0%94%D0%BE%D0%B1%D0%B0%D0%B2%D0%B8%D1%82%D1%8C%20%D0%B2%20%D1%85%D0%B5%D1%88-%D1%82%D0%B0%D0%B1%D0%BB%D0%B8%D1%86%D1%83%20%D0%BF%D0%B0%D1%80%D1%83%20%D0%BA%D0%BB%D1%8E%D1%87-%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%28key%2C%20value%29%0A%20%20%20%20hmap%5B12836%5D%20%3D%20%22%D0%A1%D1%8F%D0%BE%20%D0%A5%D0%B0%22%0A%20%20%20%20hmap%5B15937%5D%20%3D%20%22%D0%A1%D1%8F%D0%BE%20%D0%9B%D0%BE%22%0A%20%20%20%20hmap%5B16750%5D%20%3D%20%22%D0%A1%D1%8F%D0%BE%20%D0%A1%D1%83%D0%B0%D0%BD%D1%8C%22%0A%20%20%20%20hmap%5B13276%5D%20%3D%20%22%D0%A1%D1%8F%D0%BE%20%D0%A4%D0%B0%22%0A%20%20%20%20hmap%5B10583%5D%20%3D%20%22%D0%A3%D1%82%D0%B5%D0%BD%D0%BE%D0%BA%22%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%9E%D0%BF%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D1%8F%20%D0%BF%D0%BE%D0%B8%D1%81%D0%BA%D0%B0%0A%20%20%20%20%23%20%D0%9F%D0%B5%D1%80%D0%B5%D0%B4%D0%B0%D1%82%D1%8C%20%D0%BA%D0%BB%D1%8E%D1%87%20key%20%D0%B2%20%D1%85%D0%B5%D1%88-%D1%82%D0%B0%D0%B1%D0%BB%D0%B8%D1%86%D1%83%20%D0%B8%20%D0%BF%D0%BE%D0%BB%D1%83%D1%87%D0%B8%D1%82%D1%8C%20%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20value%0A%20%20%20%20name%20%3D%20hmap%5B15937%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%9E%D0%BF%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D1%8F%20%D1%83%D0%B4%D0%B0%D0%BB%D0%B5%D0%BD%D0%B8%D1%8F%0A%20%20%20%20%23%20%D0%A3%D0%B4%D0%B0%D0%BB%D0%B8%D1%82%D1%8C%20%D0%B8%D0%B7%20%D1%85%D0%B5%D1%88-%D1%82%D0%B0%D0%B1%D0%BB%D0%B8%D1%86%D1%8B%20%D0%BF%D0%B0%D1%80%D1%83%20%D0%BA%D0%BB%D1%8E%D1%87-%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%28key%2C%20value%29%0A%20%20%20%20hmap.pop%2810583%29&cumulative=false&curInstr=2&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

        Существует три распространенных способа обхода хеш-таблицы: обход пар ключ-значение, обход ключей и обход значений. Примеры кода приведены ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map.py
        # Обход хеш-таблицы\n# Обход пар ключ-значение key->value\nfor key, value in hmap.items():\n    print(key, \"->\", value)\n# Обход только ключей key\nfor key in hmap.keys():\n    print(key)\n# Обход только значений value\nfor value in hmap.values():\n    print(value)\n
        hash_map.cpp
        /* Обход хеш-таблицы */\n// Обход пар ключ-значение key->value\nfor (auto kv: map) {\n    cout << kv.first << \" -> \" << kv.second << endl;\n}\n// Обход key->value с помощью итератора\nfor (auto iter = map.begin(); iter != map.end(); iter++) {\n    cout << iter->first << \"->\" << iter->second << endl;\n}\n
        hash_map.java
        /* Обход хеш-таблицы */\n// Обход пар ключ-значение key->value\nfor (Map.Entry <Integer, String> kv: map.entrySet()) {\n    System.out.println(kv.getKey() + \" -> \" + kv.getValue());\n}\n// Обход только ключей key\nfor (int key: map.keySet()) {\n    System.out.println(key);\n}\n// Обход только значений value\nfor (String val: map.values()) {\n    System.out.println(val);\n}\n
        hash_map.cs
        /* Обход хеш-таблицы */\n// Обход пар ключ-значение Key->Value\nforeach (var kv in map) {\n    Console.WriteLine(kv.Key + \" -> \" + kv.Value);\n}\n// Обход только ключей key\nforeach (int key in map.Keys) {\n    Console.WriteLine(key);\n}\n// Обход только значений value\nforeach (string val in map.Values) {\n    Console.WriteLine(val);\n}\n
        hash_map_test.go
        /* Обход хеш-таблицы */\n// Обход пар ключ-значение key->value\nfor key, value := range hmap {\n    fmt.Println(key, \"->\", value)\n}\n// Обход только ключей key\nfor key := range hmap {\n    fmt.Println(key)\n}\n// Обход только значений value\nfor _, value := range hmap {\n    fmt.Println(value)\n}\n
        hash_map.swift
        /* Обход хеш-таблицы */\n// Обход пар ключ-значение Key->Value\nfor (key, value) in map {\n    print(\"\\(key) -> \\(value)\")\n}\n// Обход только ключей Key\nfor key in map.keys {\n    print(key)\n}\n// Обход только значений Value\nfor value in map.values {\n    print(value)\n}\n
        hash_map.js
        /* Обход хеш-таблицы */\nconsole.info('\\nОбход пар ключ-значение Key->Value');\nfor (const [k, v] of map.entries()) {\n    console.info(k + ' -> ' + v);\n}\nconsole.info('\\nОбход только ключей Key');\nfor (const k of map.keys()) {\n    console.info(k);\n}\nconsole.info('\\nОбход только значений Value');\nfor (const v of map.values()) {\n    console.info(v);\n}\n
        hash_map.ts
        /* Обход хеш-таблицы */\nconsole.info('\\nОбход пар ключ-значение Key->Value');\nfor (const [k, v] of map.entries()) {\n    console.info(k + ' -> ' + v);\n}\nconsole.info('\\nОбход только ключей Key');\nfor (const k of map.keys()) {\n    console.info(k);\n}\nconsole.info('\\nОбход только значений Value');\nfor (const v of map.values()) {\n    console.info(v);\n}\n
        hash_map.dart
        /* Обход хеш-таблицы */\n// Обход пар ключ-значение Key->Value\nmap.forEach((key, value) {\n  print('$key -> $value');\n});\n\n// Обход только ключей Key\nmap.keys.forEach((key) {\n  print(key);\n});\n\n// Обход только значений Value\nmap.values.forEach((value) {\n  print(value);\n});\n
        hash_map.rs
        /* Обход хеш-таблицы */\n// Обход пар ключ-значение Key->Value\nfor (key, value) in &map {\n    println!(\"{key} -> {value}\");\n}\n\n// Обход только ключей Key\nfor key in map.keys() {\n    println!(\"{key}\");\n}\n\n// Обход только значений Value\nfor value in map.values() {\n    println!(\"{value}\");\n}\n
        hash_map.c
        // В C нет встроенной хеш-таблицы\n
        hash_map.kt
        /* Обход хеш-таблицы */\n// Обход пар ключ-значение key->value\nfor ((key, value) in map) {\n    println(\"$key -> $value\")\n}\n// Обход только ключей key\nfor (key in map.keys) {\n    println(key)\n}\n// Обход только значений value\nfor (_val in map.values) {\n    println(_val)\n}\n
        hash_map.rb
        # Обход хеш-таблицы\n# Обход пар ключ-значение key->value\nhmap.entries.each { |key, value| puts \"#{key} -> #{value}\" }\n\n# Обход только ключей key\nhmap.keys.each { |key| puts key }\n\n# Обход только значений value\nhmap.values.each { |val| puts val }\n
        Визуализация выполнения

        https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%85%D0%B5%D1%88-%D1%82%D0%B0%D0%B1%D0%BB%D0%B8%D1%86%D1%83%0A%20%20%20%20hmap%20%3D%20%7B%7D%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%9E%D0%BF%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D1%8F%20%D0%B4%D0%BE%D0%B1%D0%B0%D0%B2%D0%BB%D0%B5%D0%BD%D0%B8%D1%8F%0A%20%20%20%20%23%20%D0%94%D0%BE%D0%B1%D0%B0%D0%B2%D0%B8%D1%82%D1%8C%20%D0%B2%20%D1%85%D0%B5%D1%88-%D1%82%D0%B0%D0%B1%D0%BB%D0%B8%D1%86%D1%83%20%D0%BF%D0%B0%D1%80%D1%83%20%D0%BA%D0%BB%D1%8E%D1%87-%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%28key%2C%20value%29%0A%20%20%20%20hmap%5B12836%5D%20%3D%20%22%D0%A1%D1%8F%D0%BE%20%D0%A5%D0%B0%22%0A%20%20%20%20hmap%5B15937%5D%20%3D%20%22%D0%A1%D1%8F%D0%BE%20%D0%9B%D0%BE%22%0A%20%20%20%20hmap%5B16750%5D%20%3D%20%22%D0%A1%D1%8F%D0%BE%20%D0%A1%D1%83%D0%B0%D0%BD%D1%8C%22%0A%20%20%20%20hmap%5B13276%5D%20%3D%20%22%D0%A1%D1%8F%D0%BE%20%D0%A4%D0%B0%22%0A%20%20%20%20hmap%5B10583%5D%20%3D%20%22%D0%A3%D1%82%D0%B5%D0%BD%D0%BE%D0%BA%22%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%9F%D0%B5%D1%80%D0%B5%D0%B1%D1%80%D0%B0%D1%82%D1%8C%20%D1%85%D0%B5%D1%88-%D1%82%D0%B0%D0%B1%D0%BB%D0%B8%D1%86%D1%83%0A%20%20%20%20%23%20%D0%9E%D0%B1%D0%BE%D0%B9%D1%82%D0%B8%D0%BF%D0%B0%D1%80%D0%B0%20%D0%BA%D0%BB%D1%8E%D1%87-%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20key-%3Evalue%0A%20%20%20%20for%20key%2C%20value%20in%20hmap.items%28%29%3A%0A%20%20%20%20%20%20%20%20print%28key%2C%20%22-%3E%22%2C%20value%29%0A%20%20%20%20%23%20%D0%BE%D1%82%D0%B4%D0%B5%D0%BB%D1%8C%D0%BD%D0%BE%D0%9E%D0%B1%D0%BE%D0%B9%D1%82%D0%B8%D0%BA%D0%BB%D1%8E%D1%87%20key%0A%20%20%20%20for%20key%20in%20hmap.keys%28%29%3A%0A%20%20%20%20%20%20%20%20print%28key%29%0A%20%20%20%20%23%20%D0%BE%D1%82%D0%B4%D0%B5%D0%BB%D1%8C%D0%BD%D0%BE%D0%9E%D0%B1%D0%BE%D0%B9%D1%82%D0%B8%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20value%0A%20%20%20%20for%20value%20in%20hmap.values%28%29%3A%0A%20%20%20%20%20%20%20%20print%28value%29&cumulative=false&curInstr=8&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

        ","path":["Глава 6. Хеш-таблицы","6.1   Хеш-таблица"],"tags":[]},{"location":"chapter_hashing/hash_map/#612-","level":2,"title":"6.1.2   Простая реализация хеш-таблицы","text":"

        Сначала рассмотрим самый простой случай: реализуем хеш-таблицу только с помощью одного массива. В хеш-таблице каждую пустую ячейку массива мы называем бакетом (bucket), и каждый бакет может хранить одну пару ключ-значение. Следовательно, операция поиска сводится к тому, чтобы найти бакет, соответствующий key , и получить из него value .

        Но как определить бакет, соответствующий заданному key ? Это делается с помощью хеш-функции (hash function). Назначение хеш-функции - отображать большое входное пространство в меньшее выходное пространство. В хеш-таблице входным пространством являются все key , а выходным - все бакеты, то есть индексы массива. Иначе говоря, передав key на вход, мы можем с помощью хеш-функции получить позицию хранения соответствующей пары ключ-значение в массиве.

        Процесс вычисления хеш-функции для одного key включает два шага.

        1. Сначала с помощью некоторого хеш-алгоритма hash() вычисляется хеш-значение.
        2. Затем хеш-значение берется по модулю числа бакетов (длины массива) capacity , чтобы получить бакет (индекс массива) index , соответствующий этому key .
        index = hash(key) % capacity\n

        После этого можно использовать index для доступа к соответствующему бакету в хеш-таблице и получения value .

        Пусть длина массива capacity = 100 , а хеш-алгоритм hash(key) = key . Тогда легко получить хеш-функцию key % 100 . На рисунке 6-2 на примере key \"номер студенческого билета\" и value \"имя\" показан принцип работы хеш-функции.

        Рисунок 6-2   Принцип работы хеш-функции

        Ниже приведен код простой реализации хеш-таблицы. В нем мы инкапсулируем key и value в класс Pair , чтобы представить пару ключ-значение.

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_hash_map.py
        class Pair:\n    \"\"\"Пара ключ-значение\"\"\"\n\n    def __init__(self, key: int, val: str):\n        self.key = key\n        self.val = val\n\nclass ArrayHashMap:\n    \"\"\"Хеш-таблица на основе массива\"\"\"\n\n    def __init__(self):\n        \"\"\"Конструктор\"\"\"\n        # Инициализировать массив, содержащий 100 корзин\n        self.buckets: list[Pair | None] = [None] * 100\n\n    def hash_func(self, key: int) -> int:\n        \"\"\"Хеш-функция\"\"\"\n        index = key % 100\n        return index\n\n    def get(self, key: int) -> str | None:\n        \"\"\"Операция поиска\"\"\"\n        index: int = self.hash_func(key)\n        pair: Pair = self.buckets[index]\n        if pair is None:\n            return None\n        return pair.val\n\n    def put(self, key: int, val: str):\n        \"\"\"Операции добавления и обновления\"\"\"\n        pair = Pair(key, val)\n        index: int = self.hash_func(key)\n        self.buckets[index] = pair\n\n    def remove(self, key: int):\n        \"\"\"Операция удаления\"\"\"\n        index: int = self.hash_func(key)\n        # Присвоить None, что означает удаление\n        self.buckets[index] = None\n\n    def entry_set(self) -> list[Pair]:\n        \"\"\"Получить все пары ключ-значение\"\"\"\n        result: list[Pair] = []\n        for pair in self.buckets:\n            if pair is not None:\n                result.append(pair)\n        return result\n\n    def key_set(self) -> list[int]:\n        \"\"\"Получить все ключи\"\"\"\n        result = []\n        for pair in self.buckets:\n            if pair is not None:\n                result.append(pair.key)\n        return result\n\n    def value_set(self) -> list[str]:\n        \"\"\"Получить все значения\"\"\"\n        result = []\n        for pair in self.buckets:\n            if pair is not None:\n                result.append(pair.val)\n        return result\n\n    def print(self):\n        \"\"\"Вывести хеш-таблицу\"\"\"\n        for pair in self.buckets:\n            if pair is not None:\n                print(pair.key, \"->\", pair.val)\n
        array_hash_map.cpp
        /* Пара ключ-значение */\nstruct Pair {\n  public:\n    int key;\n    string val;\n    Pair(int key, string val) {\n        this->key = key;\n        this->val = val;\n    }\n};\n\n/* Хеш-таблица на основе массива */\nclass ArrayHashMap {\n  private:\n    vector<Pair *> buckets;\n\n  public:\n    ArrayHashMap() {\n        // Инициализировать массив, содержащий 100 корзин\n        buckets = vector<Pair *>(100);\n    }\n\n    ~ArrayHashMap() {\n        // Освободить память\n        for (const auto &bucket : buckets) {\n            delete bucket;\n        }\n        buckets.clear();\n    }\n\n    /* Хеш-функция */\n    int hashFunc(int key) {\n        int index = key % 100;\n        return index;\n    }\n\n    /* Операция поиска */\n    string get(int key) {\n        int index = hashFunc(key);\n        Pair *pair = buckets[index];\n        if (pair == nullptr)\n            return \"\";\n        return pair->val;\n    }\n\n    /* Операция добавления */\n    void put(int key, string val) {\n        Pair *pair = new Pair(key, val);\n        int index = hashFunc(key);\n        buckets[index] = pair;\n    }\n\n    /* Операция удаления */\n    void remove(int key) {\n        int index = hashFunc(key);\n        // Освободить память и присвоить nullptr\n        delete buckets[index];\n        buckets[index] = nullptr;\n    }\n\n    /* Получить все пары ключ-значение */\n    vector<Pair *> pairSet() {\n        vector<Pair *> pairSet;\n        for (Pair *pair : buckets) {\n            if (pair != nullptr) {\n                pairSet.push_back(pair);\n            }\n        }\n        return pairSet;\n    }\n\n    /* Получить все ключи */\n    vector<int> keySet() {\n        vector<int> keySet;\n        for (Pair *pair : buckets) {\n            if (pair != nullptr) {\n                keySet.push_back(pair->key);\n            }\n        }\n        return keySet;\n    }\n\n    /* Получить все значения */\n    vector<string> valueSet() {\n        vector<string> valueSet;\n        for (Pair *pair : buckets) {\n            if (pair != nullptr) {\n                valueSet.push_back(pair->val);\n            }\n        }\n        return valueSet;\n    }\n\n    /* Вывести хеш-таблицу */\n    void print() {\n        for (Pair *kv : pairSet()) {\n            cout << kv->key << \" -> \" << kv->val << endl;\n        }\n    }\n};\n
        array_hash_map.java
        /* Пара ключ-значение */\nclass Pair {\n    public int key;\n    public String val;\n\n    public Pair(int key, String val) {\n        this.key = key;\n        this.val = val;\n    }\n}\n\n/* Хеш-таблица на основе массива */\nclass ArrayHashMap {\n    private List<Pair> buckets;\n\n    public ArrayHashMap() {\n        // Инициализировать массив, содержащий 100 корзин\n        buckets = new ArrayList<>();\n        for (int i = 0; i < 100; i++) {\n            buckets.add(null);\n        }\n    }\n\n    /* Хеш-функция */\n    private int hashFunc(int key) {\n        int index = key % 100;\n        return index;\n    }\n\n    /* Операция поиска */\n    public String get(int key) {\n        int index = hashFunc(key);\n        Pair pair = buckets.get(index);\n        if (pair == null)\n            return null;\n        return pair.val;\n    }\n\n    /* Операция добавления */\n    public void put(int key, String val) {\n        Pair pair = new Pair(key, val);\n        int index = hashFunc(key);\n        buckets.set(index, pair);\n    }\n\n    /* Операция удаления */\n    public void remove(int key) {\n        int index = hashFunc(key);\n        // Присвоить null, что означает удаление\n        buckets.set(index, null);\n    }\n\n    /* Получить все пары ключ-значение */\n    public List<Pair> pairSet() {\n        List<Pair> pairSet = new ArrayList<>();\n        for (Pair pair : buckets) {\n            if (pair != null)\n                pairSet.add(pair);\n        }\n        return pairSet;\n    }\n\n    /* Получить все ключи */\n    public List<Integer> keySet() {\n        List<Integer> keySet = new ArrayList<>();\n        for (Pair pair : buckets) {\n            if (pair != null)\n                keySet.add(pair.key);\n        }\n        return keySet;\n    }\n\n    /* Получить все значения */\n    public List<String> valueSet() {\n        List<String> valueSet = new ArrayList<>();\n        for (Pair pair : buckets) {\n            if (pair != null)\n                valueSet.add(pair.val);\n        }\n        return valueSet;\n    }\n\n    /* Вывести хеш-таблицу */\n    public void print() {\n        for (Pair kv : pairSet()) {\n            System.out.println(kv.key + \" -> \" + kv.val);\n        }\n    }\n}\n
        array_hash_map.cs
        /* Пара ключ-значение int->string */\nclass Pair(int key, string val) {\n    public int key = key;\n    public string val = val;\n}\n\n/* Хеш-таблица на основе массива */\nclass ArrayHashMap {\n    List<Pair?> buckets;\n    public ArrayHashMap() {\n        // Инициализировать массив, содержащий 100 корзин\n        buckets = [];\n        for (int i = 0; i < 100; i++) {\n            buckets.Add(null);\n        }\n    }\n\n    /* Хеш-функция */\n    int HashFunc(int key) {\n        int index = key % 100;\n        return index;\n    }\n\n    /* Операция поиска */\n    public string? Get(int key) {\n        int index = HashFunc(key);\n        Pair? pair = buckets[index];\n        if (pair == null) return null;\n        return pair.val;\n    }\n\n    /* Операция добавления */\n    public void Put(int key, string val) {\n        Pair pair = new(key, val);\n        int index = HashFunc(key);\n        buckets[index] = pair;\n    }\n\n    /* Операция удаления */\n    public void Remove(int key) {\n        int index = HashFunc(key);\n        // Присвоить null, что означает удаление\n        buckets[index] = null;\n    }\n\n    /* Получить все пары ключ-значение */\n    public List<Pair> PairSet() {\n        List<Pair> pairSet = [];\n        foreach (Pair? pair in buckets) {\n            if (pair != null)\n                pairSet.Add(pair);\n        }\n        return pairSet;\n    }\n\n    /* Получить все ключи */\n    public List<int> KeySet() {\n        List<int> keySet = [];\n        foreach (Pair? pair in buckets) {\n            if (pair != null)\n                keySet.Add(pair.key);\n        }\n        return keySet;\n    }\n\n    /* Получить все значения */\n    public List<string> ValueSet() {\n        List<string> valueSet = [];\n        foreach (Pair? pair in buckets) {\n            if (pair != null)\n                valueSet.Add(pair.val);\n        }\n        return valueSet;\n    }\n\n    /* Вывести хеш-таблицу */\n    public void Print() {\n        foreach (Pair kv in PairSet()) {\n            Console.WriteLine(kv.key + \" -> \" + kv.val);\n        }\n    }\n}\n
        array_hash_map.go
        /* Пара ключ-значение */\ntype pair struct {\n    key int\n    val string\n}\n\n/* Хеш-таблица на основе массива */\ntype arrayHashMap struct {\n    buckets []*pair\n}\n\n/* Инициализация хеш-таблицы */\nfunc newArrayHashMap() *arrayHashMap {\n    // Инициализировать массив, содержащий 100 корзин\n    buckets := make([]*pair, 100)\n    return &arrayHashMap{buckets: buckets}\n}\n\n/* Хеш-функция */\nfunc (a *arrayHashMap) hashFunc(key int) int {\n    index := key % 100\n    return index\n}\n\n/* Операция поиска */\nfunc (a *arrayHashMap) get(key int) string {\n    index := a.hashFunc(key)\n    pair := a.buckets[index]\n    if pair == nil {\n        return \"Not Found\"\n    }\n    return pair.val\n}\n\n/* Операция добавления */\nfunc (a *arrayHashMap) put(key int, val string) {\n    pair := &pair{key: key, val: val}\n    index := a.hashFunc(key)\n    a.buckets[index] = pair\n}\n\n/* Операция удаления */\nfunc (a *arrayHashMap) remove(key int) {\n    index := a.hashFunc(key)\n    // Присвоить nil, что означает удаление\n    a.buckets[index] = nil\n}\n\n/* Получить все ключи */\nfunc (a *arrayHashMap) pairSet() []*pair {\n    var pairs []*pair\n    for _, pair := range a.buckets {\n        if pair != nil {\n            pairs = append(pairs, pair)\n        }\n    }\n    return pairs\n}\n\n/* Получить все ключи */\nfunc (a *arrayHashMap) keySet() []int {\n    var keys []int\n    for _, pair := range a.buckets {\n        if pair != nil {\n            keys = append(keys, pair.key)\n        }\n    }\n    return keys\n}\n\n/* Получить все значения */\nfunc (a *arrayHashMap) valueSet() []string {\n    var values []string\n    for _, pair := range a.buckets {\n        if pair != nil {\n            values = append(values, pair.val)\n        }\n    }\n    return values\n}\n\n/* Вывести хеш-таблицу */\nfunc (a *arrayHashMap) print() {\n    for _, pair := range a.buckets {\n        if pair != nil {\n            fmt.Println(pair.key, \"->\", pair.val)\n        }\n    }\n}\n
        array_hash_map.swift
        /* Пара ключ-значение */\nclass Pair: Equatable {\n    public var key: Int\n    public var val: String\n\n    public init(key: Int, val: String) {\n        self.key = key\n        self.val = val\n    }\n\n    public static func == (lhs: Pair, rhs: Pair) -> Bool {\n        lhs.key == rhs.key && lhs.val == rhs.val\n    }\n}\n\n/* Хеш-таблица на основе массива */\nclass ArrayHashMap {\n    private var buckets: [Pair?]\n\n    init() {\n        // Инициализировать массив, содержащий 100 корзин\n        buckets = Array(repeating: nil, count: 100)\n    }\n\n    /* Хеш-функция */\n    private func hashFunc(key: Int) -> Int {\n        let index = key % 100\n        return index\n    }\n\n    /* Операция поиска */\n    func get(key: Int) -> String? {\n        let index = hashFunc(key: key)\n        let pair = buckets[index]\n        return pair?.val\n    }\n\n    /* Операция добавления */\n    func put(key: Int, val: String) {\n        let pair = Pair(key: key, val: val)\n        let index = hashFunc(key: key)\n        buckets[index] = pair\n    }\n\n    /* Операция удаления */\n    func remove(key: Int) {\n        let index = hashFunc(key: key)\n        // Присвоить nil, что означает удаление\n        buckets[index] = nil\n    }\n\n    /* Получить все пары ключ-значение */\n    func pairSet() -> [Pair] {\n        buckets.compactMap { $0 }\n    }\n\n    /* Получить все ключи */\n    func keySet() -> [Int] {\n        buckets.compactMap { $0?.key }\n    }\n\n    /* Получить все значения */\n    func valueSet() -> [String] {\n        buckets.compactMap { $0?.val }\n    }\n\n    /* Вывести хеш-таблицу */\n    func print() {\n        for pair in pairSet() {\n            Swift.print(\"\\(pair.key) -> \\(pair.val)\")\n        }\n    }\n}\n
        array_hash_map.js
        /* Пара ключ-значение Number -> String */\nclass Pair {\n    constructor(key, val) {\n        this.key = key;\n        this.val = val;\n    }\n}\n\n/* Хеш-таблица на основе массива */\nclass ArrayHashMap {\n    #buckets;\n    constructor() {\n        // Инициализировать массив, содержащий 100 корзин\n        this.#buckets = new Array(100).fill(null);\n    }\n\n    /* Хеш-функция */\n    #hashFunc(key) {\n        return key % 100;\n    }\n\n    /* Операция поиска */\n    get(key) {\n        let index = this.#hashFunc(key);\n        let pair = this.#buckets[index];\n        if (pair === null) return null;\n        return pair.val;\n    }\n\n    /* Операция добавления */\n    set(key, val) {\n        let index = this.#hashFunc(key);\n        this.#buckets[index] = new Pair(key, val);\n    }\n\n    /* Операция удаления */\n    delete(key) {\n        let index = this.#hashFunc(key);\n        // Присвоить null, что означает удаление\n        this.#buckets[index] = null;\n    }\n\n    /* Получить все пары ключ-значение */\n    entries() {\n        let arr = [];\n        for (let i = 0; i < this.#buckets.length; i++) {\n            if (this.#buckets[i]) {\n                arr.push(this.#buckets[i]);\n            }\n        }\n        return arr;\n    }\n\n    /* Получить все ключи */\n    keys() {\n        let arr = [];\n        for (let i = 0; i < this.#buckets.length; i++) {\n            if (this.#buckets[i]) {\n                arr.push(this.#buckets[i].key);\n            }\n        }\n        return arr;\n    }\n\n    /* Получить все значения */\n    values() {\n        let arr = [];\n        for (let i = 0; i < this.#buckets.length; i++) {\n            if (this.#buckets[i]) {\n                arr.push(this.#buckets[i].val);\n            }\n        }\n        return arr;\n    }\n\n    /* Вывести хеш-таблицу */\n    print() {\n        let pairSet = this.entries();\n        for (const pair of pairSet) {\n            console.info(`${pair.key} -> ${pair.val}`);\n        }\n    }\n}\n
        array_hash_map.ts
        /* Пара ключ-значение Number -> String */\nclass Pair {\n    public key: number;\n    public val: string;\n\n    constructor(key: number, val: string) {\n        this.key = key;\n        this.val = val;\n    }\n}\n\n/* Хеш-таблица на основе массива */\nclass ArrayHashMap {\n    private readonly buckets: (Pair | null)[];\n\n    constructor() {\n        // Инициализировать массив, содержащий 100 корзин\n        this.buckets = new Array(100).fill(null);\n    }\n\n    /* Хеш-функция */\n    private hashFunc(key: number): number {\n        return key % 100;\n    }\n\n    /* Операция поиска */\n    public get(key: number): string | null {\n        let index = this.hashFunc(key);\n        let pair = this.buckets[index];\n        if (pair === null) return null;\n        return pair.val;\n    }\n\n    /* Операция добавления */\n    public set(key: number, val: string) {\n        let index = this.hashFunc(key);\n        this.buckets[index] = new Pair(key, val);\n    }\n\n    /* Операция удаления */\n    public delete(key: number) {\n        let index = this.hashFunc(key);\n        // Присвоить null, что означает удаление\n        this.buckets[index] = null;\n    }\n\n    /* Получить все пары ключ-значение */\n    public entries(): (Pair | null)[] {\n        let arr: (Pair | null)[] = [];\n        for (let i = 0; i < this.buckets.length; i++) {\n            if (this.buckets[i]) {\n                arr.push(this.buckets[i]);\n            }\n        }\n        return arr;\n    }\n\n    /* Получить все ключи */\n    public keys(): (number | undefined)[] {\n        let arr: (number | undefined)[] = [];\n        for (let i = 0; i < this.buckets.length; i++) {\n            if (this.buckets[i]) {\n                arr.push(this.buckets[i].key);\n            }\n        }\n        return arr;\n    }\n\n    /* Получить все значения */\n    public values(): (string | undefined)[] {\n        let arr: (string | undefined)[] = [];\n        for (let i = 0; i < this.buckets.length; i++) {\n            if (this.buckets[i]) {\n                arr.push(this.buckets[i].val);\n            }\n        }\n        return arr;\n    }\n\n    /* Вывести хеш-таблицу */\n    public print() {\n        let pairSet = this.entries();\n        for (const pair of pairSet) {\n            console.info(`${pair.key} -> ${pair.val}`);\n        }\n    }\n}\n
        array_hash_map.dart
        /* Пара ключ-значение */\nclass Pair {\n  int key;\n  String val;\n  Pair(this.key, this.val);\n}\n\n/* Хеш-таблица на основе массива */\nclass ArrayHashMap {\n  late List<Pair?> _buckets;\n\n  ArrayHashMap() {\n    // Инициализировать массив, содержащий 100 корзин\n    _buckets = List.filled(100, null);\n  }\n\n  /* Хеш-функция */\n  int _hashFunc(int key) {\n    final int index = key % 100;\n    return index;\n  }\n\n  /* Операция поиска */\n  String? get(int key) {\n    final int index = _hashFunc(key);\n    final Pair? pair = _buckets[index];\n    if (pair == null) {\n      return null;\n    }\n    return pair.val;\n  }\n\n  /* Операция добавления */\n  void put(int key, String val) {\n    final Pair pair = Pair(key, val);\n    final int index = _hashFunc(key);\n    _buckets[index] = pair;\n  }\n\n  /* Операция удаления */\n  void remove(int key) {\n    final int index = _hashFunc(key);\n    _buckets[index] = null;\n  }\n\n  /* Получить все пары ключ-значение */\n  List<Pair> pairSet() {\n    List<Pair> pairSet = [];\n    for (final Pair? pair in _buckets) {\n      if (pair != null) {\n        pairSet.add(pair);\n      }\n    }\n    return pairSet;\n  }\n\n  /* Получить все ключи */\n  List<int> keySet() {\n    List<int> keySet = [];\n    for (final Pair? pair in _buckets) {\n      if (pair != null) {\n        keySet.add(pair.key);\n      }\n    }\n    return keySet;\n  }\n\n  /* Получить все значения */\n  List<String> values() {\n    List<String> valueSet = [];\n    for (final Pair? pair in _buckets) {\n      if (pair != null) {\n        valueSet.add(pair.val);\n      }\n    }\n    return valueSet;\n  }\n\n  /* Вывести хеш-таблицу */\n  void printHashMap() {\n    for (final Pair kv in pairSet()) {\n      print(\"${kv.key} -> ${kv.val}\");\n    }\n  }\n}\n
        array_hash_map.rs
        /* Пара ключ-значение */\n#[derive(Debug, Clone, PartialEq)]\npub struct Pair {\n    pub key: i32,\n    pub val: String,\n}\n\n/* Хеш-таблица на основе массива */\npub struct ArrayHashMap {\n    buckets: Vec<Option<Pair>>,\n}\n\nimpl ArrayHashMap {\n    pub fn new() -> ArrayHashMap {\n        // Инициализировать массив, содержащий 100 корзин\n        Self {\n            buckets: vec![None; 100],\n        }\n    }\n\n    /* Хеш-функция */\n    fn hash_func(&self, key: i32) -> usize {\n        key as usize % 100\n    }\n\n    /* Операция поиска */\n    pub fn get(&self, key: i32) -> Option<&String> {\n        let index = self.hash_func(key);\n        self.buckets[index].as_ref().map(|pair| &pair.val)\n    }\n\n    /* Операция добавления */\n    pub fn put(&mut self, key: i32, val: &str) {\n        let index = self.hash_func(key);\n        self.buckets[index] = Some(Pair {\n            key,\n            val: val.to_string(),\n        });\n    }\n\n    /* Операция удаления */\n    pub fn remove(&mut self, key: i32) {\n        let index = self.hash_func(key);\n        // Присвоить None, что означает удаление\n        self.buckets[index] = None;\n    }\n\n    /* Получить все пары ключ-значение */\n    pub fn entry_set(&self) -> Vec<&Pair> {\n        self.buckets\n            .iter()\n            .filter_map(|pair| pair.as_ref())\n            .collect()\n    }\n\n    /* Получить все ключи */\n    pub fn key_set(&self) -> Vec<&i32> {\n        self.buckets\n            .iter()\n            .filter_map(|pair| pair.as_ref().map(|pair| &pair.key))\n            .collect()\n    }\n\n    /* Получить все значения */\n    pub fn value_set(&self) -> Vec<&String> {\n        self.buckets\n            .iter()\n            .filter_map(|pair| pair.as_ref().map(|pair| &pair.val))\n            .collect()\n    }\n\n    /* Вывести хеш-таблицу */\n    pub fn print(&self) {\n        for pair in self.entry_set() {\n            println!(\"{} -> {}\", pair.key, pair.val);\n        }\n    }\n}\n
        array_hash_map.c
        /* Пара ключ-значение int->string */\ntypedef struct {\n    int key;\n    char *val;\n} Pair;\n\n/* Хеш-таблица на основе массива */\ntypedef struct {\n    Pair *buckets[MAX_SIZE];\n} ArrayHashMap;\n\n/* Конструктор */\nArrayHashMap *newArrayHashMap() {\n    ArrayHashMap *hmap = malloc(sizeof(ArrayHashMap));\n    for (int i=0; i < MAX_SIZE; i++) {\n        hmap->buckets[i] = NULL;\n    }\n    return hmap;\n}\n\n/* Деструктор */\nvoid delArrayHashMap(ArrayHashMap *hmap) {\n    for (int i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            free(hmap->buckets[i]->val);\n            free(hmap->buckets[i]);\n        }\n    }\n    free(hmap);\n}\n\n/* Операция добавления */\nvoid put(ArrayHashMap *hmap, const int key, const char *val) {\n    Pair *Pair = malloc(sizeof(Pair));\n    Pair->key = key;\n    Pair->val = malloc(strlen(val) + 1);\n    strcpy(Pair->val, val);\n\n    int index = hashFunc(key);\n    hmap->buckets[index] = Pair;\n}\n\n/* Операция удаления */\nvoid removeItem(ArrayHashMap *hmap, const int key) {\n    int index = hashFunc(key);\n    free(hmap->buckets[index]->val);\n    free(hmap->buckets[index]);\n    hmap->buckets[index] = NULL;\n}\n\n/* Получить все пары ключ-значение */\nvoid pairSet(ArrayHashMap *hmap, MapSet *set) {\n    Pair *entries;\n    int i = 0, index = 0;\n    int total = 0;\n    /* Подсчитать число действительных пар ключ-значение */\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            total++;\n        }\n    }\n    entries = malloc(sizeof(Pair) * total);\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            entries[index].key = hmap->buckets[i]->key;\n            entries[index].val = malloc(strlen(hmap->buckets[i]->val) + 1);\n            strcpy(entries[index].val, hmap->buckets[i]->val);\n            index++;\n        }\n    }\n    set->set = entries;\n    set->len = total;\n}\n\n/* Получить все ключи */\nvoid keySet(ArrayHashMap *hmap, MapSet *set) {\n    int *keys;\n    int i = 0, index = 0;\n    int total = 0;\n    /* Подсчитать число действительных пар ключ-значение */\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            total++;\n        }\n    }\n    keys = malloc(total * sizeof(int));\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            keys[index] = hmap->buckets[i]->key;\n            index++;\n        }\n    }\n    set->set = keys;\n    set->len = total;\n}\n\n/* Получить все значения */\nvoid valueSet(ArrayHashMap *hmap, MapSet *set) {\n    char **vals;\n    int i = 0, index = 0;\n    int total = 0;\n    /* Подсчитать число действительных пар ключ-значение */\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            total++;\n        }\n    }\n    vals = malloc(total * sizeof(char *));\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            vals[index] = hmap->buckets[i]->val;\n            index++;\n        }\n    }\n    set->set = vals;\n    set->len = total;\n}\n\n/* Вывести хеш-таблицу */\nvoid print(ArrayHashMap *hmap) {\n    int i;\n    MapSet set;\n    pairSet(hmap, &set);\n    Pair *entries = (Pair *)set.set;\n    for (i = 0; i < set.len; i++) {\n        printf(\"%d -> %s\\n\", entries[i].key, entries[i].val);\n    }\n    free(set.set);\n}\n
        array_hash_map.kt
        /* Пара ключ-значение */\nclass Pair(\n    var key: Int,\n    var _val: String\n)\n\n/* Хеш-таблица на основе массива */\nclass ArrayHashMap {\n    // Инициализировать массив, содержащий 100 корзин\n    private val buckets = arrayOfNulls<Pair>(100)\n\n    /* Хеш-функция */\n    fun hashFunc(key: Int): Int {\n        val index = key % 100\n        return index\n    }\n\n    /* Операция поиска */\n    fun get(key: Int): String? {\n        val index = hashFunc(key)\n        val pair = buckets[index] ?: return null\n        return pair._val\n    }\n\n    /* Операция добавления */\n    fun put(key: Int, _val: String) {\n        val pair = Pair(key, _val)\n        val index = hashFunc(key)\n        buckets[index] = pair\n    }\n\n    /* Операция удаления */\n    fun remove(key: Int) {\n        val index = hashFunc(key)\n        // Присвоить null, что означает удаление\n        buckets[index] = null\n    }\n\n    /* Получить все пары ключ-значение */\n    fun pairSet(): MutableList<Pair> {\n        val pairSet = mutableListOf<Pair>()\n        for (pair in buckets) {\n            if (pair != null)\n                pairSet.add(pair)\n        }\n        return pairSet\n    }\n\n    /* Получить все ключи */\n    fun keySet(): MutableList<Int> {\n        val keySet = mutableListOf<Int>()\n        for (pair in buckets) {\n            if (pair != null)\n                keySet.add(pair.key)\n        }\n        return keySet\n    }\n\n    /* Получить все значения */\n    fun valueSet(): MutableList<String> {\n        val valueSet = mutableListOf<String>()\n        for (pair in buckets) {\n            if (pair != null)\n                valueSet.add(pair._val)\n        }\n        return valueSet\n    }\n\n    /* Вывести хеш-таблицу */\n    fun print() {\n        for (kv in pairSet()) {\n            val key = kv.key\n            val _val = kv._val\n            println(\"$key -> $_val\")\n        }\n    }\n}\n
        array_hash_map.rb
        ### Пара ключ-значение ###\nclass Pair\n  attr_accessor :key, :val\n\n  def initialize(key, val)\n    @key = key\n    @val = val\n  end\nend\n\n### Хеш-таблица на основе массива ###\nclass ArrayHashMap\n  ### Конструктор ###\n  def initialize\n    # Инициализировать массив, содержащий 100 корзин\n    @buckets = Array.new(100)\n  end\n\n  ### Хеш-функция ###\n  def hash_func(key)\n    index = key % 100\n  end\n\n  ### Операция поиска ###\n  def get(key)\n    index = hash_func(key)\n    pair = @buckets[index]\n\n    return if pair.nil?\n    pair.val\n  end\n\n  ### Операция добавления ###\n  def put(key, val)\n    pair = Pair.new(key, val)\n    index = hash_func(key)\n    @buckets[index] = pair\n  end\n\n  ### Операция удаления ###\n  def remove(key)\n    index = hash_func(key)\n    # Присвоить nil, что означает удаление\n    @buckets[index] = nil\n  end\n\n  ### Получить все пары ключ-значение ###\n  def entry_set\n    result = []\n    @buckets.each { |pair| result << pair unless pair.nil? }\n    result\n  end\n\n  ### Получить все ключи ###\n  def key_set\n    result = []\n    @buckets.each { |pair| result << pair.key unless pair.nil? }\n    result\n  end\n\n  ### Получить все значения ###\n  def value_set\n    result = []\n    @buckets.each { |pair| result << pair.val unless pair.nil? }\n    result\n  end\n\n  ### Вывести хеш-таблицу ###\n  def print\n    @buckets.each { |pair| puts \"#{pair.key} -> #{pair.val}\" unless pair.nil? }\n  end\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 6. Хеш-таблицы","6.1   Хеш-таблица"],"tags":[]},{"location":"chapter_hashing/hash_map/#613-","level":2,"title":"6.1.3   Хеш-коллизии и расширение","text":"

        По сути, хеш-функция отображает входное пространство, состоящее из всех key , в выходное пространство, состоящее из всех индексов массива, а входное пространство обычно значительно больше выходного. Поэтому теоретически неизбежно существование ситуации \"несколько входов соответствуют одному выходу\".

        Для хеш-функции из приведенного выше примера, если последние две цифры key совпадают, то совпадает и результат хеш-функции. Например, если искать студентов с номерами 12836 и 20336, то получим:

        12836 % 100 = 36\n20336 % 100 = 36\n

        Как показано на рисунке 6-3, два номера указывают на одно и то же имя, что, очевидно, неверно. Такую ситуацию, когда нескольким входам соответствует один и тот же выход, называют хеш-коллизией (hash collision).

        Рисунок 6-3   Пример хеш-коллизии

        Легко понять, что чем больше емкость хеш-таблицы \\(n\\) , тем ниже вероятность того, что несколько key попадут в один и тот же бакет, а значит, тем меньше коллизий. Поэтому мы можем уменьшать число хеш-коллизий путем расширения хеш-таблицы.

        Как показано на рисунке 6-4, до расширения пары ключ-значение (136, A) и (236, D) конфликтовали, а после расширения коллизия исчезла.

        Рисунок 6-4   Расширение хеш-таблицы

        Подобно расширению массива, расширение хеш-таблицы требует перенести все пары ключ-значение из старой таблицы в новую, а это очень затратно по времени; кроме того, поскольку емкость хеш-таблицы capacity изменилась, нам приходится с помощью хеш-функции заново вычислять позиции хранения всех пар ключ-значение, что дополнительно увеличивает вычислительные расходы процесса расширения. Поэтому языки программирования обычно заранее резервируют достаточно большую емкость хеш-таблицы, чтобы избежать частых расширений.

        Коэффициент загрузки (load factor) - важное понятие хеш-таблицы. Он определяется как отношение числа элементов в хеш-таблице к числу бакетов и используется для оценки степени серьезности хеш-коллизий, а также часто служит условием срабатывания расширения хеш-таблицы. Например, в Java, когда коэффициент загрузки превышает \\(0.75\\) , система расширяет хеш-таблицу до \\(2\\) раз от исходной емкости.

        ","path":["Глава 6. Хеш-таблицы","6.1   Хеш-таблица"],"tags":[]},{"location":"chapter_hashing/summary/","level":1,"title":"6.4   Резюме","text":"","path":["Глава 6. Хеш-таблицы","6.4   Резюме"],"tags":[]},{"location":"chapter_hashing/summary/#1","level":3,"title":"1.   Ключевые выводы","text":"
        • Передав key , мы можем получить value из хеш-таблицы за \\(O(1)\\) времени, поэтому она очень эффективна.
        • К типичным операциям хеш-таблицы относятся поиск, добавление пары ключ-значение, удаление пары ключ-значение и обход хеш-таблицы.
        • Хеш-функция отображает key в индекс массива, после чего можно обратиться к соответствующему бакету и получить value .
        • Два разных key после хеш-функции могут дать один и тот же индекс массива, что приводит к ошибочному результату поиска; это явление называется хеш-коллизией.
        • Чем больше емкость хеш-таблицы, тем ниже вероятность хеш-коллизий. Поэтому хеш-коллизии можно смягчать путем расширения хеш-таблицы. Как и у массива, операция расширения у хеш-таблицы очень затратна.
        • Коэффициент загрузки определяется как отношение числа элементов в хеш-таблице к числу бакетов, отражает степень серьезности хеш-коллизий и часто используется как условие запуска расширения хеш-таблицы.
        • Метод цепочек превращает одиночный элемент в связный список и хранит все конфликтующие элементы в одном списке. Однако слишком длинный список снижает эффективность поиска, поэтому его можно дополнительно преобразовать в красно-черное дерево.
        • Открытая адресация обрабатывает хеш-коллизии за счет многократного пробирования. Линейное пробирование использует фиксированный шаг, его недостатки - невозможность прямого удаления элементов и склонность к кластеризации. Повторное хеширование использует несколько хеш-функций и по сравнению с линейным пробированием меньше подвержено кластеризации, но требует больше вычислений.
        • Разные языки программирования выбирают разные стратегии реализации хеш-таблиц. Например, HashMap в Java использует метод цепочек, а Dict в Python - открытую адресацию.
        • Для хеш-таблицы желательно, чтобы хеш-алгоритм был детерминированным, быстрым и обеспечивал равномерное распределение. В криптографии от него дополнительно требуют устойчивости к коллизиям и эффекта лавины.
        • В качестве модуля хеш-алгоритмы обычно используют большое простое число, чтобы максимально обеспечить равномерность распределения хеш-значений и снизить число хеш-коллизий.
        • К распространенным хеш-алгоритмам относятся MD5, SHA-1, SHA-2 и SHA-3. MD5 часто применяли для проверки целостности файлов, а SHA-2 широко используется в протоколах и приложениях, связанных с безопасностью.
        • Языки программирования обычно предоставляют для типов данных встроенные хеш-алгоритмы, чтобы вычислять индексы бакетов в хеш-таблице. Как правило, хешируемыми могут быть только неизменяемые объекты.
        ","path":["Глава 6. Хеш-таблицы","6.4   Резюме"],"tags":[]},{"location":"chapter_hashing/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

        Q: В каких случаях временная сложность хеш-таблицы становится \\(O(n)\\) ?

        Когда хеш-коллизии становятся достаточно серьезными, временная сложность хеш-таблицы деградирует до \\(O(n)\\) . Если хеш-функция спроектирована хорошо, емкость выбрана разумно, а конфликты распределены достаточно равномерно, то временная сложность обычно считается \\(O(1)\\) . При использовании встроенной хеш-таблицы языка программирования мы, как правило, и принимаем ее за \\(O(1)\\) .

        Q: Почему бы не использовать хеш-функцию \\(f(x) = x\\) ? Тогда ведь коллизий не будет.

        При хеш-функции \\(f(x) = x\\) каждому элементу соответствует уникальный индекс бакета, и такая структура становится эквивалентна массиву. Однако входное пространство обычно намного больше выходного пространства (длины массива), поэтому последним шагом хеш-функции обычно выступает взятие по модулю длины массива. Иначе говоря, цель хеш-таблицы состоит в том, чтобы отобразить большее пространство состояний в меньшее пространство и при этом обеспечить \\(O(1)\\) поиска.

        Q: В основе хеш-таблицы лежат массив, связный список и двоичное дерево. Почему же она может быть быстрее них?

        Во-первых, у хеш-таблицы повышается временная эффективность, но снижается пространственная эффективность. Значительная часть ее памяти остается неиспользованной.

        Во-вторых, она быстрее только в определенных сценариях. Если одну и ту же задачу можно реализовать на массиве или связном списке с той же асимптотикой, то часто такая реализация окажется быстрее, чем хеш-таблица. Причина в том, что вычисление хеш-функции само по себе стоит времени, то есть константа в сложности получается выше.

        Наконец, временная сложность хеш-таблицы тоже может деградировать. Например, при методе цепочек мы все равно выполняем поиск в связном списке или красно-черном дереве, поэтому риск деградации до \\(O(n)\\) сохраняется.

        Q: Есть ли у повторного хеширования недостаток \"нельзя напрямую удалять элементы\"? Можно ли повторно использовать место, помеченное как удаленное?

        Повторное хеширование - это разновидность открытой адресации, а у всех методов открытой адресации есть недостаток: элементы нельзя удалять напрямую, поэтому приходится использовать метку удаления. Пространство, помеченное как удаленное, можно использовать повторно. Когда новый элемент вставляется в хеш-таблицу и в процессе пробирования попадает на такую отмеченную позицию, эта позиция может быть занята новым элементом. Такой подход сохраняет последовательность пробирования и одновременно поддерживает приемлемую эффективность использования памяти.

        Q: Почему при линейном пробировании во время поиска элемента вообще возникает хеш-коллизия?

        Во время поиска мы через хеш-функцию находим соответствующий бакет и соответствующую пару ключ-значение, но видим, что key не совпадает, а это и означает наличие хеш-коллизии. Поэтому метод линейного пробирования в соответствии с заранее заданным шагом последовательно движется дальше, пока не найдет правильную пару ключ-значение или не убедится, что поиск завершился неудачей.

        Q: Почему расширение хеш-таблицы помогает смягчать хеш-коллизии?

        Последний шаг хеш-функции обычно состоит во взятии по модулю длины массива \\(n\\) , чтобы результат попадал в диапазон индексов массива; после расширения длина массива \\(n\\) меняется, а значит, может измениться и индекс, соответствующий данному key . Несколько key , которые раньше попадали в один бакет, после расширения могут распределиться по нескольким бакетам, и тем самым хеш-коллизии будут ослаблены.

        Q: Если нам нужен быстрый доступ, почему бы просто не использовать массив?

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

        ","path":["Глава 6. Хеш-таблицы","6.4   Резюме"],"tags":[]},{"location":"chapter_heap/","level":1,"title":"Глава 8.   Куча","text":"

        Abstract

        Куча - это полное двоичное дерево, удовлетворяющее определенным условиям.

        В максимальной и минимальной куче элемент на вершине всегда обладает самым выраженным приоритетом.

        ","path":["Глава 8. Куча","Глава 8.   Куча"],"tags":[]},{"location":"chapter_heap/#_1","level":2,"title":"Содержание главы","text":"
        • 8.1   Куча
        • 8.2   Построение кучи
        • 8.3   Задача Top-k
        • 8.4   Резюме
        ","path":["Глава 8. Куча","Глава 8.   Куча"],"tags":[]},{"location":"chapter_heap/build_heap/","level":1,"title":"8.2   Построение кучи","text":"

        В некоторых случаях требуется построить кучу, используя сразу все элементы списка. Этот процесс называется построением кучи.

        ","path":["Глава 8. Куча","8.2   Построение кучи"],"tags":[]},{"location":"chapter_heap/build_heap/#821","level":2,"title":"8.2.1   Реализация через операцию добавления в кучу","text":"

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

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

        Пусть число элементов равно \\(n\\) ; так как каждая операция добавления требует \\(O(\\log{n})\\) времени, временная сложность такого построения кучи составляет \\(O(n \\log n)\\) .

        ","path":["Глава 8. Куча","8.2   Построение кучи"],"tags":[]},{"location":"chapter_heap/build_heap/#822","level":2,"title":"8.2.2   Реализация через обход и упорядочивание","text":"

        На самом деле можно реализовать и более эффективный способ построения кучи, который состоит из двух шагов.

        1. Без изменений добавить все элементы списка в кучу; в этот момент свойства кучи еще не выполняются.
        2. Обойти кучу в обратном порядке, то есть в порядке, обратном обходу по уровням, и по очереди выполнить упорядочивание сверху вниз для каждого нелистового узла.

        После того как некоторый узел был упорядочен, поддерево с этим узлом в качестве корня становится корректной подкучей. А поскольку обход выполняется в обратном порядке, куча строится снизу вверх.

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

        Стоит отметить, что листовые узлы не имеют дочерних узлов, поэтому они естественным образом являются корректными подкучами и не требуют упорядочивания. Как показано в коде ниже, последний нелистовой узел является родителем последнего узла, и именно с него мы начинаем обратный обход и упорядочивание:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
        def __init__(self, nums: list[int]):\n    \"\"\"Конструктор, строящий кучу по входному списку\"\"\"\n    # Добавить элементы списка в кучу без изменений\n    self.max_heap = nums\n    # Выполнить heapify для всех узлов, кроме листовых\n    for i in range(self.parent(self.size() - 1), -1, -1):\n        self.sift_down(i)\n
        my_heap.cpp
        /* Конструктор, строящий кучу по входному списку */\nMaxHeap(vector<int> nums) {\n    // Добавить элементы списка в кучу без изменений\n    maxHeap = nums;\n    // Выполнить heapify для всех узлов, кроме листовых\n    for (int i = parent(size() - 1); i >= 0; i--) {\n        siftDown(i);\n    }\n}\n
        my_heap.java
        /* Конструктор, строящий кучу по входному списку */\nMaxHeap(List<Integer> nums) {\n    // Добавить элементы списка в кучу без изменений\n    maxHeap = new ArrayList<>(nums);\n    // Выполнить heapify для всех узлов, кроме листовых\n    for (int i = parent(size() - 1); i >= 0; i--) {\n        siftDown(i);\n    }\n}\n
        my_heap.cs
        /* Конструктор: построить кучу по входному списку */\nMaxHeap(IEnumerable<int> nums) {\n    // Добавить элементы списка в кучу без изменений\n    maxHeap = new List<int>(nums);\n    // Выполнить heapify для всех узлов, кроме листовых\n    var size = Parent(this.Size() - 1);\n    for (int i = size; i >= 0; i--) {\n        SiftDown(i);\n    }\n}\n
        my_heap.go
        /* Конструктор, строящий кучу по срезу */\nfunc newMaxHeap(nums []any) *maxHeap {\n    // Добавить элементы списка в кучу без изменений\n    h := &maxHeap{data: nums}\n    for i := h.parent(len(h.data) - 1); i >= 0; i-- {\n        // Выполнить heapify для всех узлов, кроме листовых\n        h.siftDown(i)\n    }\n    return h\n}\n
        my_heap.swift
        /* Конструктор, строящий кучу по входному списку */\ninit(nums: [Int]) {\n    // Добавить элементы списка в кучу без изменений\n    maxHeap = nums\n    // Выполнить heapify для всех узлов, кроме листовых\n    for i in (0 ... parent(i: size() - 1)).reversed() {\n        siftDown(i: i)\n    }\n}\n
        my_heap.js
        /* Конструктор, создающий пустую кучу или строящий кучу по входному списку */\nconstructor(nums) {\n    // Добавить элементы списка в кучу без изменений\n    this.#maxHeap = nums === undefined ? [] : [...nums];\n    // Выполнить heapify для всех узлов, кроме листовых\n    for (let i = this.#parent(this.size() - 1); i >= 0; i--) {\n        this.#siftDown(i);\n    }\n}\n
        my_heap.ts
        /* Конструктор, создающий пустую кучу или строящий кучу по входному списку */\nconstructor(nums?: number[]) {\n    // Добавить элементы списка в кучу без изменений\n    this.maxHeap = nums === undefined ? [] : [...nums];\n    // Выполнить heapify для всех узлов, кроме листовых\n    for (let i = this.parent(this.size() - 1); i >= 0; i--) {\n        this.siftDown(i);\n    }\n}\n
        my_heap.dart
        /* Конструктор, строящий кучу по входному списку */\nMaxHeap(List<int> nums) {\n  // Добавить элементы списка в кучу без изменений\n  _maxHeap = nums;\n  // Выполнить heapify для всех узлов, кроме листовых\n  for (int i = _parent(size() - 1); i >= 0; i--) {\n    siftDown(i);\n  }\n}\n
        my_heap.rs
        /* Конструктор, строящий кучу по входному списку */\nfn new(nums: Vec<i32>) -> Self {\n    // Добавить элементы списка в кучу без изменений\n    let mut heap = MaxHeap { max_heap: nums };\n    // Выполнить heapify для всех узлов, кроме листовых\n    for i in (0..=Self::parent(heap.size() - 1)).rev() {\n        heap.sift_down(i);\n    }\n    heap\n}\n
        my_heap.c
        /* Конструктор, строящий кучу по срезу */\nMaxHeap *newMaxHeap(int nums[], int size) {\n    // Поместить все элементы в кучу\n    MaxHeap *maxHeap = (MaxHeap *)malloc(sizeof(MaxHeap));\n    maxHeap->size = size;\n    memcpy(maxHeap->data, nums, size * sizeof(int));\n    for (int i = parent(maxHeap, size - 1); i >= 0; i--) {\n        // Выполнить heapify для всех узлов, кроме листовых\n        siftDown(maxHeap, i);\n    }\n    return maxHeap;\n}\n
        my_heap.kt
        /* Максимальная куча */\nclass MaxHeap(nums: MutableList<Int>?) {\n    // Использовать список вместо массива, чтобы не учитывать проблему расширения\n    private val maxHeap = mutableListOf<Int>()\n\n    /* Конструктор, строящий кучу по входному списку */\n    init {\n        // Добавить элементы списка в кучу без изменений\n        maxHeap.addAll(nums!!)\n        // Выполнить heapify для всех узлов, кроме листовых\n        for (i in parent(size() - 1) downTo 0) {\n            siftDown(i)\n        }\n    }\n\n    /* Получить индекс левого дочернего узла */\n    private fun left(i: Int): Int {\n        return 2 * i + 1\n    }\n\n    /* Получить индекс правого дочернего узла */\n    private fun right(i: Int): Int {\n        return 2 * i + 2\n    }\n\n    /* Получить индекс родительского узла */\n    private fun parent(i: Int): Int {\n        return (i - 1) / 2 // Округление вниз при делении\n    }\n\n    /* Поменять элементы местами */\n    private fun swap(i: Int, j: Int) {\n        val temp = maxHeap[i]\n        maxHeap[i] = maxHeap[j]\n        maxHeap[j] = temp\n    }\n\n    /* Получение размера кучи */\n    fun size(): Int {\n        return maxHeap.size\n    }\n\n    /* Проверка, пуста ли куча */\n    fun isEmpty(): Boolean {\n        /* Проверка, пуста ли куча */\n        return size() == 0\n    }\n\n    /* Доступ к элементу на вершине кучи */\n    fun peek(): Int {\n        return maxHeap[0]\n    }\n\n    /* Добавление элемента в кучу */\n    fun push(_val: Int) {\n        // Добавление узла\n        maxHeap.add(_val)\n        // Просеивание снизу вверх\n        siftUp(size() - 1)\n    }\n\n    /* Начиная с узла i, выполнить просеивание снизу вверх */\n    private fun siftUp(it: Int) {\n        // Параметры функций в Kotlin неизменяемы, поэтому создается временная переменная\n        var i = it\n        while (true) {\n            // Получение родительского узла для узла i\n            val p = parent(i)\n            // Завершить heapify, когда «корневой узел уже пройден» или «узел не требует исправления»\n            if (p < 0 || maxHeap[i] <= maxHeap[p]) break\n            // Поменять два узла местами\n            swap(i, p)\n            // Циклическое просеивание вверх\n            i = p\n        }\n    }\n\n    /* Извлечение элемента из кучи */\n    fun pop(): Int {\n        // Обработка пустого случая\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n        swap(0, size() - 1)\n        // Удаление узла\n        val _val = maxHeap.removeAt(size() - 1)\n        // Просеивание сверху вниз\n        siftDown(0)\n        // Вернуть элемент с вершины кучи\n        return _val\n    }\n\n    /* Начиная с узла i, выполнить просеивание сверху вниз */\n    private fun siftDown(it: Int) {\n        // Параметры функций в Kotlin неизменяемы, поэтому создается временная переменная\n        var i = it\n        while (true) {\n            // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n            val l = left(i)\n            val r = right(i)\n            var ma = i\n            if (l < size() && maxHeap[l] > maxHeap[ma]) ma = l\n            if (r < size() && maxHeap[r] > maxHeap[ma]) ma = r\n            // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n            if (ma == i) break\n            // Поменять два узла местами\n            swap(i, ma)\n            // Циклическое просеивание вниз\n            i = ma\n        }\n    }\n\n    /* Вывести кучу (двоичное дерево) */\n    fun print() {\n        val queue = PriorityQueue { a: Int, b: Int -> b - a }\n        queue.addAll(maxHeap)\n        printHeap(queue)\n    }\n}\n
        my_heap.rb
        ### Конструктор, строящий кучу по входному списку ###\ndef initialize(nums)\n  # Добавить элементы списка в кучу без изменений\n  @max_heap = nums\n  # Выполнить heapify для всех узлов, кроме листовых\n  parent(size - 1).downto(0) do |i|\n    sift_down(i)\n  end\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 8. Куча","8.2   Построение кучи"],"tags":[]},{"location":"chapter_heap/build_heap/#823","level":2,"title":"8.2.3   Анализ сложности","text":"

        Теперь попробуем оценить временную сложность второго способа построения кучи.

        • Пусть число узлов полного двоичного дерева равно \\(n\\) , тогда число листовых узлов равно \\((n + 1) / 2\\) , где \\(/\\) означает целочисленное деление вниз. Следовательно, число узлов, которые нужно упорядочивать, равно \\((n - 1) / 2\\) .
        • В процессе упорядочивания сверху вниз каждый узел в худшем случае может просеяться до листа, поэтому максимальное число итераций равно высоте двоичного дерева \\(\\log n\\) .

        Перемножив эти два значения, можно получить временную сложность построения кучи \\(O(n \\log n)\\) . Но эта оценка неточна, потому что мы не учли свойство двоичного дерева: на нижних уровнях узлов гораздо больше, чем на верхних.

        Далее выполним более точный расчет. Чтобы упростить вычисления, предположим, что дано \"идеальное двоичное дерево\" высоты \\(h\\) с числом узлов \\(n\\) ; это предположение не повлияет на корректность результата.

        Рисунок 8-5   Число узлов на каждом уровне идеального двоичного дерева

        Как показано на рисунке 8-5, максимальное число итераций упорядочивания сверху вниз для некоторого узла равно расстоянию от этого узла до листового узла, а это расстояние как раз и есть высота узла. Поэтому мы можем просуммировать для каждого уровня выражение \"число узлов \\(\\times\\) высота узла\" и получить суммарное число итераций упорядочивания для всех узлов.

        \\[ T(h) = 2^0h + 2^1(h-1) + 2^2(h-2) + \\dots + 2^{(h-1)}\\times1 \\]

        Чтобы упростить это выражение, воспользуемся школьными знаниями о последовательностях и сначала умножим \\(T(h)\\) на \\(2\\) :

        \\[ \\begin{aligned} T(h) & = 2^0h + 2^1(h-1) + 2^2(h-2) + \\dots + 2^{h-1}\\times1 \\newline 2 T(h) & = 2^1h + 2^2(h-1) + 2^3(h-2) + \\dots + 2^{h}\\times1 \\newline \\end{aligned} \\]

        Используя метод вычитания со сдвигом, вычтем из нижней строки \\(2 T(h)\\) верхнюю строку \\(T(h)\\) , тогда получим:

        \\[ 2T(h) - T(h) = T(h) = -2^0h + 2^1 + 2^2 + \\dots + 2^{h-1} + 2^h \\]

        Из этого выражения видно, что \\(T(h)\\) представляет собой геометрическую прогрессию, поэтому можно напрямую применить формулу суммы и получить временную сложность:

        \\[ \\begin{aligned} T(h) & = 2 \\frac{1 - 2^h}{1 - 2} - h \\newline & = 2^{h+1} - h - 2 \\newline & = O(2^h) \\end{aligned} \\]

        Далее, число узлов идеального двоичного дерева высоты \\(h\\) равно \\(n = 2^{h+1} - 1\\) , поэтому несложно получить сложность \\(O(2^h) = O(n)\\) . Из этого вывода следует, что построение кучи из входного списка имеет временную сложность \\(O(n)\\) , то есть выполняется очень эффективно.

        ","path":["Глава 8. Куча","8.2   Построение кучи"],"tags":[]},{"location":"chapter_heap/heap/","level":1,"title":"8.1   Куча","text":"

        Куча (heap) - это полное двоичное дерево, удовлетворяющее определенным условиям. Основных типов кучи два, как показано на рисунке 8-1.

        • Минимальная куча (min heap): значение любого узла \\(\\leq\\) значения его дочерних узлов.
        • Максимальная куча (max heap): значение любого узла \\(\\geq\\) значения его дочерних узлов.

        Рисунок 8-1   Минимальная куча и максимальная куча

        Куча, являясь частным случаем полного двоичного дерева, обладает следующими свойствами.

        • Узлы самого нижнего уровня заполняются слева, а все остальные уровни заполнены полностью.
        • Корневой узел двоичного дерева мы называем вершиной кучи, а самый правый узел нижнего уровня - основанием кучи.
        • Для максимальной (минимальной) кучи значение элемента на вершине, то есть у корневого узла, является максимальным (минимальным).
        ","path":["Глава 8. Куча","8.1   Куча"],"tags":[]},{"location":"chapter_heap/heap/#811","level":2,"title":"8.1.1   Распространенные операции с кучей","text":"

        Нужно отметить, что многие языки программирования предоставляют не саму кучу, а очередь с приоритетом (priority queue) - абстрактную структуру данных, определяемую как очередь, в которой элементы извлекаются в соответствии с приоритетом.

        На практике куча обычно используется для реализации очереди с приоритетом, а максимальная куча эквивалентна очереди с приоритетом, в которой элементы извлекаются по убыванию. С точки зрения использования очередь с приоритетом и куча могут считаться эквивалентными структурами данных. Поэтому в этой книге мы не будем специально различать их и в дальнейшем будем единообразно называть кучей.

        Распространенные операции с кучей приведены в таблице 8-1. Конкретные имена методов зависят от языка программирования.

        Таблица 8-1   Эффективность операций с кучей

        Имя метода Описание Временная сложность push() Поместить элемент в кучу \\(O(\\log n)\\) pop() Извлечь элемент с вершины кучи \\(O(\\log n)\\) peek() Получить доступ к вершине кучи (для max / min кучи это соответственно максимум / минимум) \\(O(1)\\) size() Получить число элементов в куче \\(O(1)\\) isEmpty() Проверить, пуста ли куча \\(O(1)\\)

        В реальных приложениях мы можем напрямую использовать классы кучи, предоставляемые языком программирования, или классы очереди с приоритетом.

        Подобно сортировкам \"по возрастанию\" и \"по убыванию\", мы можем переключаться между \"минимальной кучей\" и \"максимальной кучей\", изменяя flag или модифицируя Comparator . Код приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby heap.py
        # Инициализация минимальной кучи\nmin_heap, flag = [], 1\n# Инициализация максимальной кучи\nmax_heap, flag = [], -1\n\n# Модуль heapq в Python по умолчанию реализует минимальную кучу\n# Если инвертировать знак элемента перед добавлением, то отношение порядка перевернется и так реализуется максимальная куча\n# В этом примере flag = 1 соответствует минимальной куче, а flag = -1 - максимальной\n\n# Добавление элементов в кучу\nheapq.heappush(max_heap, flag * 1)\nheapq.heappush(max_heap, flag * 3)\nheapq.heappush(max_heap, flag * 2)\nheapq.heappush(max_heap, flag * 5)\nheapq.heappush(max_heap, flag * 4)\n\n# Получение элемента на вершине кучи\npeek: int = flag * max_heap[0] # 5\n\n# Извлечение элемента с вершины кучи\n# Извлеченные элементы образуют последовательность по убыванию\nval = flag * heapq.heappop(max_heap) # 5\nval = flag * heapq.heappop(max_heap) # 4\nval = flag * heapq.heappop(max_heap) # 3\nval = flag * heapq.heappop(max_heap) # 2\nval = flag * heapq.heappop(max_heap) # 1\n\n# Получение размера кучи\nsize: int = len(max_heap)\n\n# Проверка, пуста ли куча\nis_empty: bool = not max_heap\n\n# Построение кучи из входного списка\nmin_heap: list[int] = [1, 3, 2, 5, 4]\nheapq.heapify(min_heap)\n
        heap.cpp
        /* Инициализация кучи */\n// Инициализация минимальной кучи\npriority_queue<int, vector<int>, greater<int>> minHeap;\n// Инициализация максимальной кучи\npriority_queue<int, vector<int>, less<int>> maxHeap;\n\n/* Добавление элементов в кучу */\nmaxHeap.push(1);\nmaxHeap.push(3);\nmaxHeap.push(2);\nmaxHeap.push(5);\nmaxHeap.push(4);\n\n/* Получение элемента на вершине кучи */\nint peek = maxHeap.top(); // 5\n\n/* Извлечение элемента с вершины кучи */\n// Извлеченные элементы образуют последовательность по убыванию\nmaxHeap.pop(); // 5\nmaxHeap.pop(); // 4\nmaxHeap.pop(); // 3\nmaxHeap.pop(); // 2\nmaxHeap.pop(); // 1\n\n/* Получение размера кучи */\nint size = maxHeap.size();\n\n/* Проверка, пуста ли куча */\nbool isEmpty = maxHeap.empty();\n\n/* Построение кучи из входного списка */\nvector<int> input{1, 3, 2, 5, 4};\npriority_queue<int, vector<int>, greater<int>> minHeap(input.begin(), input.end());\n
        heap.java
        /* Инициализация кучи */\n// Инициализация минимальной кучи\nQueue<Integer> minHeap = new PriorityQueue<>();\n// Инициализация максимальной кучи (достаточно изменить Comparator через lambda)\nQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a);\n\n/* Добавление элементов в кучу */\nmaxHeap.offer(1);\nmaxHeap.offer(3);\nmaxHeap.offer(2);\nmaxHeap.offer(5);\nmaxHeap.offer(4);\n\n/* Получение элемента на вершине кучи */\nint peek = maxHeap.peek(); // 5\n\n/* Извлечение элемента с вершины кучи */\n// Извлеченные элементы образуют последовательность по убыванию\npeek = maxHeap.poll(); // 5\npeek = maxHeap.poll(); // 4\npeek = maxHeap.poll(); // 3\npeek = maxHeap.poll(); // 2\npeek = maxHeap.poll(); // 1\n\n/* Получение размера кучи */\nint size = maxHeap.size();\n\n/* Проверка, пуста ли куча */\nboolean isEmpty = maxHeap.isEmpty();\n\n/* Построение кучи из входного списка */\nminHeap = new PriorityQueue<>(Arrays.asList(1, 3, 2, 5, 4));\n
        heap.cs
        /* Инициализация кучи */\n// Инициализация минимальной кучи\nPriorityQueue<int, int> minHeap = new();\n// Инициализация максимальной кучи (достаточно изменить Comparer через lambda)\nPriorityQueue<int, int> maxHeap = new(Comparer<int>.Create((x, y) => y.CompareTo(x)));\n\n/* Добавление элементов в кучу */\nmaxHeap.Enqueue(1, 1);\nmaxHeap.Enqueue(3, 3);\nmaxHeap.Enqueue(2, 2);\nmaxHeap.Enqueue(5, 5);\nmaxHeap.Enqueue(4, 4);\n\n/* Получение элемента на вершине кучи */\nint peek = maxHeap.Peek();//5\n\n/* Извлечение элемента с вершины кучи */\n// Извлеченные элементы образуют последовательность по убыванию\npeek = maxHeap.Dequeue();  // 5\npeek = maxHeap.Dequeue();  // 4\npeek = maxHeap.Dequeue();  // 3\npeek = maxHeap.Dequeue();  // 2\npeek = maxHeap.Dequeue();  // 1\n\n/* Получение размера кучи */\nint size = maxHeap.Count;\n\n/* Проверка, пуста ли куча */\nbool isEmpty = maxHeap.Count == 0;\n\n/* Построение кучи из входного списка */\nminHeap = new PriorityQueue<int, int>([(1, 1), (3, 3), (2, 2), (5, 5), (4, 4)]);\n
        heap.go
        // В Go можно построить целочисленную максимальную кучу, реализовав heap.Interface\n// Для реализации heap.Interface также нужно реализовать sort.Interface\ntype intHeap []any\n\n// Метод Push из heap.Interface, реализует добавление элемента в кучу\nfunc (h *intHeap) Push(x any) {\n    // Push и Pop используют pointer receiver\n    // Потому что они не только изменяют содержимое среза, но и его длину\n    *h = append(*h, x.(int))\n}\n\n// Метод Pop из heap.Interface, реализует извлечение элемента с вершины кучи\nfunc (h *intHeap) Pop() any {\n    // Извлекаемый элемент хранится в конце\n    last := (*h)[len(*h)-1]\n    *h = (*h)[:len(*h)-1]\n    return last\n}\n\n// Метод Len из sort.Interface\nfunc (h *intHeap) Len() int {\n    return len(*h)\n}\n\n// Метод Less из sort.Interface\nfunc (h *intHeap) Less(i, j int) bool {\n    // Для минимальной кучи здесь нужно заменить сравнение на <\n    return (*h)[i].(int) > (*h)[j].(int)\n}\n\n// Метод Swap из sort.Interface\nfunc (h *intHeap) Swap(i, j int) {\n    (*h)[i], (*h)[j] = (*h)[j], (*h)[i]\n}\n\n// Top получает элемент на вершине кучи\nfunc (h *intHeap) Top() any {\n    return (*h)[0]\n}\n\n/* Driver Code */\nfunc TestHeap(t *testing.T) {\n    /* Инициализация кучи */\n    // Инициализация максимальной кучи\n    maxHeap := &intHeap{}\n    heap.Init(maxHeap)\n    /* Добавление элементов в кучу */\n    // Вызываем методы heap.Interface для добавления элементов\n    heap.Push(maxHeap, 1)\n    heap.Push(maxHeap, 3)\n    heap.Push(maxHeap, 2)\n    heap.Push(maxHeap, 4)\n    heap.Push(maxHeap, 5)\n\n    /* Получение элемента на вершине кучи */\n    top := maxHeap.Top()\n    fmt.Printf(\"Элемент на вершине кучи: %d\\n\", top)\n\n    /* Извлечение элемента с вершины кучи */\n    // Вызываем методы heap.Interface для удаления элементов\n    heap.Pop(maxHeap) // 5\n    heap.Pop(maxHeap) // 4\n    heap.Pop(maxHeap) // 3\n    heap.Pop(maxHeap) // 2\n    heap.Pop(maxHeap) // 1\n\n    /* Получение размера кучи */\n    size := len(*maxHeap)\n    fmt.Printf(\"Число элементов в куче: %d\\n\", size)\n\n    /* Проверка, пуста ли куча */\n    isEmpty := len(*maxHeap) == 0\n    fmt.Printf(\"Пуста ли куча: %t\\n\", isEmpty)\n}\n
        heap.swift
        /* Инициализация кучи */\n// Тип Heap в Swift поддерживает и max-heap, и min-heap, но требует swift-collections\nvar heap = Heap<Int>()\n\n/* Добавление элементов в кучу */\nheap.insert(1)\nheap.insert(3)\nheap.insert(2)\nheap.insert(5)\nheap.insert(4)\n\n/* Получение элемента на вершине кучи */\nvar peek = heap.max()!\n\n/* Извлечение элемента с вершины кучи */\npeek = heap.removeMax() // 5\npeek = heap.removeMax() // 4\npeek = heap.removeMax() // 3\npeek = heap.removeMax() // 2\npeek = heap.removeMax() // 1\n\n/* Получение размера кучи */\nlet size = heap.count\n\n/* Проверка, пуста ли куча */\nlet isEmpty = heap.isEmpty\n\n/* Построение кучи из входного списка */\nlet heap2 = Heap([1, 3, 2, 5, 4])\n
        heap.js
        // В JavaScript нет встроенного класса Heap\n
        heap.ts
        // В TypeScript нет встроенного класса Heap\n
        heap.dart
        // В Dart нет встроенного класса Heap\n
        heap.rs
        use std::collections::BinaryHeap;\nuse std::cmp::Reverse;\n\n/* Инициализация кучи */\n// Инициализация минимальной кучи\nlet mut min_heap = BinaryHeap::<Reverse<i32>>::new();\n// Инициализация максимальной кучи\nlet mut max_heap = BinaryHeap::new();\n\n/* Добавление элементов в кучу */\nmax_heap.push(1);\nmax_heap.push(3);\nmax_heap.push(2);\nmax_heap.push(5);\nmax_heap.push(4);\n\n/* Получение элемента на вершине кучи */\nlet peek = max_heap.peek().unwrap();  // 5\n\n/* Извлечение элемента с вершины кучи */\n// Извлеченные элементы образуют последовательность по убыванию\nlet peek = max_heap.pop().unwrap();   // 5\nlet peek = max_heap.pop().unwrap();   // 4\nlet peek = max_heap.pop().unwrap();   // 3\nlet peek = max_heap.pop().unwrap();   // 2\nlet peek = max_heap.pop().unwrap();   // 1\n\n/* Получение размера кучи */\nlet size = max_heap.len();\n\n/* Проверка, пуста ли куча */\nlet is_empty = max_heap.is_empty();\n\n/* Построение кучи из входного списка */\nlet min_heap = BinaryHeap::from(vec![Reverse(1), Reverse(3), Reverse(2), Reverse(5), Reverse(4)]);\n
        heap.c
        // В C нет встроенного класса Heap\n
        heap.kt
        /* Инициализация кучи */\n// Инициализация минимальной кучи\nvar minHeap = PriorityQueue<Int>()\n// Инициализация максимальной кучи (достаточно изменить Comparator через lambda)\nval maxHeap = PriorityQueue { a: Int, b: Int -> b - a }\n\n/* Добавление элементов в кучу */\nmaxHeap.offer(1)\nmaxHeap.offer(3)\nmaxHeap.offer(2)\nmaxHeap.offer(5)\nmaxHeap.offer(4)\n\n/* Получение элемента на вершине кучи */\nvar peek = maxHeap.peek() // 5\n\n/* Извлечение элемента с вершины кучи */\n// Извлеченные элементы образуют последовательность по убыванию\npeek = maxHeap.poll() // 5\npeek = maxHeap.poll() // 4\npeek = maxHeap.poll() // 3\npeek = maxHeap.poll() // 2\npeek = maxHeap.poll() // 1\n\n/* Получение размера кучи */\nval size = maxHeap.size\n\n/* Проверка, пуста ли куча */\nval isEmpty = maxHeap.isEmpty()\n\n/* Построение кучи из входного списка */\nminHeap = PriorityQueue(mutableListOf(1, 3, 2, 5, 4))\n
        heap.rb
        # В Ruby нет встроенного класса Heap\n
        Визуализация выполнения

        https://pythontutor.com/render.html#code=import%20heapq%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20min-%D0%BA%D1%83%D1%87%D1%83%0A%20%20%20%20min_heap%2C%20flag%20%3D%20%5B%5D%2C%201%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20max-%D0%BA%D1%83%D1%87%D1%83%0A%20%20%20%20max_heap%2C%20flag%20%3D%20%5B%5D%2C%20-1%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%9C%D0%BE%D0%B4%D1%83%D0%BB%D1%8C%20heapq%20%D0%B2%20Python%20%D0%BF%D0%BE%20%D1%83%D0%BC%D0%BE%D0%BB%D1%87%D0%B0%D0%BD%D0%B8%D1%8E%20%D1%80%D0%B5%D0%B0%D0%BB%D0%B8%D0%B7%D1%83%D0%B5%D1%82%20min-%D0%BA%D1%83%D1%87%D1%83%0A%20%20%20%20%23%20%D0%95%D1%81%D0%BB%D0%B8%20%D0%BF%D0%B5%D1%80%D0%B5%D0%B4%20%D0%BF%D0%BE%D0%BC%D0%B5%D1%89%D0%B5%D0%BD%D0%B8%D0%B5%D0%BC%20%D0%B2%20%D0%BA%D1%83%D1%87%D1%83%20%D0%B1%D1%80%D0%B0%D1%82%D1%8C%20%D0%BE%D1%82%D1%80%D0%B8%D1%86%D0%B0%D0%BD%D0%B8%D0%B5%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D0%B0%2C%20%D0%BC%D0%BE%D0%B6%D0%BD%D0%BE%20%D0%BE%D0%B1%D1%80%D0%B0%D1%82%D0%B8%D1%82%D1%8C%20%D0%BE%D1%82%D0%BD%D0%BE%D1%88%D0%B5%D0%BD%D0%B8%D0%B5%20%D0%BF%D0%BE%D1%80%D1%8F%D0%B4%D0%BA%D0%B0%20%D0%B8%20%D1%82%D0%B5%D0%BC%20%D1%81%D0%B0%D0%BC%D1%8B%D0%BC%20%D1%80%D0%B5%D0%B0%D0%BB%D0%B8%D0%B7%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20max-%D0%BA%D1%83%D1%87%D1%83%0A%20%20%20%20%23%20%D0%92%20%D1%8D%D1%82%D0%BE%D0%BC%20%D0%BF%D1%80%D0%B8%D0%BC%D0%B5%D1%80%D0%B5%20flag%20%3D%201%20%D1%81%D0%BE%D0%BE%D1%82%D0%B2%D0%B5%D1%82%D1%81%D1%82%D0%B2%D1%83%D0%B5%D1%82%20min-%D0%BA%D1%83%D1%87%D0%B5%2C%20%D0%B0%20flag%20%3D%20-1%20%D1%81%D0%BE%D0%BE%D1%82%D0%B2%D0%B5%D1%82%D1%81%D1%82%D0%B2%D1%83%D0%B5%D1%82%20max-%D0%BA%D1%83%D1%87%D0%B5%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%94%D0%BE%D0%B1%D0%B0%D0%B2%D0%B8%D1%82%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B2%20%D0%BA%D1%83%D1%87%D1%83%0A%20%20%20%20heapq.heappush%28max_heap%2C%20flag%20%2A%201%29%0A%20%20%20%20heapq.heappush%28max_heap%2C%20flag%20%2A%203%29%0A%20%20%20%20heapq.heappush%28max_heap%2C%20flag%20%2A%202%29%0A%20%20%20%20heapq.heappush%28max_heap%2C%20flag%20%2A%205%29%0A%20%20%20%20heapq.heappush%28max_heap%2C%20flag%20%2A%204%29%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B8%D1%82%D1%8C%20%D0%B2%D0%B5%D1%80%D1%85%D0%BD%D0%B8%D0%B9%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%BA%D1%83%D1%87%D0%B8%0A%20%20%20%20peek%20%3D%20flag%20%2A%20max_heap%5B0%5D%20%23%205%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%98%D0%B7%D0%B2%D0%BB%D0%B5%D1%87%D1%8C%20%D0%B2%D0%B5%D1%80%D1%85%D0%BD%D0%B8%D0%B9%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B8%D0%B7%20%D0%BA%D1%83%D1%87%D0%B8%0A%20%20%20%20%23%20%D0%98%D0%B7%D0%B2%D0%BB%D0%B5%D1%87%D0%B5%D0%BD%D0%BD%D1%8B%D0%B5%20%D0%B8%D0%B7%20%D0%BA%D1%83%D1%87%D0%B8%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D1%8B%20%D0%BE%D0%B1%D1%80%D0%B0%D0%B7%D1%83%D1%8E%D1%82%20%D0%BF%D0%BE%D1%81%D0%BB%D0%B5%D0%B4%D0%BE%D0%B2%D0%B0%D1%82%D0%B5%D0%BB%D1%8C%D0%BD%D0%BE%D1%81%D1%82%D1%8C%20%D0%BE%D1%82%20%D0%B1%D0%BE%D0%BB%D1%8C%D1%88%D0%B5%D0%B3%D0%BE%20%D0%BA%20%D0%BC%D0%B5%D0%BD%D1%8C%D1%88%D0%B5%D0%BC%D1%83%0A%20%20%20%20val%20%3D%20flag%20%2A%20heapq.heappop%28max_heap%29%20%23%205%0A%20%20%20%20val%20%3D%20flag%20%2A%20heapq.heappop%28max_heap%29%20%23%204%0A%20%20%20%20val%20%3D%20flag%20%2A%20heapq.heappop%28max_heap%29%20%23%203%0A%20%20%20%20val%20%3D%20flag%20%2A%20heapq.heappop%28max_heap%29%20%23%202%0A%20%20%20%20val%20%3D%20flag%20%2A%20heapq.heappop%28max_heap%29%20%23%201%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B8%D1%82%D1%8C%20%D1%80%D0%B0%D0%B7%D0%BC%D0%B5%D1%80%20%D0%BA%D1%83%D1%87%D0%B8%0A%20%20%20%20size%20%3D%20len%28max_heap%29%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%9F%D1%80%D0%BE%D0%B2%D0%B5%D1%80%D0%B8%D1%82%D1%8C%2C%20%D0%BF%D1%83%D1%81%D1%82%D0%B0%20%D0%BB%D0%B8%20%D0%BA%D1%83%D1%87%D0%B0%0A%20%20%20%20is_empty%20%3D%20not%20max_heap%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%92%D1%85%D0%BE%D0%B4%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%D0%B8%D0%BF%D0%BE%D1%81%D1%82%D1%80%D0%BE%D0%B5%D0%BD%D0%B8%D0%B5%20%D0%BA%D1%83%D1%87%D0%B8%0A%20%20%20%20min_heap%20%3D%20%5B1%2C%203%2C%202%2C%205%2C%204%5D%0A%20%20%20%20heapq.heapify%28min_heap%29&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

        ","path":["Глава 8. Куча","8.1   Куча"],"tags":[]},{"location":"chapter_heap/heap/#812","level":2,"title":"8.1.2   Реализация кучи","text":"

        Ниже реализуется максимальная куча. Чтобы преобразовать ее в минимальную кучу, достаточно инвертировать всю логику сравнений по величине, например заменить \\(\\geq\\) на \\(\\leq\\) . Заинтересованные читатели могут попробовать реализовать это самостоятельно.

        ","path":["Глава 8. Куча","8.1   Куча"],"tags":[]},{"location":"chapter_heap/heap/#1","level":3,"title":"1.   Хранение и представление кучи","text":"

        В разделе \"Двоичные деревья\" мы уже говорили, что полное двоичное дерево очень удобно представлять массивом. Поскольку куча как раз и является полным двоичным деревом, для хранения кучи мы также будем использовать массив.

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

        Как показано на рисунке 8-2, для заданного индекса \\(i\\) индекс левого дочернего узла равен \\(2i + 1\\) , правого дочернего узла - \\(2i + 2\\) , а родительского узла - \\((i - 1) / 2\\) с округлением вниз. Если индекс выходит за допустимые границы, это означает пустой узел или отсутствие узла.

        Рисунок 8-2   Представление и хранение кучи

        Мы можем инкапсулировать формулы отображения индексов в функции, чтобы потом было удобнее ими пользоваться:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
        def left(self, i: int) -> int:\n    \"\"\"Получить индекс левого дочернего узла\"\"\"\n    return 2 * i + 1\n\ndef right(self, i: int) -> int:\n    \"\"\"Получить индекс правого дочернего узла\"\"\"\n    return 2 * i + 2\n\ndef parent(self, i: int) -> int:\n    \"\"\"Получить индекс родительского узла\"\"\"\n    return (i - 1) // 2  # Округление вниз при делении\n
        my_heap.cpp
        /* Получить индекс левого дочернего узла */\nint left(int i) {\n    return 2 * i + 1;\n}\n\n/* Получить индекс правого дочернего узла */\nint right(int i) {\n    return 2 * i + 2;\n}\n\n/* Получить индекс родительского узла */\nint parent(int i) {\n    return (i - 1) / 2; // Округление вниз при делении\n}\n
        my_heap.java
        /* Получить индекс левого дочернего узла */\nint left(int i) {\n    return 2 * i + 1;\n}\n\n/* Получить индекс правого дочернего узла */\nint right(int i) {\n    return 2 * i + 2;\n}\n\n/* Получить индекс родительского узла */\nint parent(int i) {\n    return (i - 1) / 2; // Округление вниз при делении\n}\n
        my_heap.cs
        /* Получить индекс левого дочернего узла */\nint Left(int i) {\n    return 2 * i + 1;\n}\n\n/* Получить индекс правого дочернего узла */\nint Right(int i) {\n    return 2 * i + 2;\n}\n\n/* Получить индекс родительского узла */\nint Parent(int i) {\n    return (i - 1) / 2; // Округление вниз при делении\n}\n
        my_heap.go
        /* Получить индекс левого дочернего узла */\nfunc (h *maxHeap) left(i int) int {\n    return 2*i + 1\n}\n\n/* Получить индекс правого дочернего узла */\nfunc (h *maxHeap) right(i int) int {\n    return 2*i + 2\n}\n\n/* Получить индекс родительского узла */\nfunc (h *maxHeap) parent(i int) int {\n    // Округление вниз при делении\n    return (i - 1) / 2\n}\n
        my_heap.swift
        /* Получить индекс левого дочернего узла */\nfunc left(i: Int) -> Int {\n    2 * i + 1\n}\n\n/* Получить индекс правого дочернего узла */\nfunc right(i: Int) -> Int {\n    2 * i + 2\n}\n\n/* Получить индекс родительского узла */\nfunc parent(i: Int) -> Int {\n    (i - 1) / 2 // Округление вниз при делении\n}\n
        my_heap.js
        /* Получить индекс левого дочернего узла */\n#left(i) {\n    return 2 * i + 1;\n}\n\n/* Получить индекс правого дочернего узла */\n#right(i) {\n    return 2 * i + 2;\n}\n\n/* Получить индекс родительского узла */\n#parent(i) {\n    return Math.floor((i - 1) / 2); // Округление вниз при делении\n}\n
        my_heap.ts
        /* Получить индекс левого дочернего узла */\nleft(i: number): number {\n    return 2 * i + 1;\n}\n\n/* Получить индекс правого дочернего узла */\nright(i: number): number {\n    return 2 * i + 2;\n}\n\n/* Получить индекс родительского узла */\nparent(i: number): number {\n    return Math.floor((i - 1) / 2); // Округление вниз при делении\n}\n
        my_heap.dart
        /* Получить индекс левого дочернего узла */\nint _left(int i) {\n  return 2 * i + 1;\n}\n\n/* Получить индекс правого дочернего узла */\nint _right(int i) {\n  return 2 * i + 2;\n}\n\n/* Получить индекс родительского узла */\nint _parent(int i) {\n  return (i - 1) ~/ 2; // Округление вниз при делении\n}\n
        my_heap.rs
        /* Получить индекс левого дочернего узла */\nfn left(i: usize) -> usize {\n    2 * i + 1\n}\n\n/* Получить индекс правого дочернего узла */\nfn right(i: usize) -> usize {\n    2 * i + 2\n}\n\n/* Получить индекс родительского узла */\nfn parent(i: usize) -> usize {\n    (i - 1) / 2 // Округление вниз при делении\n}\n
        my_heap.c
        /* Получить индекс левого дочернего узла */\nint left(MaxHeap *maxHeap, int i) {\n    return 2 * i + 1;\n}\n\n/* Получить индекс правого дочернего узла */\nint right(MaxHeap *maxHeap, int i) {\n    return 2 * i + 2;\n}\n\n/* Получить индекс родительского узла */\nint parent(MaxHeap *maxHeap, int i) {\n    return (i - 1) / 2; // Округление вниз\n}\n
        my_heap.kt
        /* Получить индекс левого дочернего узла */\nfun left(i: Int): Int {\n    return 2 * i + 1\n}\n\n/* Получить индекс правого дочернего узла */\nfun right(i: Int): Int {\n    return 2 * i + 2\n}\n\n/* Получить индекс родительского узла */\nfun parent(i: Int): Int {\n    return (i - 1) / 2 // Округление вниз при делении\n}\n
        my_heap.rb
        ### Получить индекс левого дочернего узла ###\ndef left(i)\n  2 * i + 1\nend\n\n### Получить индекс правого дочернего узла ###\ndef right(i)\n  2 * i + 2\nend\n\n### Получить индекс родительского узла ###\ndef parent(i)\n  (i - 1) / 2     # Округление вниз при делении\nend\n
        ","path":["Глава 8. Куча","8.1   Куча"],"tags":[]},{"location":"chapter_heap/heap/#2","level":3,"title":"2.   Доступ к элементу на вершине кучи","text":"

        Элемент на вершине кучи - это корневой узел двоичного дерева, то есть первый элемент списка:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
        def peek(self) -> int:\n    \"\"\"Доступ к элементу на вершине кучи\"\"\"\n    return self.max_heap[0]\n
        my_heap.cpp
        /* Доступ к элементу на вершине кучи */\nint peek() {\n    return maxHeap[0];\n}\n
        my_heap.java
        /* Доступ к элементу на вершине кучи */\nint peek() {\n    return maxHeap.get(0);\n}\n
        my_heap.cs
        /* Доступ к элементу на вершине кучи */\nint Peek() {\n    return maxHeap[0];\n}\n
        my_heap.go
        /* Доступ к элементу на вершине кучи */\nfunc (h *maxHeap) peek() any {\n    return h.data[0]\n}\n
        my_heap.swift
        /* Доступ к элементу на вершине кучи */\nfunc peek() -> Int {\n    maxHeap[0]\n}\n
        my_heap.js
        /* Доступ к элементу на вершине кучи */\npeek() {\n    return this.#maxHeap[0];\n}\n
        my_heap.ts
        /* Доступ к элементу на вершине кучи */\npeek(): number {\n    return this.maxHeap[0];\n}\n
        my_heap.dart
        /* Доступ к элементу на вершине кучи */\nint peek() {\n  return _maxHeap[0];\n}\n
        my_heap.rs
        /* Доступ к элементу на вершине кучи */\nfn peek(&self) -> Option<i32> {\n    self.max_heap.first().copied()\n}\n
        my_heap.c
        /* Доступ к элементу на вершине кучи */\nint peek(MaxHeap *maxHeap) {\n    return maxHeap->data[0];\n}\n
        my_heap.kt
        /* Доступ к элементу на вершине кучи */\nfun peek(): Int {\n    return maxHeap[0]\n}\n
        my_heap.rb
        ### Доступ к элементу на вершине кучи ###\ndef peek\n  @max_heap[0]\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 8. Куча","8.1   Куча"],"tags":[]},{"location":"chapter_heap/heap/#3","level":3,"title":"3.   Добавление элемента в кучу","text":"

        Пусть дан элемент val . Сначала мы помещаем его в основание кучи. После добавления свойства кучи могут нарушиться, потому что val может оказаться больше, чем другие элементы в куче. Поэтому необходимо восстановить порядок на пути от вставленного узла к корню ; эта операция называется упорядочиванием кучи.

        Рассмотрим ситуацию, когда упорядочивание выполняется снизу вверх, начиная от только что вставленного узла. Как показано на рисунках ниже, мы сравниваем значение вставленного узла со значением его родителя; если вставленный узел больше, то меняем их местами. Затем продолжаем выполнять ту же операцию и последовательно восстанавливать корректность всех узлов по пути снизу вверх, пока не выйдем за корень или не встретим узел, для которого обмен не требуется.

        <1><2><3><4><5><6><7><8><9>

        Рисунок 8-3   Шаги добавления элемента в кучу

        Пусть общее число узлов равно \\(n\\) , тогда высота дерева равна \\(O(\\log n)\\) . Следовательно, максимальное число итераций операции упорядочивания кучи тоже не превышает \\(O(\\log n)\\) . Отсюда временная сложность добавления элемента в кучу равна \\(O(\\log n)\\) . Код приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
        def push(self, val: int):\n    \"\"\"Добавление элемента в кучу\"\"\"\n    # Добавление узла\n    self.max_heap.append(val)\n    # Просеивание снизу вверх\n    self.sift_up(self.size() - 1)\n\ndef sift_up(self, i: int):\n    \"\"\"Начиная с узла i, выполнить просеивание снизу вверх\"\"\"\n    while True:\n        # Получение родительского узла для узла i\n        p = self.parent(i)\n        # Завершить heapify, когда «корневой узел уже пройден» или «узел не требует исправления»\n        if p < 0 or self.max_heap[i] <= self.max_heap[p]:\n            break\n        # Поменять два узла местами\n        self.swap(i, p)\n        # Циклическое просеивание вверх\n        i = p\n
        my_heap.cpp
        /* Добавление элемента в кучу */\nvoid push(int val) {\n    // Добавление узла\n    maxHeap.push_back(val);\n    // Просеивание снизу вверх\n    siftUp(size() - 1);\n}\n\n/* Начиная с узла i, выполнить просеивание снизу вверх */\nvoid siftUp(int i) {\n    while (true) {\n        // Получение родительского узла для узла i\n        int p = parent(i);\n        // Завершить heapify, когда «корневой узел уже пройден» или «узел не требует исправления»\n        if (p < 0 || maxHeap[i] <= maxHeap[p])\n            break;\n        // Поменять два узла местами\n        swap(maxHeap[i], maxHeap[p]);\n        // Циклическое просеивание вверх\n        i = p;\n    }\n}\n
        my_heap.java
        /* Добавление элемента в кучу */\nvoid push(int val) {\n    // Добавление узла\n    maxHeap.add(val);\n    // Просеивание снизу вверх\n    siftUp(size() - 1);\n}\n\n/* Начиная с узла i, выполнить просеивание снизу вверх */\nvoid siftUp(int i) {\n    while (true) {\n        // Получение родительского узла для узла i\n        int p = parent(i);\n        // Завершить heapify, когда «корневой узел уже пройден» или «узел не требует исправления»\n        if (p < 0 || maxHeap.get(i) <= maxHeap.get(p))\n            break;\n        // Поменять два узла местами\n        swap(i, p);\n        // Циклическое просеивание вверх\n        i = p;\n    }\n}\n
        my_heap.cs
        /* Добавление элемента в кучу */\nvoid Push(int val) {\n    // Добавление узла\n    maxHeap.Add(val);\n    // Просеивание снизу вверх\n    SiftUp(Size() - 1);\n}\n\n/* Начиная с узла i, выполнить просеивание снизу вверх */\nvoid SiftUp(int i) {\n    while (true) {\n        // Получение родительского узла для узла i\n        int p = Parent(i);\n        // Если «выход за пределы корневого узла» или «узел не требует исправления», завершить просеивание\n        if (p < 0 || maxHeap[i] <= maxHeap[p])\n            break;\n        // Поменять два узла местами\n        Swap(i, p);\n        // Циклическое просеивание вверх\n        i = p;\n    }\n}\n
        my_heap.go
        /* Добавление элемента в кучу */\nfunc (h *maxHeap) push(val any) {\n    // Добавление узла\n    h.data = append(h.data, val)\n    // Просеивание снизу вверх\n    h.siftUp(len(h.data) - 1)\n}\n\n/* Начиная с узла i, выполнить просеивание снизу вверх */\nfunc (h *maxHeap) siftUp(i int) {\n    for true {\n        // Получение родительского узла для узла i\n        p := h.parent(i)\n        // Завершить heapify, когда «корневой узел уже пройден» или «узел не требует исправления»\n        if p < 0 || h.data[i].(int) <= h.data[p].(int) {\n            break\n        }\n        // Поменять два узла местами\n        h.swap(i, p)\n        // Циклическое просеивание вверх\n        i = p\n    }\n}\n
        my_heap.swift
        /* Добавление элемента в кучу */\nfunc push(val: Int) {\n    // Добавление узла\n    maxHeap.append(val)\n    // Просеивание снизу вверх\n    siftUp(i: size() - 1)\n}\n\n/* Начиная с узла i, выполнить просеивание снизу вверх */\nfunc siftUp(i: Int) {\n    var i = i\n    while true {\n        // Получение родительского узла для узла i\n        let p = parent(i: i)\n        // Завершить heapify, когда «корневой узел уже пройден» или «узел не требует исправления»\n        if p < 0 || maxHeap[i] <= maxHeap[p] {\n            break\n        }\n        // Поменять два узла местами\n        swap(i: i, j: p)\n        // Циклическое просеивание вверх\n        i = p\n    }\n}\n
        my_heap.js
        /* Добавление элемента в кучу */\npush(val) {\n    // Добавление узла\n    this.#maxHeap.push(val);\n    // Просеивание снизу вверх\n    this.#siftUp(this.size() - 1);\n}\n\n/* Начиная с узла i, выполнить просеивание снизу вверх */\n#siftUp(i) {\n    while (true) {\n        // Получение родительского узла для узла i\n        const p = this.#parent(i);\n        // Завершить heapify, когда «корневой узел уже пройден» или «узел не требует исправления»\n        if (p < 0 || this.#maxHeap[i] <= this.#maxHeap[p]) break;\n        // Поменять два узла местами\n        this.#swap(i, p);\n        // Циклическое просеивание вверх\n        i = p;\n    }\n}\n
        my_heap.ts
        /* Добавление элемента в кучу */\npush(val: number): void {\n    // Добавление узла\n    this.maxHeap.push(val);\n    // Просеивание снизу вверх\n    this.siftUp(this.size() - 1);\n}\n\n/* Начиная с узла i, выполнить просеивание снизу вверх */\nsiftUp(i: number): void {\n    while (true) {\n        // Получение родительского узла для узла i\n        const p = this.parent(i);\n        // Завершить heapify, когда «корневой узел уже пройден» или «узел не требует исправления»\n        if (p < 0 || this.maxHeap[i] <= this.maxHeap[p]) break;\n        // Поменять два узла местами\n        this.swap(i, p);\n        // Циклическое просеивание вверх\n        i = p;\n    }\n}\n
        my_heap.dart
        /* Добавление элемента в кучу */\nvoid push(int val) {\n  // Добавление узла\n  _maxHeap.add(val);\n  // Просеивание снизу вверх\n  siftUp(size() - 1);\n}\n\n/* Начиная с узла i, выполнить просеивание снизу вверх */\nvoid siftUp(int i) {\n  while (true) {\n    // Получение родительского узла для узла i\n    int p = _parent(i);\n    // Завершить heapify, когда «корневой узел уже пройден» или «узел не требует исправления»\n    if (p < 0 || _maxHeap[i] <= _maxHeap[p]) {\n      break;\n    }\n    // Поменять два узла местами\n    _swap(i, p);\n    // Циклическое просеивание вверх\n    i = p;\n  }\n}\n
        my_heap.rs
        /* Добавление элемента в кучу */\nfn push(&mut self, val: i32) {\n    // Добавление узла\n    self.max_heap.push(val);\n    // Просеивание снизу вверх\n    self.sift_up(self.size() - 1);\n}\n\n/* Начиная с узла i, выполнить просеивание снизу вверх */\nfn sift_up(&mut self, mut i: usize) {\n    loop {\n        // Если узел i уже является вершиной кучи, завершить просеивание\n        if i == 0 {\n            break;\n        }\n        // Получение родительского узла для узла i\n        let p = Self::parent(i);\n        // Когда «узел не требует исправления», завершить просеивание\n        if self.max_heap[i] <= self.max_heap[p] {\n            break;\n        }\n        // Поменять два узла местами\n        self.swap(i, p);\n        // Циклическое просеивание вверх\n        i = p;\n    }\n}\n
        my_heap.c
        /* Добавление элемента в кучу */\nvoid push(MaxHeap *maxHeap, int val) {\n    // По умолчанию не следует добавлять так много узлов\n    if (maxHeap->size == MAX_SIZE) {\n        printf(\"heap is full!\");\n        return;\n    }\n    // Добавление узла\n    maxHeap->data[maxHeap->size] = val;\n    maxHeap->size++;\n\n    // Просеивание снизу вверх\n    siftUp(maxHeap, maxHeap->size - 1);\n}\n\n/* Начиная с узла i, выполнить просеивание снизу вверх */\nvoid siftUp(MaxHeap *maxHeap, int i) {\n    while (true) {\n        // Получение родительского узла для узла i\n        int p = parent(maxHeap, i);\n        // Завершить heapify, когда «корневой узел уже пройден» или «узел не требует исправления»\n        if (p < 0 || maxHeap->data[i] <= maxHeap->data[p]) {\n            break;\n        }\n        // Поменять два узла местами\n        swap(maxHeap, i, p);\n        // Циклическое просеивание вверх\n        i = p;\n    }\n}\n
        my_heap.kt
        /* Добавление элемента в кучу */\nfun push(_val: Int) {\n    // Добавление узла\n    maxHeap.add(_val)\n    // Просеивание снизу вверх\n    siftUp(size() - 1)\n}\n\n/* Начиная с узла i, выполнить просеивание снизу вверх */\nfun siftUp(it: Int) {\n    // Параметры функций в Kotlin неизменяемы, поэтому создается временная переменная\n    var i = it\n    while (true) {\n        // Получение родительского узла для узла i\n        val p = parent(i)\n        // Завершить heapify, когда «корневой узел уже пройден» или «узел не требует исправления»\n        if (p < 0 || maxHeap[i] <= maxHeap[p]) break\n        // Поменять два узла местами\n        swap(i, p)\n        // Циклическое просеивание вверх\n        i = p\n    }\n}\n
        my_heap.rb
        ### Добавление элемента в кучу ###\ndef push(val)\n  # Добавление узла\n  @max_heap << val\n  # Просеивание снизу вверх\n  sift_up(size - 1)\nend\n\n### Начиная с узла i, выполнить просеивание снизу вверх ###\ndef sift_up(i)\n  loop do\n    # Получение родительского узла для узла i\n    p = parent(i)\n    # Завершить heapify, когда «корневой узел уже пройден» или «узел не требует исправления»\n    break if p < 0 || @max_heap[i] <= @max_heap[p]\n    # Поменять два узла местами\n    swap(i, p)\n    # Циклическое просеивание вверх\n    i = p\n  end\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 8. Куча","8.1   Куча"],"tags":[]},{"location":"chapter_heap/heap/#4","level":3,"title":"4.   Извлечение элемента с вершины кучи","text":"

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

        1. Поменять местами элемент на вершине кучи и элемент у основания кучи, то есть поменять корневой узел с самым правым листовым узлом.
        2. После обмена удалить основание кучи из списка. Стоит отметить, что, поскольку обмен уже выполнен, фактически удаляется исходный элемент вершины кучи.
        3. Начиная от корневого узла, выполнить упорядочивание кучи сверху вниз.

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

        <1><2><3><4><5><6><7><8><9><10>

        Рисунок 8-4   Шаги извлечения элемента с вершины кучи

        Как и операция добавления в кучу, операция извлечения элемента с вершины кучи также имеет временную сложность \\(O(\\log n)\\) . Код приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
        def pop(self) -> int:\n    \"\"\"Извлечение элемента из кучи\"\"\"\n    # Обработка пустого случая\n    if self.is_empty():\n        raise IndexError(\"куча пуста\")\n    # Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n    self.swap(0, self.size() - 1)\n    # Удаление узла\n    val = self.max_heap.pop()\n    # Просеивание сверху вниз\n    self.sift_down(0)\n    # Вернуть элемент с вершины кучи\n    return val\n\ndef sift_down(self, i: int):\n    \"\"\"Начиная с узла i, выполнить просеивание сверху вниз\"\"\"\n    while True:\n        # Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n        l, r, ma = self.left(i), self.right(i), i\n        if l < self.size() and self.max_heap[l] > self.max_heap[ma]:\n            ma = l\n        if r < self.size() and self.max_heap[r] > self.max_heap[ma]:\n            ma = r\n        # Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if ma == i:\n            break\n        # Поменять два узла местами\n        self.swap(i, ma)\n        # Циклическое просеивание вниз\n        i = ma\n
        my_heap.cpp
        /* Извлечение элемента из кучи */\nvoid pop() {\n    // Обработка пустого случая\n    if (isEmpty()) {\n        throw out_of_range(\"куча пуста\");\n    }\n    // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n    swap(maxHeap[0], maxHeap[size() - 1]);\n    // Удаление узла\n    maxHeap.pop_back();\n    // Просеивание сверху вниз\n    siftDown(0);\n}\n\n/* Начиная с узла i, выполнить просеивание сверху вниз */\nvoid siftDown(int i) {\n    while (true) {\n        // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n        int l = left(i), r = right(i), ma = i;\n        if (l < size() && maxHeap[l] > maxHeap[ma])\n            ma = l;\n        if (r < size() && maxHeap[r] > maxHeap[ma])\n            ma = r;\n        // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if (ma == i)\n            break;\n        swap(maxHeap[i], maxHeap[ma]);\n        // Циклическое просеивание вниз\n        i = ma;\n    }\n}\n
        my_heap.java
        /* Извлечение элемента из кучи */\nint pop() {\n    // Обработка пустого случая\n    if (isEmpty())\n        throw new IndexOutOfBoundsException();\n    // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n    swap(0, size() - 1);\n    // Удаление узла\n    int val = maxHeap.remove(size() - 1);\n    // Просеивание сверху вниз\n    siftDown(0);\n    // Вернуть элемент с вершины кучи\n    return val;\n}\n\n/* Начиная с узла i, выполнить просеивание сверху вниз */\nvoid siftDown(int i) {\n    while (true) {\n        // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n        int l = left(i), r = right(i), ma = i;\n        if (l < size() && maxHeap.get(l) > maxHeap.get(ma))\n            ma = l;\n        if (r < size() && maxHeap.get(r) > maxHeap.get(ma))\n            ma = r;\n        // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if (ma == i)\n            break;\n        // Поменять два узла местами\n        swap(i, ma);\n        // Циклическое просеивание вниз\n        i = ma;\n    }\n}\n
        my_heap.cs
        /* Извлечение элемента из кучи */\nint Pop() {\n    // Обработка пустого случая\n    if (IsEmpty())\n        throw new IndexOutOfRangeException();\n    // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n    Swap(0, Size() - 1);\n    // Удаление узла\n    int val = maxHeap.Last();\n    maxHeap.RemoveAt(Size() - 1);\n    // Просеивание сверху вниз\n    SiftDown(0);\n    // Вернуть элемент с вершины кучи\n    return val;\n}\n\n/* Начиная с узла i, выполнить просеивание сверху вниз */\nvoid SiftDown(int i) {\n    while (true) {\n        // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n        int l = Left(i), r = Right(i), ma = i;\n        if (l < Size() && maxHeap[l] > maxHeap[ma])\n            ma = l;\n        if (r < Size() && maxHeap[r] > maxHeap[ma])\n            ma = r;\n        // Если «узел i максимален» или «выход за пределы листовых узлов», завершить просеивание\n        if (ma == i) break;\n        // Поменять два узла местами\n        Swap(i, ma);\n        // Циклическое просеивание вниз\n        i = ma;\n    }\n}\n
        my_heap.go
        /* Извлечение элемента из кучи */\nfunc (h *maxHeap) pop() any {\n    // Обработка пустого случая\n    if h.isEmpty() {\n        fmt.Println(\"error\")\n        return nil\n    }\n    // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n    h.swap(0, h.size()-1)\n    // Удаление узла\n    val := h.data[len(h.data)-1]\n    h.data = h.data[:len(h.data)-1]\n    // Просеивание сверху вниз\n    h.siftDown(0)\n\n    // Вернуть элемент с вершины кучи\n    return val\n}\n\n/* Начиная с узла i, выполнить просеивание сверху вниз */\nfunc (h *maxHeap) siftDown(i int) {\n    for true {\n        // Определить узел с максимальным значением среди i, l и r и обозначить его как max\n        l, r, max := h.left(i), h.right(i), i\n        if l < h.size() && h.data[l].(int) > h.data[max].(int) {\n            max = l\n        }\n        if r < h.size() && h.data[r].(int) > h.data[max].(int) {\n            max = r\n        }\n        // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if max == i {\n            break\n        }\n        // Поменять два узла местами\n        h.swap(i, max)\n        // Циклическое просеивание вниз\n        i = max\n    }\n}\n
        my_heap.swift
        /* Извлечение элемента из кучи */\nfunc pop() -> Int {\n    // Обработка пустого случая\n    if isEmpty() {\n        fatalError(\"куча пуста\")\n    }\n    // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n    swap(i: 0, j: size() - 1)\n    // Удаление узла\n    let val = maxHeap.remove(at: size() - 1)\n    // Просеивание сверху вниз\n    siftDown(i: 0)\n    // Вернуть элемент с вершины кучи\n    return val\n}\n\n/* Начиная с узла i, выполнить просеивание сверху вниз */\nfunc siftDown(i: Int) {\n    var i = i\n    while true {\n        // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n        let l = left(i: i)\n        let r = right(i: i)\n        var ma = i\n        if l < size(), maxHeap[l] > maxHeap[ma] {\n            ma = l\n        }\n        if r < size(), maxHeap[r] > maxHeap[ma] {\n            ma = r\n        }\n        // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if ma == i {\n            break\n        }\n        // Поменять два узла местами\n        swap(i: i, j: ma)\n        // Циклическое просеивание вниз\n        i = ma\n    }\n}\n
        my_heap.js
        /* Извлечение элемента из кучи */\npop() {\n    // Обработка пустого случая\n    if (this.isEmpty()) throw new Error('куча пуста');\n    // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n    this.#swap(0, this.size() - 1);\n    // Удаление узла\n    const val = this.#maxHeap.pop();\n    // Просеивание сверху вниз\n    this.#siftDown(0);\n    // Вернуть элемент с вершины кучи\n    return val;\n}\n\n/* Начиная с узла i, выполнить просеивание сверху вниз */\n#siftDown(i) {\n    while (true) {\n        // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n        const l = this.#left(i),\n            r = this.#right(i);\n        let ma = i;\n        if (l < this.size() && this.#maxHeap[l] > this.#maxHeap[ma]) ma = l;\n        if (r < this.size() && this.#maxHeap[r] > this.#maxHeap[ma]) ma = r;\n        // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if (ma === i) break;\n        // Поменять два узла местами\n        this.#swap(i, ma);\n        // Циклическое просеивание вниз\n        i = ma;\n    }\n}\n
        my_heap.ts
        /* Извлечение элемента из кучи */\npop(): number {\n    // Обработка пустого случая\n    if (this.isEmpty()) throw new RangeError('Heap is empty.');\n    // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n    this.swap(0, this.size() - 1);\n    // Удаление узла\n    const val = this.maxHeap.pop();\n    // Просеивание сверху вниз\n    this.siftDown(0);\n    // Вернуть элемент с вершины кучи\n    return val;\n}\n\n/* Начиная с узла i, выполнить просеивание сверху вниз */\nsiftDown(i: number): void {\n    while (true) {\n        // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n        const l = this.left(i),\n            r = this.right(i);\n        let ma = i;\n        if (l < this.size() && this.maxHeap[l] > this.maxHeap[ma]) ma = l;\n        if (r < this.size() && this.maxHeap[r] > this.maxHeap[ma]) ma = r;\n        // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if (ma === i) break;\n        // Поменять два узла местами\n        this.swap(i, ma);\n        // Циклическое просеивание вниз\n        i = ma;\n    }\n}\n
        my_heap.dart
        /* Извлечение элемента из кучи */\nint pop() {\n  // Обработка пустого случая\n  if (isEmpty()) throw Exception('куча пуста');\n  // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n  _swap(0, size() - 1);\n  // Удаление узла\n  int val = _maxHeap.removeLast();\n  // Просеивание сверху вниз\n  siftDown(0);\n  // Вернуть элемент с вершины кучи\n  return val;\n}\n\n/* Начиная с узла i, выполнить просеивание сверху вниз */\nvoid siftDown(int i) {\n  while (true) {\n    // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n    int l = _left(i);\n    int r = _right(i);\n    int ma = i;\n    if (l < size() && _maxHeap[l] > _maxHeap[ma]) ma = l;\n    if (r < size() && _maxHeap[r] > _maxHeap[ma]) ma = r;\n    // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n    if (ma == i) break;\n    // Поменять два узла местами\n    _swap(i, ma);\n    // Циклическое просеивание вниз\n    i = ma;\n  }\n}\n
        my_heap.rs
        /* Извлечение элемента из кучи */\nfn pop(&mut self) -> i32 {\n    // Обработка пустого случая\n    if self.is_empty() {\n        panic!(\"index out of bounds\");\n    }\n    // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n    self.swap(0, self.size() - 1);\n    // Удаление узла\n    let val = self.max_heap.pop().unwrap();\n    // Просеивание сверху вниз\n    self.sift_down(0);\n    // Вернуть элемент с вершины кучи\n    val\n}\n\n/* Начиная с узла i, выполнить просеивание сверху вниз */\nfn sift_down(&mut self, mut i: usize) {\n    loop {\n        // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n        let (l, r, mut ma) = (Self::left(i), Self::right(i), i);\n        if l < self.size() && self.max_heap[l] > self.max_heap[ma] {\n            ma = l;\n        }\n        if r < self.size() && self.max_heap[r] > self.max_heap[ma] {\n            ma = r;\n        }\n        // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if ma == i {\n            break;\n        }\n        // Поменять два узла местами\n        self.swap(i, ma);\n        // Циклическое просеивание вниз\n        i = ma;\n    }\n}\n
        my_heap.c
        /* Извлечение элемента из кучи */\nint pop(MaxHeap *maxHeap) {\n    // Обработка пустого случая\n    if (isEmpty(maxHeap)) {\n        printf(\"heap is empty!\");\n        return INT_MAX;\n    }\n    // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n    swap(maxHeap, 0, size(maxHeap) - 1);\n    // Удаление узла\n    int val = maxHeap->data[maxHeap->size - 1];\n    maxHeap->size--;\n    // Просеивание сверху вниз\n    siftDown(maxHeap, 0);\n\n    // Вернуть элемент с вершины кучи\n    return val;\n}\n\n/* Начиная с узла i, выполнить просеивание сверху вниз */\nvoid siftDown(MaxHeap *maxHeap, int i) {\n    while (true) {\n        // Определить узел с максимальным значением среди i, l и r и обозначить его как max\n        int l = left(maxHeap, i);\n        int r = right(maxHeap, i);\n        int max = i;\n        if (l < size(maxHeap) && maxHeap->data[l] > maxHeap->data[max]) {\n            max = l;\n        }\n        if (r < size(maxHeap) && maxHeap->data[r] > maxHeap->data[max]) {\n            max = r;\n        }\n        // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if (max == i) {\n            break;\n        }\n        // Поменять два узла местами\n        swap(maxHeap, i, max);\n        // Циклическое просеивание вниз\n        i = max;\n    }\n}\n
        my_heap.kt
        /* Извлечение элемента из кучи */\nfun pop(): Int {\n    // Обработка пустого случая\n    if (isEmpty()) throw IndexOutOfBoundsException()\n    // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n    swap(0, size() - 1)\n    // Удаление узла\n    val _val = maxHeap.removeAt(size() - 1)\n    // Просеивание сверху вниз\n    siftDown(0)\n    // Вернуть элемент с вершины кучи\n    return _val\n}\n\n/* Начиная с узла i, выполнить просеивание сверху вниз */\nfun siftDown(it: Int) {\n    // Параметры функций в Kotlin неизменяемы, поэтому создается временная переменная\n    var i = it\n    while (true) {\n        // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n        val l = left(i)\n        val r = right(i)\n        var ma = i\n        if (l < size() && maxHeap[l] > maxHeap[ma]) ma = l\n        if (r < size() && maxHeap[r] > maxHeap[ma]) ma = r\n        // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if (ma == i) break\n        // Поменять два узла местами\n        swap(i, ma)\n        // Циклическое просеивание вниз\n        i = ma\n    }\n}\n
        my_heap.rb
        ### Извлечение элемента из кучи ###\ndef pop\n  # Обработка пустого случая\n  raise IndexError, \"куча пуста\" if is_empty?\n  # Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n  swap(0, size - 1)\n  # Удаление узла\n  val = @max_heap.pop\n  # Просеивание сверху вниз\n  sift_down(0)\n  # Вернуть элемент с вершины кучи\n  val\nend\n\n### Начиная с узла i, выполнить просеивание сверху вниз ###\ndef sift_down(i)\n  loop do\n    # Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n    l, r, ma = left(i), right(i), i\n    ma = l if l < size && @max_heap[l] > @max_heap[ma]\n    ma = r if r < size && @max_heap[r] > @max_heap[ma]\n\n    # Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n    break if ma == i\n\n    # Поменять два узла местами\n    swap(i, ma)\n    # Циклическое просеивание вниз\n    i = ma\n  end\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 8. Куча","8.1   Куча"],"tags":[]},{"location":"chapter_heap/heap/#813","level":2,"title":"8.1.3   Типичные применения кучи","text":"
        • Очередь с приоритетом: куча обычно является предпочтительной структурой данных для реализации очереди с приоритетом; добавление и извлечение элементов имеют временную сложность \\(O(\\log n)\\) , а построение кучи - \\(O(n)\\) , и все эти операции выполняются очень эффективно.
        • Пирамидальная сортировка: для заданного набора данных можно построить кучу, а затем непрерывно извлекать из нее элементы, получая отсортированные данные. Однако на практике мы обычно используем более изящную реализацию пирамидальной сортировки; подробности см. в разделе \"Пирамидальная сортировка\".
        • Получение наибольших \\(k\\) элементов: это классическая алгоритмическая задача и одновременно типичное применение кучи. Например, выбор 10 самых горячих новостей для списка популярных тем или выбор 10 самых продаваемых товаров.
        ","path":["Глава 8. Куча","8.1   Куча"],"tags":[]},{"location":"chapter_heap/summary/","level":1,"title":"8.4   Резюме","text":"","path":["Глава 8. Куча","8.4   Резюме"],"tags":[]},{"location":"chapter_heap/summary/#1","level":3,"title":"1.   Ключевые выводы","text":"
        • Куча представляет собой полное двоичное дерево и делится на максимальную кучу и минимальную кучу. Элемент на вершине максимальной (минимальной) кучи является наибольшим (наименьшим).
        • Очередь с приоритетом определяется как очередь, элементы которой извлекаются в соответствии с приоритетом; обычно ее реализуют с помощью кучи.
        • К основным операциям кучи и их временным сложностям относятся: добавление элемента в кучу \\(O(\\log n)\\) , извлечение элемента с вершины кучи \\(O(\\log n)\\) и доступ к вершине кучи \\(O(1)\\) .
        • Полное двоичное дерево очень удобно представлять массивом, поэтому кучу обычно тоже хранят в массиве.
        • Операция упорядочивания кучи используется для поддержания свойств кучи и применяется как при добавлении элемента, так и при извлечении элемента.
        • Временную сложность построения кучи из \\(n\\) элементов можно оптимизировать до \\(O(n)\\) , что очень эффективно.
        • Top-k - это классическая алгоритмическая задача, которую можно эффективно решать с помощью кучи за \\(O(n \\log k)\\) .
        ","path":["Глава 8. Куча","8.4   Резюме"],"tags":[]},{"location":"chapter_heap/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

        Q: Является ли \"куча\" как структура данных тем же самым понятием, что и \"куча\" в управлении памятью?

        Это не одно и то же, просто у них случайно совпало название. Куча в памяти компьютерной системы является частью динамического распределения памяти: во время выполнения программы она используется для хранения данных. Программа может запросить определенный объем памяти в куче для хранения сложных структур, таких как объекты и массивы. Когда эти данные больше не нужны, память нужно освободить, чтобы не допустить утечек. По сравнению со стековой памятью управление памятью в куче требует большей осторожности, а неправильное использование может привести к утечкам памяти и проблемам с указателями.

        ","path":["Глава 8. Куча","8.4   Резюме"],"tags":[]},{"location":"chapter_heap/top_k/","level":1,"title":"8.3   Задача Top-k","text":"

        Question

        Дан неупорядоченный массив nums длины \\(n\\) . Требуется вернуть наибольшие \\(k\\) элементов массива.

        Для этой задачи мы сначала покажем два относительно прямолинейных способа решения, а затем более эффективный способ на основе кучи.

        ","path":["Глава 8. Куча","8.3   Задача Top-k"],"tags":[]},{"location":"chapter_heap/top_k/#831-1","level":2,"title":"8.3.1   Метод 1: выбор через обход","text":"

        Как показано на рисунке 8-6, можно выполнить \\(k\\) проходов по массиву и на каждом проходе извлекать соответственно \\(1\\)-й, \\(2\\)-й, \\(\\dots\\) , \\(k\\)-й по величине элемент; временная сложность такого подхода равна \\(O(nk)\\) .

        Этот метод подходит только для случая \\(k \\ll n\\) , потому что когда \\(k\\) приближается к \\(n\\) , его временная сложность стремится к \\(O(n^2)\\) , а это уже очень затратно.

        Рисунок 8-6   Поиск наибольших k элементов через обход

        Tip

        Когда \\(k = n\\) , мы получаем полную упорядоченную последовательность, и в этот момент задача становится эквивалентной алгоритму \"сортировка выбором\".

        ","path":["Глава 8. Куча","8.3   Задача Top-k"],"tags":[]},{"location":"chapter_heap/top_k/#832-2","level":2,"title":"8.3.2   Метод 2: сортировка","text":"

        Как показано на рисунке 8-7, можно сначала отсортировать массив nums , а затем вернуть его крайние правые \\(k\\) элементов; временная сложность такого метода равна \\(O(n \\log n)\\) .

        Очевидно, что этот способ делает слишком много, потому что нам нужно только найти наибольшие \\(k\\) элементов, а сортировать остальные элементы совсем не обязательно.

        Рисунок 8-7   Поиск наибольших k элементов через сортировку

        ","path":["Глава 8. Куча","8.3   Задача Top-k"],"tags":[]},{"location":"chapter_heap/top_k/#833-3","level":2,"title":"8.3.3   Метод 3: куча","text":"

        Задачу Top-k можно решить гораздо эффективнее с помощью кучи, как показано на рисунках ниже.

        1. Инициализировать минимальную кучу, у которой вершина содержит наименьший элемент.
        2. Сначала по очереди поместить в кучу первые \\(k\\) элементов массива.
        3. Начиная с элемента номер \\(k + 1\\) , если текущий элемент больше элемента на вершине кучи, то извлечь вершину кучи и поместить в кучу текущий элемент.
        4. После завершения обхода в куче будут храниться как раз наибольшие \\(k\\) элементов.
        <1><2><3><4><5><6><7><8><9>

        Рисунок 8-8   Поиск наибольших k элементов с помощью кучи

        Пример кода приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby top_k.py
        def top_k_heap(nums: list[int], k: int) -> list[int]:\n    \"\"\"Найти k наибольших элементов массива с помощью кучи\"\"\"\n    # Инициализация минимальной кучи\n    heap = []\n    # Поместить первые k элементов массива в кучу\n    for i in range(k):\n        heapq.heappush(heap, nums[i])\n    # Начиная с элемента k+1, поддерживать длину кучи равной k\n    for i in range(k, len(nums)):\n        # Если текущий элемент больше элемента на вершине кучи, извлечь вершину кучи и добавить текущий элемент в кучу\n        if nums[i] > heap[0]:\n            heapq.heappop(heap)\n            heapq.heappush(heap, nums[i])\n    return heap\n
        top_k.cpp
        /* Найти k наибольших элементов массива с помощью кучи */\npriority_queue<int, vector<int>, greater<int>> topKHeap(vector<int> &nums, int k) {\n    // Инициализация минимальной кучи\n    priority_queue<int, vector<int>, greater<int>> heap;\n    // Поместить первые k элементов массива в кучу\n    for (int i = 0; i < k; i++) {\n        heap.push(nums[i]);\n    }\n    // Начиная с элемента k+1, поддерживать длину кучи равной k\n    for (int i = k; i < nums.size(); i++) {\n        // Если текущий элемент больше элемента на вершине кучи, извлечь вершину кучи и добавить текущий элемент в кучу\n        if (nums[i] > heap.top()) {\n            heap.pop();\n            heap.push(nums[i]);\n        }\n    }\n    return heap;\n}\n
        top_k.java
        /* Найти k наибольших элементов массива с помощью кучи */\nQueue<Integer> topKHeap(int[] nums, int k) {\n    // Инициализация минимальной кучи\n    Queue<Integer> heap = new PriorityQueue<Integer>();\n    // Поместить первые k элементов массива в кучу\n    for (int i = 0; i < k; i++) {\n        heap.offer(nums[i]);\n    }\n    // Начиная с элемента k+1, поддерживать длину кучи равной k\n    for (int i = k; i < nums.length; i++) {\n        // Если текущий элемент больше элемента на вершине кучи, извлечь вершину кучи и добавить текущий элемент в кучу\n        if (nums[i] > heap.peek()) {\n            heap.poll();\n            heap.offer(nums[i]);\n        }\n    }\n    return heap;\n}\n
        top_k.cs
        /* Найти k наибольших элементов массива с помощью кучи */\nPriorityQueue<int, int> TopKHeap(int[] nums, int k) {\n    // Инициализация минимальной кучи\n    PriorityQueue<int, int> heap = new();\n    // Поместить первые k элементов массива в кучу\n    for (int i = 0; i < k; i++) {\n        heap.Enqueue(nums[i], nums[i]);\n    }\n    // Начиная с элемента k+1, поддерживать длину кучи равной k\n    for (int i = k; i < nums.Length; i++) {\n        // Если текущий элемент больше элемента на вершине кучи, извлечь вершину кучи и добавить текущий элемент в кучу\n        if (nums[i] > heap.Peek()) {\n            heap.Dequeue();\n            heap.Enqueue(nums[i], nums[i]);\n        }\n    }\n    return heap;\n}\n
        top_k.go
        /* Найти k наибольших элементов массива с помощью кучи */\nfunc topKHeap(nums []int, k int) *minHeap {\n    // Инициализация минимальной кучи\n    h := &minHeap{}\n    heap.Init(h)\n    // Поместить первые k элементов массива в кучу\n    for i := 0; i < k; i++ {\n        heap.Push(h, nums[i])\n    }\n    // Начиная с элемента k+1, поддерживать длину кучи равной k\n    for i := k; i < len(nums); i++ {\n        // Если текущий элемент больше элемента на вершине кучи, извлечь вершину кучи и добавить текущий элемент в кучу\n        if nums[i] > h.Top().(int) {\n            heap.Pop(h)\n            heap.Push(h, nums[i])\n        }\n    }\n    return h\n}\n
        top_k.swift
        /* Найти k наибольших элементов массива с помощью кучи */\nfunc topKHeap(nums: [Int], k: Int) -> [Int] {\n    // Инициализировать минимальную кучу и построить ее по первым k элементам\n    var heap = Heap(nums.prefix(k))\n    // Начиная с элемента k+1, поддерживать длину кучи равной k\n    for i in nums.indices.dropFirst(k) {\n        // Если текущий элемент больше элемента на вершине кучи, извлечь вершину кучи и добавить текущий элемент в кучу\n        if nums[i] > heap.min()! {\n            _ = heap.removeMin()\n            heap.insert(nums[i])\n        }\n    }\n    return heap.unordered\n}\n
        top_k.js
        /* Добавление элемента в кучу */\nfunction pushMinHeap(maxHeap, val) {\n    // Инвертировать знак элемента\n    maxHeap.push(-val);\n}\n\n/* Извлечение элемента из кучи */\nfunction popMinHeap(maxHeap) {\n    // Инвертировать знак элемента\n    return -maxHeap.pop();\n}\n\n/* Доступ к элементу на вершине кучи */\nfunction peekMinHeap(maxHeap) {\n    // Инвертировать знак элемента\n    return -maxHeap.peek();\n}\n\n/* Извлечь элементы из кучи */\nfunction getMinHeap(maxHeap) {\n    // Инвертировать знак элемента\n    return maxHeap.getMaxHeap().map((num) => -num);\n}\n\n/* Найти k наибольших элементов массива с помощью кучи */\nfunction topKHeap(nums, k) {\n    // Инициализация минимальной кучи\n    // Обратите внимание: мы инвертируем все элементы кучи, чтобы с помощью максимальной кучи имитировать минимальную\n    const maxHeap = new MaxHeap([]);\n    // Поместить первые k элементов массива в кучу\n    for (let i = 0; i < k; i++) {\n        pushMinHeap(maxHeap, nums[i]);\n    }\n    // Начиная с элемента k+1, поддерживать длину кучи равной k\n    for (let i = k; i < nums.length; i++) {\n        // Если текущий элемент больше элемента на вершине кучи, извлечь вершину кучи и добавить текущий элемент в кучу\n        if (nums[i] > peekMinHeap(maxHeap)) {\n            popMinHeap(maxHeap);\n            pushMinHeap(maxHeap, nums[i]);\n        }\n    }\n    // Вернуть элементы кучи\n    return getMinHeap(maxHeap);\n}\n
        top_k.ts
        /* Добавление элемента в кучу */\nfunction pushMinHeap(maxHeap: MaxHeap, val: number): void {\n    // Инвертировать знак элемента\n    maxHeap.push(-val);\n}\n\n/* Извлечение элемента из кучи */\nfunction popMinHeap(maxHeap: MaxHeap): number {\n    // Инвертировать знак элемента\n    return -maxHeap.pop();\n}\n\n/* Доступ к элементу на вершине кучи */\nfunction peekMinHeap(maxHeap: MaxHeap): number {\n    // Инвертировать знак элемента\n    return -maxHeap.peek();\n}\n\n/* Извлечь элементы из кучи */\nfunction getMinHeap(maxHeap: MaxHeap): number[] {\n    // Инвертировать знак элемента\n    return maxHeap.getMaxHeap().map((num: number) => -num);\n}\n\n/* Найти k наибольших элементов массива с помощью кучи */\nfunction topKHeap(nums: number[], k: number): number[] {\n    // Инициализация минимальной кучи\n    // Обратите внимание: мы инвертируем все элементы кучи, чтобы с помощью максимальной кучи имитировать минимальную\n    const maxHeap = new MaxHeap([]);\n    // Поместить первые k элементов массива в кучу\n    for (let i = 0; i < k; i++) {\n        pushMinHeap(maxHeap, nums[i]);\n    }\n    // Начиная с элемента k+1, поддерживать длину кучи равной k\n    for (let i = k; i < nums.length; i++) {\n        // Если текущий элемент больше элемента на вершине кучи, извлечь вершину кучи и добавить текущий элемент в кучу\n        if (nums[i] > peekMinHeap(maxHeap)) {\n            popMinHeap(maxHeap);\n            pushMinHeap(maxHeap, nums[i]);\n        }\n    }\n    // Вернуть элементы кучи\n    return getMinHeap(maxHeap);\n}\n
        top_k.dart
        /* Найти k наибольших элементов массива с помощью кучи */\nMinHeap topKHeap(List<int> nums, int k) {\n  // Инициализировать минимальную кучу, поместив в нее первые k элементов массива\n  MinHeap heap = MinHeap(nums.sublist(0, k));\n  // Начиная с элемента k+1, поддерживать длину кучи равной k\n  for (int i = k; i < nums.length; i++) {\n    // Если текущий элемент больше элемента на вершине кучи, извлечь вершину кучи и добавить текущий элемент в кучу\n    if (nums[i] > heap.peek()) {\n      heap.pop();\n      heap.push(nums[i]);\n    }\n  }\n  return heap;\n}\n
        top_k.rs
        /* Найти k наибольших элементов массива с помощью кучи */\nfn top_k_heap(nums: Vec<i32>, k: usize) -> BinaryHeap<Reverse<i32>> {\n    // BinaryHeap — это максимальная куча; с помощью Reverse элементы инвертируются, чтобы реализовать минимальную кучу\n    let mut heap = BinaryHeap::<Reverse<i32>>::new();\n    // Поместить первые k элементов массива в кучу\n    for &num in nums.iter().take(k) {\n        heap.push(Reverse(num));\n    }\n    // Начиная с элемента k+1, поддерживать длину кучи равной k\n    for &num in nums.iter().skip(k) {\n        // Если текущий элемент больше элемента на вершине кучи, извлечь вершину кучи и добавить текущий элемент в кучу\n        if num > heap.peek().unwrap().0 {\n            heap.pop();\n            heap.push(Reverse(num));\n        }\n    }\n    heap\n}\n
        top_k.c
        /* Добавление элемента в кучу */\nvoid pushMinHeap(MaxHeap *maxHeap, int val) {\n    // Инвертировать знак элемента\n    push(maxHeap, -val);\n}\n\n/* Извлечение элемента из кучи */\nint popMinHeap(MaxHeap *maxHeap) {\n    // Инвертировать знак элемента\n    return -pop(maxHeap);\n}\n\n/* Доступ к элементу на вершине кучи */\nint peekMinHeap(MaxHeap *maxHeap) {\n    // Инвертировать знак элемента\n    return -peek(maxHeap);\n}\n\n/* Извлечь элементы из кучи */\nint *getMinHeap(MaxHeap *maxHeap) {\n    // Инвертировать все элементы кучи и записать их в массив res\n    int *res = (int *)malloc(maxHeap->size * sizeof(int));\n    for (int i = 0; i < maxHeap->size; i++) {\n        res[i] = -maxHeap->data[i];\n    }\n    return res;\n}\n\n/* Извлечь элементы из кучи */\nint *getMinHeap(MaxHeap *maxHeap) {\n    // Инвертировать все элементы кучи и записать их в массив res\n    int *res = (int *)malloc(maxHeap->size * sizeof(int));\n    for (int i = 0; i < maxHeap->size; i++) {\n        res[i] = -maxHeap->data[i];\n    }\n    return res;\n}\n\n// Функция поиска k наибольших элементов массива на основе кучи\nint *topKHeap(int *nums, int sizeNums, int k) {\n    // Инициализация минимальной кучи\n    // Обратите внимание: мы инвертируем все элементы кучи, чтобы с помощью максимальной кучи имитировать минимальную\n    int *empty = (int *)malloc(0);\n    MaxHeap *maxHeap = newMaxHeap(empty, 0);\n    // Поместить первые k элементов массива в кучу\n    for (int i = 0; i < k; i++) {\n        pushMinHeap(maxHeap, nums[i]);\n    }\n    // Начиная с элемента k+1, поддерживать длину кучи равной k\n    for (int i = k; i < sizeNums; i++) {\n        // Если текущий элемент больше элемента на вершине кучи, извлечь вершину кучи и добавить текущий элемент в кучу\n        if (nums[i] > peekMinHeap(maxHeap)) {\n            popMinHeap(maxHeap);\n            pushMinHeap(maxHeap, nums[i]);\n        }\n    }\n    int *res = getMinHeap(maxHeap);\n    // Освободить память\n    delMaxHeap(maxHeap);\n    return res;\n}\n
        top_k.kt
        /* Найти k наибольших элементов массива с помощью кучи */\nfun topKHeap(nums: IntArray, k: Int): Queue<Int> {\n    // Инициализация минимальной кучи\n    val heap = PriorityQueue<Int>()\n    // Поместить первые k элементов массива в кучу\n    for (i in 0..<k) {\n        heap.offer(nums[i])\n    }\n    // Начиная с элемента k+1, поддерживать длину кучи равной k\n    for (i in k..<nums.size) {\n        // Если текущий элемент больше элемента на вершине кучи, извлечь вершину кучи и добавить текущий элемент в кучу\n        if (nums[i] > heap.peek()) {\n            heap.poll()\n            heap.offer(nums[i])\n        }\n    }\n    return heap\n}\n
        top_k.rb
        ### Поиск k наибольших элементов массива с помощью кучи ###\ndef top_k_heap(nums, k)\n  # Инициализация минимальной кучи\n  # Обратите внимание: мы инвертируем все элементы кучи, чтобы с помощью максимальной кучи имитировать минимальную\n  max_heap = MaxHeap.new([])\n\n  # Поместить первые k элементов массива в кучу\n  for i in 0...k\n    push_min_heap(max_heap, nums[i])\n  end\n\n  # Начиная с элемента k+1, поддерживать длину кучи равной k\n  for i in k...nums.length\n    # Если текущий элемент больше элемента на вершине кучи, извлечь вершину кучи и добавить текущий элемент в кучу\n    if nums[i] > peek_min_heap(max_heap)\n      pop_min_heap(max_heap)\n      push_min_heap(max_heap, nums[i])\n    end\n  end\n\n  get_min_heap(max_heap)\nend\n
        Визуализация кода

        Во весь экран >

        Всего выполняется \\(n\\) операций добавления и извлечения из кучи, а максимальная длина кучи равна \\(k\\) , поэтому временная сложность равна \\(O(n \\log k)\\) . Этот метод очень эффективен: когда \\(k\\) мало, временная сложность стремится к \\(O(n)\\) ; когда \\(k\\) велико, она все равно не превышает \\(O(n \\log n)\\) .

        Кроме того, этот метод подходит и для сценариев с динамическим потоком данных. При непрерывном поступлении новых данных мы можем продолжать поддерживать содержимое кучи, тем самым динамически обновляя наибольшие \\(k\\) элементов.

        ","path":["Глава 8. Куча","8.3   Задача Top-k"],"tags":[]},{"location":"chapter_hello_algo/","level":1,"title":"Перед началом","text":"

        Несколько лет назад я публиковал на LeetCode разборы серии задач \"Sword for Offer\" и получил поддержку и ободрение от многих читателей. Во время общения с ними мне чаще всего задавали один и тот же вопрос: \"как начать изучать алгоритмы?\" Постепенно этот вопрос начал меня по-настоящему занимать.

        Слепо бросаться в решение задач кажется самым популярным способом: он прост, прямолинеен и действительно работает. Но решение задач похоже на игру в \"Сапера\": люди с сильными навыками самообучения способны обезвредить мины одну за другой, а тем, у кого не хватает базы, легко набить себе шишки и шаг за шагом отступить под давлением неудач. Полностью проходить учебники тоже принято часто, но для тех, кто готовится к поиску работы, диплом, резюме, письменные тесты и собеседования уже отнимают большую часть сил, и потому толстые книги нередко превращаются в тяжелое испытание.

        Если ты тоже сталкиваешься с такими трудностями, то можно сказать, что эта книга сама \"нашла\" тебя. Она стала моим ответом на этот вопрос: пусть и не идеальным, но как минимум честной и активной попыткой. Эта книга сама по себе не гарантирует предложения о работе, но поможет тебе увидеть \"карту знаний\" по структурам данных и алгоритмам, понять форму, размер и расположение разных \"мин\" и освоить разные \"способы разминирования\". Освоив это, ты сможешь увереннее решать задачи и читать технические материалы, шаг за шагом выстраивая целостную систему знаний.

        Я глубоко согласен со словами профессора Фейнмана: \"Knowledge isn't free. You have to pay attention.\" В этом смысле книга не совсем \"бесплатна\". Чтобы не подвести то драгоценное \"внимание\", которое ты ей уделишь, я постараюсь вложить в ее создание максимум собственного \"внимания\".

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

        Hello, алгоритмы!

        Появление компьютеров радикально изменило мир. Благодаря высокой скорости вычислений и отличной программируемости они стали идеальной средой для исполнения алгоритмов и обработки данных. Реалистичная графика в играх, интеллектуальные решения в автономном вождении, впечатляющие партии AlphaGo и естественное взаимодействие ChatGPT: все это изящные проявления алгоритмов на компьютере.

        На самом деле еще до появления компьютеров алгоритмы и структуры данных уже существовали во всех уголках мира. Ранние алгоритмы были сравнительно простыми: например, древние способы счета или последовательности действий при изготовлении инструментов. По мере развития цивилизации алгоритмы становились тоньше и сложнее. За мастерством ремесленников, промышленными продуктами, освобождающими производительные силы, и даже за научными законами движения Вселенной почти всегда стоит изобретательная алгоритмическая мысль.

        Точно так же структуры данных встречаются повсюду: от социальных сетей до схем метро многие системы можно моделировать как \"граф\"; от государства до семьи основные формы общественной организации обладают свойствами \"дерева\"; зимняя одежда похожа на \"стек\", где то, что надевают первым, снимают последним; тубус для бадминтонных воланов похож на \"очередь\", где элементы добавляются с одного конца и извлекаются с другого; словарь похож на \"хеш-таблицу\", позволяющую быстро находить нужную статью.

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

        ","path":["Перед началом"],"tags":[]},{"location":"chapter_introduction/","level":1,"title":"Глава 1.   Введение в алгоритмы","text":"

        Abstract

        Юная девушка кружится в танце, переплетаясь с данными, а по подолу ее платья струится мелодия алгоритмов.

        Она приглашает вас присоединиться к танцу: следуйте за ее шагами и войдите в мир алгоритмов, полный логики и красоты.

        ","path":["Глава 1. Введение в алгоритмы","Глава 1.   Введение в алгоритмы"],"tags":[]},{"location":"chapter_introduction/#_1","level":2,"title":"Содержание главы","text":"
        • 1.1   Алгоритмы повсюду
        • 1.2   Что такое алгоритм
        • 1.3   Резюме
        ","path":["Глава 1. Введение в алгоритмы","Глава 1.   Введение в алгоритмы"],"tags":[]},{"location":"chapter_introduction/algorithms_are_everywhere/","level":1,"title":"1.1   Алгоритмы повсюду","text":"

        Говоря об алгоритмах, естественно вспомнить о математике. Однако на самом деле многие алгоритмы не связаны со сложной математикой, а больше полагаются на базовую логику, которая повсеместно встречается в нашей повседневной жизни.

        Прежде чем углубиться в обсуждение алгоритмов, стоит упомянуть интересный факт: вы уже точно освоили множество алгоритмов и привыкли применять их в повседневной жизни. Далее приведем несколько конкретных примеров, чтобы подтвердить этот факт.

        Пример 1: поиск в словаре. В словаре все слова упорядочены по алфавиту. Предположим, нам нужно найти слово, начинающееся на букву \\(r\\); обычно для этого нужно выполнить следующие действия.

        1. Откройте словарь примерно на половине страниц и посмотрите, какая буква является первой на этой странице; предположим, это буква \\(m\\).
        2. Поскольку в алфавите буква \\(r\\) идет после \\(m\\), исключаем первую половину словаря, и область поиска сужается до второй половины.
        3. Продолжайте повторять шаги 1. и 2. , пока не найдете страницу, где первой буквой слов будет \\(r\\).
        <1><2><3><4><5>

        Рисунок 1-1   Этапы поиска в словаре. Шаг 1

        Навык поиска в словаре, которым владеет каждый школьник, на самом деле является известным алгоритмом двоичного поиска. С точки зрения структуры данных словарь можно рассматривать как отсортированный массив; с точки зрения алгоритма последовательность операций по поиску в словаре можно считать двоичным поиском.

        Пример 2: упорядочивание карт. Во время игры в карты необходимо каждый раз упорядочивать карты в руке от меньшего к большему. Для этого нужно выполнить следующие действия.

        1. Разделите карты на упорядоченную и неупорядоченную части, предполагая, что изначально самая левая карта уже упорядочена.
        2. Из неупорядоченной части извлеките одну карту и вставьте ее в правильное место в упорядоченной части; после этого две самые левые карты станут упорядоченными.
        3. Повторяйте шаг 2. , каждый раз перемещая одну карту из неупорядоченной части в упорядоченную, пока все карты не станут упорядоченными.

        Рисунок 1-2   Этапы упорядочивания карт

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

        Пример 3: сдача. Предположим, что в супермаркете мы купили товар стоимостью \\(69\\) руб. и дали кассиру купюру в \\(100\\) руб. Кассир должен вернуть нам \\(31\\) руб. Для этого ему нужно выполнить следующие действия.

        1. Варианты выбора - это купюры номиналом меньше \\(31\\) руб. Пусть у нас имеются номиналы \\(1\\) , \\(5\\) , \\(10\\) и \\(20\\) руб.
        2. Возьмем самую крупную доступную купюру в \\(20\\) руб. Остаток сдачи составит \\(31 - 20 = 11\\) руб.
        3. Возьмем самую крупную из оставшихся купюр в \\(10\\) руб. Остаток составит \\(11 - 10 = 1\\) руб.
        4. Возьмем самую крупную из оставшихся купюр в \\(1\\) руб. Остаток составит \\(1 - 1 = 0\\) руб.
        5. Завершим выдачу сдачи, схема: \\(20 + 10 + 1 = 31\\) руб.

        Рисунок 1-3   Этапы выдачи сдачи

        В этих шагах мы на каждом этапе выбираем наилучший вариант, используя купюры наибольшего номинала, и в итоге получаем рабочую схему сдачи. С точки зрения структуры данных и алгоритмов этот метод по своей сути является жадным алгоритмом.

        От приготовления блюда до межзвездных путешествий решение практически любой задачи неразрывно связано с алгоритмами. Появление компьютеров позволило нам с помощью программирования хранить структуры данных в памяти, а также писать код для вызовов к CPU и GPU для выполнения алгоритмов. Таким образом, мы можем переносить задачи из реальной жизни в компьютер и решать различные сложные проблемы более эффективно.

        Tip

        Если представление о структурах данных, алгоритмах, массивах и двоичном поиске пока остается расплывчатым, просто продолжайте читать. Эта книга постепенно введет вас в мир структур данных и алгоритмов.

        ","path":["Глава 1. Введение в алгоритмы","1.1   Алгоритмы повсюду"],"tags":[]},{"location":"chapter_introduction/summary/","level":1,"title":"1.3   Резюме","text":"","path":["Глава 1. Введение в алгоритмы","1.3   Резюме"],"tags":[]},{"location":"chapter_introduction/summary/#1","level":3,"title":"1.   Ключевые выводы","text":"
        • Алгоритмы повсеместно присутствуют в нашей повседневной жизни и не являются недосягаемыми сложными знаниями. На самом деле мы уже освоили множество алгоритмов, которые помогают решать различные жизненные задачи.
        • Принцип поиска в словаре соответствует алгоритму двоичного поиска. Двоичный поиск иллюстрирует важную идею алгоритмов \"разделяй и властвуй\".
        • Процесс сортировки карт в колоде очень похож на алгоритм сортировки вставками, который хорошо подходит для сортировки небольших наборов данных.
        • Процесс размена по своей сути является жадным алгоритмом, в котором на каждом этапе принимается наилучшее на данный момент решение.
        • Алгоритм представляет собой набор инструкций или шагов, предназначенных для решения конкретной задачи в ограниченное время, а структура данных - это способ организации и хранения данных в компьютере.
        • Структуры данных и алгоритмы тесно связаны. Структуры данных являются основой для алгоритмов, а алгоритмы оживляют структуры данных.
        • Структуры данных и алгоритмы можно сравнить с конструктором: детали конструктора представляют данные, их форма и способы соединения - структуры данных, а этапы сборки конструктора соответствуют алгоритмам.
        ","path":["Глава 1. Введение в алгоритмы","1.3   Резюме"],"tags":[]},{"location":"chapter_introduction/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

        Q: Я программист и в повседневной работе никогда не использовал алгоритмы для решения задач, поскольку часто используемые алгоритмы уже встроены в языки программирования и ими можно пользоваться напрямую. Значит ли это, что рабочие задачи еще не требуют применения алгоритмов?

        Если сравнить конкретные профессиональные навыки с приемами в боевых искусствах, то базовые дисциплины скорее напоминают \"внутреннюю силу\".

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

        • Если бы мы не изучали структуры данных и алгоритмы, то, получив любые данные, возможно, просто передали бы их этой функции сортировки. Все работает гладко, производительность хорошая, и на первый взгляд проблем нет.
        • Однако если мы изучили алгоритмы, то знаем, что временная сложность встроенной функции сортировки составляет \\(O(n \\log n)\\) ; если же данные представлены целыми числами фиксированной разрядности, например номерами студентов, то можно использовать более эффективный метод поразрядной сортировки, снизив временную сложность до \\(O(nk)\\) , где \\(k\\) - это количество разрядов, а при больших объемах данных выиграть во времени, затратах и пользовательском опыте.

        В инженерной практике множество задач трудно решить оптимальным образом, и многие из них решаются \"как-то\". Сложность задачи зависит как от ее природы, так и от уровня знаний и опыта человека, который ее анализирует. Чем более полными знаниями и большим опытом обладает человек, тем глубже он может проанализировать проблему и тем изящнее может быть ее решение.

        ","path":["Глава 1. Введение в алгоритмы","1.3   Резюме"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/","level":1,"title":"1.2   Что такое алгоритм","text":"","path":["Глава 1. Введение в алгоритмы","1.2   Что такое алгоритм"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/#121","level":2,"title":"1.2.1   Определение алгоритма","text":"

        Алгоритм (algorithm) - это набор инструкций или шагов, предназначенных для решения конкретной задачи за ограниченное время. Он обладает следующими свойствами.

        • Задача четко определена и включает ясные определения входных и выходных данных.
        • Обладает осуществимостью и может быть выполнен за ограниченное количество шагов, времени и памяти.
        • Каждый шаг имеет определенное значение, и при одинаковых входных данных и условиях выполнения результат всегда будет одинаковым.
        ","path":["Глава 1. Введение в алгоритмы","1.2   Что такое алгоритм"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/#122","level":2,"title":"1.2.2   Определение структуры данных","text":"

        Структура данных (data structure) - это способ организации и хранения данных, включающий содержимое данных, их взаимосвязи и методы операций с ними. Структура данных преследует следующие цели.

        • Минимизировать занимаемое пространство для экономии памяти компьютера.
        • Обеспечивать максимально быструю обработку данных, включая доступ, добавление, удаление и обновление данных.
        • Обеспечивать простое представление данных и логическую информацию для эффективного выполнения алгоритмов.

        Проектирование структуры данных - это процесс, полный компромиссов. Если вы хотите улучшить один аспект, часто приходится идти на уступки в другом. Приведем два примера.

        • Связный список, по сравнению с массивом, более удобен для добавления и удаления данных, но имеет проблемы со скоростью доступа к данным.
        • Граф, по сравнению со связным списком, предоставляет более богатую логическую информацию, но требует большего объема памяти.
        ","path":["Глава 1. Введение в алгоритмы","1.2   Что такое алгоритм"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/#123","level":2,"title":"1.2.3   Связь между структурами данных и алгоритмами","text":"

        Как показано на рисунке 1-4, структуры данных и алгоритмы тесно взаимосвязаны, что проявляется в следующих трех аспектах.

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

        Рисунок 1-4   Связь между структурами данных и алгоритмами

        Структуры данных и алгоритмы подобны конструктору, как показано на рисунке 1-5. Комплект конструктора, помимо множества деталей, содержит также подробную инструкцию по сборке. Следуя этой инструкции шаг за шагом, можно собрать красивую модель.

        Рисунок 1-5   Сборка конструктора

        Подробное описание аналогии с конструктором представлено в таблице 1-1.

        Таблица 1-1   Сравнение структур данных и алгоритмов с конструктором

        Структуры данных и алгоритмы Конструктор Входные данные Несобранные детали конструктора Структура данных Организация деталей конструктора, включая форму, размер, способы соединения и т. д. Алгоритм Последовательность действий по сборке деталей в целевую модель Выходные данные Собранная модель конструктора

        Стоит отметить, что структуры данных и алгоритмы не зависят от языка программирования. Именно поэтому данная книга предлагает их реализации на различных языках.

        Принятое сокращение

        В реальных обсуждениях выражение \"структуры данных и алгоритмы\" обычно сокращают до просто \"алгоритмы\". Например, хорошо известные задачи LeetCode на деле одновременно проверяют знания и по структурам данных, и по алгоритмам.

        ","path":["Глава 1. Введение в алгоритмы","1.2   Что такое алгоритм"],"tags":[]},{"location":"chapter_preface/","level":1,"title":"Глава 0.   Предисловие","text":"

        Abstract

        Алгоритмы подобны прекрасной симфонии, а каждая строка кода льется подобно мелодии.

        Пусть эта книга тихо зазвучит в вашем сознании и оставит после себя особую и глубокую мелодию.

        ","path":["Глава 0. Предисловие","Глава 0.   Предисловие"],"tags":[]},{"location":"chapter_preface/#_1","level":2,"title":"Содержание главы","text":"
        • 0.1   Об этой книге
        • 0.2   Как пользоваться этой книгой
        • 0.3   Резюме
        ","path":["Глава 0. Предисловие","Глава 0.   Предисловие"],"tags":[]},{"location":"chapter_preface/about_the_book/","level":1,"title":"0.1   Об этой книге","text":"

        Этот проект задуман как открытое, бесплатное и дружелюбное к новичкам введение в структуры данных и алгоритмы.

        • В книге используются анимированные иллюстрации: материал изложен ясно и последовательно, что облегчает освоение и помогает начинающим выстроить карту знаний по структурам данных и алгоритмам.
        • Исходный код можно запустить одним нажатием, что позволяет тренироваться, развивать навыки программирования и понимать принципы работы алгоритмов и реализации структур данных на фундаментальном уровне.
        • Мы призываем читателей к взаимопомощи: задавайте вопросы и делитесь идеями в комментариях. Обсуждения помогают двигаться вперед всем вместе.
        ","path":["Глава 0. Предисловие","0.1   Об этой книге"],"tags":[]},{"location":"chapter_preface/about_the_book/#011","level":2,"title":"0.1.1   Целевая аудитория","text":"

        Если вы новичок в алгоритмах, никогда с ними не сталкивались или уже имеете некоторый опыт решения задач, но еще не обладаете четким пониманием структур данных и алгоритмов, эта книга создана специально для вас!

        Если у вас уже есть определенный опыт решения задач и вы знакомы с большинством типов задач, эта книга поможет вам освежить и систематизировать знания об алгоритмах, а исходный код может служить набором инструментов для решения задач или алгоритмическим словарем.

        Если вы владеете алгоритмами на экспертном уровне, мы будем рады вашим ценным советам или совместному участию в создании книги.

        Предварительные требования

        Необходимо иметь хотя бы базовую подготовку в одном из языков программирования, чтобы читать и писать простой код.

        ","path":["Глава 0. Предисловие","0.1   Об этой книге"],"tags":[]},{"location":"chapter_preface/about_the_book/#012","level":2,"title":"0.1.2   Структура содержания","text":"

        Основное содержание книги представлено на рисунке 0-1.

        • Анализ сложности: критерии и методы оценки структур данных и алгоритмов. Методы расчета временной и пространственной сложности, распространенные типы, примеры и т. д.
        • Структуры данных: классификация основных типов данных и структур данных. Определение, преимущества и недостатки, основные операции, распространенные типы, типичные приложения и методы реализации массивов, списков, стеков, очередей, хеш-таблиц, деревьев, куч и графов.
        • Алгоритмы: определение, преимущества и недостатки, эффективность, области применения, этапы решения и примеры задач для поиска, сортировки, алгоритма \"разделяй и властвуй\", поиска с возвратом, динамического программирования и жадных алгоритмов.

        Рисунок 0-1   Основное содержание книги

        ","path":["Глава 0. Предисловие","0.1   Об этой книге"],"tags":[]},{"location":"chapter_preface/about_the_book/#013","level":2,"title":"0.1.3   Благодарности","text":"

        Эта книга постоянно совершенствуется благодаря совместным усилиям множества участников открытого сообщества. Благодарим каждого автора, вложившего свое время и силы; их имена перечислены в порядке, автоматически сгенерированном GitHub: krahets, coderonion, Gonglja, nuomi1, Reanon, justin-tse, hpstory, danielsss, curtishd, night-cruise, S-N-O-R-L-A-X, rongyi, msk397, gvenusleo, khoaxuantu, rivertwilight, K3v123, gyt95, zhuoqinyue, yuelinxin, Zuoxun, mingXta, Phoenix0415, FangYuan33, GN-Yu, longsizhuo, pengchzn, QiLOL, Cathay-Chen, guowei-gong, xBLACKICEx, IsChristina, JoseHung, qualifier1024, hello-ikun, magentaqin, Guanngxu, thomasq0, sunshinesDL, L-Super, Transmigration-zhou, WSL0809, Slone123c, lhxsm, yuan0221, what-is-me, theNefelibatas, Shyam-Chen, sangxiaai, longranger2, codeberg-user, xiongsp, JeffersonHuang, prinpal, seven1240, Wonderdch, malone6, xiaomiusa87, gaofer, bluebean-cloud, a16su, SamJin98, hongyun-robot, nanlei, XiaChuerwu, yd-j, iron-irax, mgisr, steventimes, junminhong, heshuyue, danny900714, Nigh, Dr-XYZ, MolDuM, XC-Zero, reeswell, PXG-XPG, NI-SW, Horbin-Magician, Enlightenus, YangXuanyi, xjr7670, beatrix-chan, DullSword, qq909244296, iStig, boloboloda, hts0000, gledfish, fbigm, echo1937, jiaxianhua, wenjianmin, keshida, kilikilikid, lclc6, lwbaptx, linyejoe2, liuxjerry, szu17dmy, dshlstarr, Yucao-cy, coderlef, czruby, bongbongbakudan, beintentional, ZongYangL, ZhongYuuu, ZhongGuanbin, hezhizhen, linzeyan, ZJKung, JTCPOWI, KawaiiAsh, luluxia, xb534, ztkuaikuai, yw-1021, ElaBosak233, baagod, zhouLion, yishangzhang, yi427, yanedie, yabo083, weibk, wangwang105, th1nk3r-ing, tao363, 4yDX3906, syd168, sslmj2020, smilelsb, siqyka, selear, sdshaoda, Xi-Row, popozhu, nuquist19, noobcodemaker, XiaoK29, chadyi, lyl625760, lucaswangdev, llql1211, 0130w, shanghai-Jerry, EJackYang, Javesun99, eltociear, lipusheng, KNChiu, BlindTerran, ShiMaRing, lovelock, FreddieLi, FloranceYeh, fanchenggang, gltianwen, goerll, nedchu, curly210102, CuB3y0nd, KraHsu, CarrotDLaw, youshaoXG, bubble9um, Asashishi, Asa0oo0o0o, fanenr, eagleanurag, akshiterate, 52coder, foursevenlove, KorsChen, hopkings2008, yang-le, realwujing, Evilrabbit520, Umer-Jahangir, Turing-1024-Lee, Suremotoo, paoxiaomooo, Chieko-Seren, Senrian, Allen-Scai, 19santosh99, ymmmas, Risuntsy, Richard-Zhang1019, RafaelCaso, qingpeng9802, primexiao, Urbaner3, codetypess, nidhoggfgg, MwumLi, CreatorMetaSky, martinx, ZnYang2018, hugtyftg, logan-qiu, psychelzh, Kunchen-Luo, Keynman и KeiichiKasai.

        Рецензирование кода книги выполнили coderonion, curtishd, Gonglja, gvenusleo, hpstory, justin-tse, khoaxuantu, krahets, night-cruise, nuomi1, Reanon и rongyi (в алфавитном порядке). Благодарим их за потраченное время и силы, которые обеспечили стандартизацию и единообразие кода на различных языках.

        Английскую версию книги вычитали yuelinxin, K3v123, magentaqin, QiLOL, Phoenix0415, SamJin98, yanedie, RafaelCaso, pengchzn и thomasq0; японскую версию - eltociear; русскую версию - И. А. Шевкун и Yuyan Huang; традиционную китайскую версию - Shyam-Chen и Dr-XYZ. Именно благодаря их вкладу эта книга может служить более широкому кругу читателей, и мы искренне благодарим их.

        Инструмент генерации ePub-версии этой книги разработал zhongfq. Благодарим его за вклад, который дал читателям более гибкий способ чтения.

        В процессе создания этой книги мне помогало много людей.

        • Благодарю моего наставника в компании, доктора Ли Си: в одной из бесед вы вдохновили меня быстрее начать, что укрепило мою решимость написать эту книгу;
        • Благодарю мою девушку Bubble, первого читателя этой книги: с позиции новичка в алгоритмах она дала много ценных советов, благодаря которым книга стала более понятной и доступной;
        • Благодарю Tengbao, Qibao и Feibao за креативное название книги, которое навевает приятные воспоминания о первой строке кода \"Hello World!\";
        • Благодарю Xiaoquan за профессиональную помощь в вопросах интеллектуальной собственности, что сыграло важную роль в совершенствовании этой открытой книги;
        • Благодарю Sutong за дизайн обложки и логотипа книги, а также за терпение при многочисленных исправлениях по моим просьбам;
        • Благодарю @squidfunk за советы по оформлению и за разработку открытой темы документации Material-for-MkDocs.

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

        Эта книга пропагандирует метод обучения, сочетающий умственную и практическую деятельность; в этом отношении на меня сильно повлияла Dive into Deep Learning. Я настоятельно рекомендую эту замечательную работу всем читателям.

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

        ","path":["Глава 0. Предисловие","0.1   Об этой книге"],"tags":[]},{"location":"chapter_preface/suggestions/","level":1,"title":"0.2   Как пользоваться этой книгой","text":"

        Tip

        Для получения наилучшего опыта чтения рекомендуется полностью прочитать этот раздел.

        ","path":["Глава 0. Предисловие","0.2   Как пользоваться этой книгой"],"tags":[]},{"location":"chapter_preface/suggestions/#021","level":2,"title":"0.2.1   Соглашения о стиле изложения","text":"
        • Главы, помеченные * в заголовке, являются дополнительными и содержат более сложный материал. Если времени мало, их можно пропустить.
        • Профессиональные термины выделяются полужирным шрифтом в печатной и PDF-версии или подчеркиванием в веб-версии, например массив (array). Рекомендуется запоминать их для удобства чтения литературы.
        • Важные моменты и обобщающие фразы будут выделяться полужирным шрифтом, и на такие тексты следует обращать особое внимание.
        • Слова и выражения со специальным смыслом будут отмечаться \"кавычками\", чтобы избежать неоднозначности.
        • Когда термины различаются между языками программирования, в качестве стандарта используется Python; например, None применяется для обозначения \"пустого\" значения.
        • В некоторых местах книга отходит от стандартов комментирования программного кода ради более компактного оформления. Комментарии в основном делятся на три типа: заголовочные, содержательные и многострочные.
        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
        \"\"\"Комментарий-заголовок: используется для обозначения функций, классов, тестовых примеров и т. п.\"\"\"\n\n# Содержательный комментарий: подробно поясняет код\n\n\"\"\"\nМногострочный\nкомментарий\n\"\"\"\n
        /* Комментарий-заголовок: используется для обозначения функций, классов, тестовых примеров и т. п. */\n\n// Содержательный комментарий: подробно поясняет код\n\n/**\n * Многострочный\n * комментарий\n */\n
        /* Комментарий-заголовок: используется для обозначения функций, классов, тестовых примеров и т. п. */\n\n// Содержательный комментарий: подробно поясняет код\n\n/**\n * Многострочный\n * комментарий\n */\n
        /* Комментарий-заголовок: используется для обозначения функций, классов, тестовых примеров и т. п. */\n\n// Содержательный комментарий: подробно поясняет код\n\n/**\n * Многострочный\n * комментарий\n */\n
        /* Комментарий-заголовок: используется для обозначения функций, классов, тестовых примеров и т. п. */\n\n// Содержательный комментарий: подробно поясняет код\n\n/**\n * Многострочный\n * комментарий\n */\n
        /* Комментарий-заголовок: используется для обозначения функций, классов, тестовых примеров и т. п. */\n\n// Содержательный комментарий: подробно поясняет код\n\n/**\n * Многострочный\n * комментарий\n */\n
        /* Комментарий-заголовок: используется для обозначения функций, классов, тестовых примеров и т. п. */\n\n// Содержательный комментарий: подробно поясняет код\n\n/**\n * Многострочный\n * комментарий\n */\n
        /* Комментарий-заголовок: используется для обозначения функций, классов, тестовых примеров и т. п. */\n\n// Содержательный комментарий: подробно поясняет код\n\n/**\n * Многострочный\n * комментарий\n */\n
        /* Комментарий-заголовок: используется для обозначения функций, классов, тестовых примеров и т. п. */\n\n// Содержательный комментарий: подробно поясняет код\n\n/**\n * Многострочный\n * комментарий\n */\n
        /* Комментарий-заголовок: используется для обозначения функций, классов, тестовых примеров и т. п. */\n\n// Содержательный комментарий: подробно поясняет код\n\n// Многострочный\n// комментарий\n
        /* Комментарий-заголовок: используется для обозначения функций, классов, тестовых примеров и т. п. */\n\n// Содержательный комментарий: подробно поясняет код\n\n/**\n * Многострочный\n * комментарий\n */\n
        /* Комментарий-заголовок: используется для обозначения функций, классов, тестовых примеров и т. п. */\n\n// Содержательный комментарий: подробно поясняет код\n\n/**\n * Многострочный\n * комментарий\n */\n
        ### Комментарий-заголовок: используется для обозначения функций, классов, тестовых примеров и т. п. ###\n\n# Содержательный комментарий: подробно поясняет код\n\n# Многострочный\n# комментарий\n
        ","path":["Глава 0. Предисловие","0.2   Как пользоваться этой книгой"],"tags":[]},{"location":"chapter_preface/suggestions/#022","level":2,"title":"0.2.2   Эффективное обучение с помощью анимированных иллюстраций","text":"

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

        Если во время чтения вы встречаете фрагмент с анимированной иллюстрацией, как на рисунке 0-2, используйте иллюстрацию в качестве основного источника информации, а текст - в качестве вспомогательного, объединяя оба источника для понимания материала.

        Рисунок 0-2   Пример анимированной иллюстрации

        ","path":["Глава 0. Предисловие","0.2   Как пользоваться этой книгой"],"tags":[]},{"location":"chapter_preface/suggestions/#023","level":2,"title":"0.2.3   Углубление понимания через практику кода","text":"

        Сопроводительный код этой книги размещен в репозитории GitHub. Как показано ниже, исходный код содержит тестовые примеры и может быть запущен одним нажатием кнопки.

        Если позволяет время, рекомендуется самостоятельно набирать код. Если времени на обучение мало, по крайней мере просмотрите и выполните весь код.

        Процесс написания кода приносит больше пользы, чем его чтение. Настоящее обучение - это обучение на практике.

        Рисунок 0-3   Пример запуска кода

        Подготовка к запуску кода в основном состоит из трех этапов.

        Шаг 1: установка локальной среды программирования. Воспользуйтесь руководством из приложения. Если среда уже установлена, этот шаг можно пропустить.

        Шаг 2: клонирование или загрузка репозитория кода. Перейдите в репозиторий GitHub. Если у вас уже установлен Git, репозиторий можно клонировать следующей командой:

        git clone https://github.com/krahets/hello-algo.git\n

        Также можно нажать кнопку \"Download ZIP\" в месте, показанном на рисунке 0-4, напрямую скачать архив с кодом и затем распаковать его локально.

        Рисунок 0-4   Клонирование репозитория и загрузка кода

        Шаг 3: запуск исходного кода. Как показано на рисунке 0-5, для блоков кода, у которых сверху указано имя файла, соответствующий исходный файл можно найти в папке codes репозитория. Исходные файлы запускаются одним нажатием, что помогает не тратить лишнее время на отладку и сосредоточиться на изучении материала.

        Рисунок 0-5   Блоки кода и соответствующие исходные файлы

        Помимо локального запуска, веб-версия также поддерживает визуальное выполнение Python-кода (на базе pythontutor). Как показано ниже, можно нажать \"Визуализировать выполнение\" под блоком кода, чтобы раскрыть окно и наблюдать за выполнением алгоритма; также можно нажать \"Полноэкранный режим\" для более удобного просмотра.

        Рисунок 0-6   Визуальный запуск Python-кода

        ","path":["Глава 0. Предисловие","0.2   Как пользоваться этой книгой"],"tags":[]},{"location":"chapter_preface/suggestions/#024","level":2,"title":"0.2.4   Совместный рост через вопросы и обсуждения","text":"

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

        Как показано на рисунке 0-7, в веб-версии у каждой главы внизу есть раздел комментариев. Рекомендуется уделять внимание его содержанию. С одной стороны, это поможет увидеть, с какими трудностями сталкиваются другие читатели, восполнить пробелы и подтолкнуть себя к более глубокому пониманию. С другой стороны, мы надеемся, что вы будете отвечать на вопросы других участников и делиться своими мнениями.

        Рисунок 0-7   Пример раздела комментариев

        ","path":["Глава 0. Предисловие","0.2   Как пользоваться этой книгой"],"tags":[]},{"location":"chapter_preface/suggestions/#025","level":2,"title":"0.2.5   Дорожная карта изучения алгоритмов","text":"

        В целом процесс изучения структур данных и алгоритмов можно разделить на три этапа.

        1. Этап 1: введение в алгоритмы. Необходимо познакомиться с особенностями и применением различных структур данных, изучить принципы, процессы, назначение и эффективность различных алгоритмов.
        2. Этап 2: решение алгоритмических задач. Рекомендуется начинать с популярных задач и решить не менее 100 из них, чтобы познакомиться с основными алгоритмическими проблемами. При первых попытках \"забывание знаний\" может стать испытанием, но это нормально. Следуйте при повторении задач \"кривой забывания Эббингауза\", и обычно после 3-5 циклов повторения материал хорошо запоминается. Рекомендуемые списки задач и планы практики см. в этом репозитории GitHub.
        3. Этап 3: построение системы знаний. В процессе обучения можно читать статьи по алгоритмам, изучать каркасы решений и учебники, чтобы постоянно обогащать свою систему знаний. В решении задач можно применять продвинутые стратегии, например классификацию по темам, несколько решений одной задачи или одно решение для нескольких задач; соответствующий опыт можно найти в различных сообществах.

        Как показано на рисунке 0-8, содержание этой книги в основном охватывает \"этап 1\" и призвано помочь вам более эффективно перейти к обучению на этапах 2 и 3.

        Рисунок 0-8   Дорожная карта изучения алгоритмов

        ","path":["Глава 0. Предисловие","0.2   Как пользоваться этой книгой"],"tags":[]},{"location":"chapter_preface/summary/","level":1,"title":"0.3   Резюме","text":"","path":["Глава 0. Предисловие","0.3   Резюме"],"tags":[]},{"location":"chapter_preface/summary/#1","level":3,"title":"1.   Ключевые выводы","text":"
        • Основная аудитория этой книги - новички в изучении алгоритмов. Если у вас уже есть определенная база, книга поможет систематизировать знания, а исходный код послужит инструментальной библиотекой для решения задач.
        • Содержание книги включает три основные части - анализ сложности, структуры данных и алгоритмы - и охватывает большинство тем в этой области.
        • Для новичков в алгоритмах крайне важно изучить начальные разделы книги, чтобы избежать множества ошибок в будущем.
        • Анимированные иллюстрации в книге обычно используются для представления ключевых и сложных аспектов. При чтении книги следует уделять этим материалам больше внимания.
        • Практика - лучший способ изучения программирования. Настоятельно рекомендуется запускать исходный код и самостоятельно писать программы.
        • В веб-версии книги каждая глава имеет область комментариев, где можно задавать вопросы и делиться своими мыслями.
        ","path":["Глава 0. Предисловие","0.3   Резюме"],"tags":[]},{"location":"chapter_reference/","level":1,"title":"Список литературы","text":"

        [1] Thomas H. Cormen и др. Introduction to Algorithms (3rd Edition).

        [2] Aditya Bhargava. Grokking Algorithms: An Illustrated Guide for Programmers and Other Curious People (1st Edition).

        [3] Robert Sedgewick и др. Algorithms (4th Edition).

        [4] Yan Weimin. Data Structures (C Language Edition).

        [5] Deng Junhui. Data Structures (C++ Language Edition, 3rd Edition).

        [6] Mark Allen Weiss; пер. Chen Yue. Data Structures and Algorithm Analysis: Java Description (3rd Edition).

        [7] Cheng Jie. A Plainspoken Guide to Data Structures.

        [8] Wang Zheng. The Beauty of Data Structures and Algorithms.

        [9] Gayle Laakmann McDowell. Cracking the Coding Interview: 189 Programming Questions and Solutions (6th Edition).

        [10] Aston Zhang и др. Dive into Deep Learning.

        ","path":["Список литературы"],"tags":[]},{"location":"chapter_searching/","level":1,"title":"Глава 10.   Поиск","text":"

        Abstract

        Поиск - это движение в неизвестность: иногда приходится пройти каждый уголок пространства, а иногда удается быстро найти цель.

        В этом пути каждый новый шаг может привести к ответу, которого мы не ожидали.

        ","path":["Глава 10. Поиск","Глава 10.   Поиск"],"tags":[]},{"location":"chapter_searching/#_1","level":2,"title":"Содержание главы","text":"
        • 10.1   Двоичный поиск
        • 10.2   Двоичный поиск точки вставки
        • 10.3   Двоичный поиск границ
        • 10.4   Стратегии оптимизации хеширования
        • 10.5   Переосмысление алгоритмов поиска
        • 10.6   Резюме
        ","path":["Глава 10. Поиск","Глава 10.   Поиск"],"tags":[]},{"location":"chapter_searching/binary_search/","level":1,"title":"10.1   Двоичный поиск","text":"

        Двоичный поиск (binary search) - это эффективный алгоритм поиска, основанный на стратегии \"разделяй и властвуй\". Он использует упорядоченность данных, сокращая на каждом шаге область поиска вдвое, пока не будет найден целевой элемент или пока интервал поиска не опустеет.

        Question

        Дан массив nums длины \\(n\\), элементы которого расположены в порядке возрастания и не повторяются. Найдите и верните индекс элемента target в этом массиве. Если массив не содержит этого элемента, верните \\(-1\\) . Пример показан на рисунке 10-1.

        Рисунок 10-1   Пример данных для двоичного поиска

        Как показано на рисунке 10-2, сначала инициализируем указатели \\(i = 0\\) и \\(j = n - 1\\) , которые указывают на первый и последний элементы массива и задают интервал поиска \\([0, n - 1]\\) . Обратите внимание: квадратные скобки обозначают замкнутый интервал и включают граничные значения.

        Далее в цикле выполняются следующие два шага.

        1. Вычислить индекс середины \\(m = \\lfloor {(i + j) / 2} \\rfloor\\) , где \\(\\lfloor \\: \\rfloor\\) означает операцию округления вниз.
        2. Сравнить nums[m] и target , после чего возможны три случая.
          1. Если nums[m] < target , это означает, что target находится в интервале \\([m + 1, j]\\) , поэтому выполняется \\(i = m + 1\\) .
          2. Если nums[m] > target , это означает, что target находится в интервале \\([i, m - 1]\\) , поэтому выполняется \\(j = m - 1\\) .
          3. Если nums[m] = target , значит, элемент target найден, поэтому возвращается индекс \\(m\\) .

        Если массив не содержит целевой элемент, область поиска в итоге сузится до пустого интервала. В этом случае возвращается \\(-1\\) .

        <1><2><3><4><5><6><7>

        Рисунок 10-2   Процесс двоичного поиска

        Стоит отметить, что поскольку и \\(i\\) , и \\(j\\) имеют тип int , то сумма \\(i + j\\) может выйти за пределы диапазона типа int. Чтобы избежать переполнения, обычно используют формулу \\(m = \\lfloor {i + (j - i) / 2} \\rfloor\\) для вычисления середины.

        Код приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search.py
        def binary_search(nums: list[int], target: int) -> int:\n    \"\"\"Бинарный поиск (двусторонне замкнутый интервал)\"\"\"\n    # Инициализировать двусторонне замкнутый интервал [0, n-1], то есть i и j указывают на первый и последний элементы массива соответственно\n    i, j = 0, len(nums) - 1\n    # Цикл завершается, когда диапазон поиска пуст (при i > j диапазон пуст)\n    while i <= j:\n        # Теоретически числа в Python могут быть сколь угодно большими (ограничены только объемом памяти), поэтому не нужно учитывать переполнение больших чисел\n        m = (i + j) // 2  # Вычислить индекс середины m\n        if nums[m] < target:\n            i = m + 1  # Это означает, что target находится в интервале [m+1, j]\n        elif nums[m] > target:\n            j = m - 1  # Это означает, что target находится в интервале [i, m-1]\n        else:\n            return m  # Целевой элемент найден, вернуть его индекс\n    return -1  # Целевой элемент не найден, вернуть -1\n
        binary_search.cpp
        /* Бинарный поиск (двусторонне замкнутый интервал) */\nint binarySearch(vector<int> &nums, int target) {\n    // Инициализировать двусторонне замкнутый интервал [0, n-1], то есть i и j указывают на первый и последний элементы массива соответственно\n    int i = 0, j = nums.size() - 1;\n    // Цикл завершается, когда диапазон поиска пуст (при i > j диапазон пуст)\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Вычислить индекс середины m\n        if (nums[m] < target)    // Это означает, что target находится в интервале [m+1, j]\n            i = m + 1;\n        else if (nums[m] > target) // Это означает, что target находится в интервале [i, m-1]\n            j = m - 1;\n        else // Целевой элемент найден, вернуть его индекс\n            return m;\n    }\n    // Целевой элемент не найден, вернуть -1\n    return -1;\n}\n
        binary_search.java
        /* Бинарный поиск (двусторонне замкнутый интервал) */\nint binarySearch(int[] nums, int target) {\n    // Инициализировать двусторонне замкнутый интервал [0, n-1], то есть i и j указывают на первый и последний элементы массива соответственно\n    int i = 0, j = nums.length - 1;\n    // Цикл завершается, когда диапазон поиска пуст (при i > j диапазон пуст)\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Вычислить индекс середины m\n        if (nums[m] < target) // Это означает, что target находится в интервале [m+1, j]\n            i = m + 1;\n        else if (nums[m] > target) // Это означает, что target находится в интервале [i, m-1]\n            j = m - 1;\n        else // Целевой элемент найден, вернуть его индекс\n            return m;\n    }\n    // Целевой элемент не найден, вернуть -1\n    return -1;\n}\n
        binary_search.cs
        /* Бинарный поиск (двусторонне замкнутый интервал) */\nint BinarySearch(int[] nums, int target) {\n    // Инициализировать двусторонне замкнутый интервал [0, n-1], то есть i и j указывают на первый и последний элементы массива соответственно\n    int i = 0, j = nums.Length - 1;\n    // Цикл завершается, когда диапазон поиска пуст (при i > j диапазон пуст)\n    while (i <= j) {\n        int m = i + (j - i) / 2;   // Вычислить индекс середины m\n        if (nums[m] < target)      // Это означает, что target находится в интервале [m+1, j]\n            i = m + 1;\n        else if (nums[m] > target) // Это означает, что target находится в интервале [i, m-1]\n            j = m - 1;\n        else                       // Целевой элемент найден, вернуть его индекс\n            return m;\n    }\n    // Целевой элемент не найден, вернуть -1\n    return -1;\n}\n
        binary_search.go
        /* Бинарный поиск (двусторонне замкнутый интервал) */\nfunc binarySearch(nums []int, target int) int {\n    // Инициализировать двусторонне замкнутый интервал [0, n-1], то есть i и j указывают на первый и последний элементы массива соответственно\n    i, j := 0, len(nums)-1\n    // Цикл завершается, когда диапазон поиска пуст (при i > j диапазон пуст)\n    for i <= j {\n        m := i + (j-i)/2      // Вычислить индекс середины m\n        if nums[m] < target { // Это означает, что target находится в интервале [m+1, j]\n            i = m + 1\n        } else if nums[m] > target { // Это означает, что target находится в интервале [i, m-1]\n            j = m - 1\n        } else { // Целевой элемент найден, вернуть его индекс\n            return m\n        }\n    }\n    // Целевой элемент не найден, вернуть -1\n    return -1\n}\n
        binary_search.swift
        /* Бинарный поиск (двусторонне замкнутый интервал) */\nfunc binarySearch(nums: [Int], target: Int) -> Int {\n    // Инициализировать двусторонне замкнутый интервал [0, n-1], то есть i и j указывают на первый и последний элементы массива соответственно\n    var i = nums.startIndex\n    var j = nums.endIndex - 1\n    // Цикл завершается, когда диапазон поиска пуст (при i > j диапазон пуст)\n    while i <= j {\n        let m = i + (j - i) / 2 // Вычислить индекс середины m\n        if nums[m] < target { // Это означает, что target находится в интервале [m+1, j]\n            i = m + 1\n        } else if nums[m] > target { // Это означает, что target находится в интервале [i, m-1]\n            j = m - 1\n        } else { // Целевой элемент найден, вернуть его индекс\n            return m\n        }\n    }\n    // Целевой элемент не найден, вернуть -1\n    return -1\n}\n
        binary_search.js
        /* Бинарный поиск (двусторонне замкнутый интервал) */\nfunction binarySearch(nums, target) {\n    // Инициализировать двусторонне замкнутый интервал [0, n-1], то есть i и j указывают на первый и последний элементы массива соответственно\n    let i = 0,\n        j = nums.length - 1;\n    // Цикл завершается, когда диапазон поиска пуст (при i > j диапазон пуст)\n    while (i <= j) {\n        // Вычислить индекс середины m, используя parseInt() для округления вниз\n        const m = parseInt(i + (j - i) / 2);\n        if (nums[m] < target)\n            // Это означает, что target находится в интервале [m+1, j]\n            i = m + 1;\n        else if (nums[m] > target)\n            // Это означает, что target находится в интервале [i, m-1]\n            j = m - 1;\n        else return m; // Целевой элемент найден, вернуть его индекс\n    }\n    // Целевой элемент не найден, вернуть -1\n    return -1;\n}\n
        binary_search.ts
        /* Бинарный поиск (двусторонне замкнутый интервал) */\nfunction binarySearch(nums: number[], target: number): number {\n    // Инициализировать двусторонне замкнутый интервал [0, n-1], то есть i и j указывают на первый и последний элементы массива соответственно\n    let i = 0,\n        j = nums.length - 1;\n    // Цикл завершается, когда диапазон поиска пуст (при i > j диапазон пуст)\n    while (i <= j) {\n        // Вычислить индекс середины m\n        const m = Math.floor(i + (j - i) / 2);\n        if (nums[m] < target) {\n            // Это означает, что target находится в интервале [m+1, j]\n            i = m + 1;\n        } else if (nums[m] > target) {\n            // Это означает, что target находится в интервале [i, m-1]\n            j = m - 1;\n        } else {\n            // Целевой элемент найден, вернуть его индекс\n            return m;\n        }\n    }\n    return -1; // Целевой элемент не найден, вернуть -1\n}\n
        binary_search.dart
        /* Бинарный поиск (двусторонне замкнутый интервал) */\nint binarySearch(List<int> nums, int target) {\n  // Инициализировать двусторонне замкнутый интервал [0, n-1], то есть i и j указывают на первый и последний элементы массива соответственно\n  int i = 0, j = nums.length - 1;\n  // Цикл завершается, когда диапазон поиска пуст (при i > j диапазон пуст)\n  while (i <= j) {\n    int m = i + (j - i) ~/ 2; // Вычислить индекс середины m\n    if (nums[m] < target) {\n      // Это означает, что target находится в интервале [m+1, j]\n      i = m + 1;\n    } else if (nums[m] > target) {\n      // Это означает, что target находится в интервале [i, m-1]\n      j = m - 1;\n    } else {\n      // Целевой элемент найден, вернуть его индекс\n      return m;\n    }\n  }\n  // Целевой элемент не найден, вернуть -1\n  return -1;\n}\n
        binary_search.rs
        /* Бинарный поиск (двусторонне замкнутый интервал) */\nfn binary_search(nums: &[i32], target: i32) -> i32 {\n    // Инициализировать двусторонне замкнутый интервал [0, n-1], то есть i и j указывают на первый и последний элементы массива соответственно\n    let mut i = 0;\n    let mut j = nums.len() as i32 - 1;\n    // Цикл завершается, когда диапазон поиска пуст (при i > j диапазон пуст)\n    while i <= j {\n        let m = i + (j - i) / 2; // Вычислить индекс середины m\n        if nums[m as usize] < target {\n            // Это означает, что target находится в интервале [m+1, j]\n            i = m + 1;\n        } else if nums[m as usize] > target {\n            // Это означает, что target находится в интервале [i, m-1]\n            j = m - 1;\n        } else {\n            // Целевой элемент найден, вернуть его индекс\n            return m;\n        }\n    }\n    // Целевой элемент не найден, вернуть -1\n    return -1;\n}\n
        binary_search.c
        /* Бинарный поиск (двусторонне замкнутый интервал) */\nint binarySearch(int *nums, int len, int target) {\n    // Инициализировать двусторонне замкнутый интервал [0, n-1], то есть i и j указывают на первый и последний элементы массива соответственно\n    int i = 0, j = len - 1;\n    // Цикл завершается, когда диапазон поиска пуст (при i > j диапазон пуст)\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Вычислить индекс середины m\n        if (nums[m] < target)    // Это означает, что target находится в интервале [m+1, j]\n            i = m + 1;\n        else if (nums[m] > target) // Это означает, что target находится в интервале [i, m-1]\n            j = m - 1;\n        else // Целевой элемент найден, вернуть его индекс\n            return m;\n    }\n    // Целевой элемент не найден, вернуть -1\n    return -1;\n}\n
        binary_search.kt
        /* Бинарный поиск (двусторонне замкнутый интервал) */\nfun binarySearch(nums: IntArray, target: Int): Int {\n    // Инициализировать двусторонне замкнутый интервал [0, n-1], то есть i и j указывают на первый и последний элементы массива соответственно\n    var i = 0\n    var j = nums.size - 1\n    // Цикл завершается, когда диапазон поиска пуст (при i > j диапазон пуст)\n    while (i <= j) {\n        val m = i + (j - i) / 2 // Вычислить индекс середины m\n        if (nums[m] < target) // Это означает, что target находится в интервале [m+1, j]\n            i = m + 1\n        else if (nums[m] > target) // Это означает, что target находится в интервале [i, m-1]\n            j = m - 1\n        else  // Целевой элемент найден, вернуть его индекс\n            return m\n    }\n    // Целевой элемент не найден, вернуть -1\n    return -1\n}\n
        binary_search.rb
        ### Бинарный поиск (двусторонне замкнутый интервал) ###\ndef binary_search(nums, target)\n  # Инициализировать двусторонне замкнутый интервал [0, n-1], то есть i и j указывают на первый и последний элементы массива соответственно\n  i, j = 0, nums.length - 1\n\n  # Цикл завершается, когда диапазон поиска пуст (при i > j диапазон пуст)\n  while i <= j\n    # Теоретически числа в Ruby могут быть сколь угодно большими (ограничены только объемом памяти), поэтому не нужно учитывать переполнение больших чисел\n    m = (i + j) / 2   # Вычислить индекс середины m\n\n    if nums[m] < target\n      i = m + 1 # Это означает, что target находится в интервале [m+1, j]\n    elsif nums[m] > target\n      j = m - 1 # Это означает, что target находится в интервале [i, m-1]\n    else\n      return m  # Целевой элемент найден, вернуть его индекс\n    end\n  end\n\n  -1  # Целевой элемент не найден, вернуть -1\nend\n
        Визуализация кода

        Во весь экран >

        Временная сложность равна \\(O(\\log n)\\) : в цикле двоичного поиска интервал каждый раз сокращается вдвое, поэтому число итераций равно \\(\\log_2 n\\) .

        Пространственная сложность равна \\(O(1)\\) : указатели \\(i\\) и \\(j\\) занимают константный объем памяти.

        ","path":["Глава 10. Поиск","10.1   Двоичный поиск"],"tags":[]},{"location":"chapter_searching/binary_search/#1011","level":2,"title":"10.1.1   Методы представления интервалов","text":"

        Помимо описанного выше двойного замкнутого интервала, часто используется и левозамкнутый правооткрытый интервал, который задается как \\([0, n)\\) , то есть левая граница включается, а правая - нет. В этом представлении интервал \\([i, j)\\) пуст, когда \\(i = j\\) .

        На основе этого представления можно реализовать двоичный поиск с той же функциональностью:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search.py
        def binary_search_lcro(nums: list[int], target: int) -> int:\n    \"\"\"Бинарный поиск (лево замкнутый, право открытый интервал)\"\"\"\n    # Инициализировать лево замкнутый, право открытый интервал [0, n), то есть i и j указывают на первый элемент массива и позицию сразу за последним элементом соответственно\n    i, j = 0, len(nums)\n    # Цикл завершается, когда диапазон поиска пуст (при i = j диапазон пуст)\n    while i < j:\n        m = (i + j) // 2  # Вычислить индекс середины m\n        if nums[m] < target:\n            i = m + 1  # Это означает, что target находится в интервале [m+1, j)\n        elif nums[m] > target:\n            j = m  # Это означает, что target находится в интервале [i, m)\n        else:\n            return m  # Целевой элемент найден, вернуть его индекс\n    return -1  # Целевой элемент не найден, вернуть -1\n
        binary_search.cpp
        /* Бинарный поиск (лево замкнутый, право открытый интервал) */\nint binarySearchLCRO(vector<int> &nums, int target) {\n    // Инициализировать лево замкнутый, право открытый интервал [0, n), то есть i и j указывают на первый элемент массива и позицию сразу за последним элементом соответственно\n    int i = 0, j = nums.size();\n    // Цикл завершается, когда диапазон поиска пуст (при i = j диапазон пуст)\n    while (i < j) {\n        int m = i + (j - i) / 2; // Вычислить индекс середины m\n        if (nums[m] < target)    // Это означает, что target находится в интервале [m+1, j)\n            i = m + 1;\n        else if (nums[m] > target) // Это означает, что target находится в интервале [i, m)\n            j = m;\n        else // Целевой элемент найден, вернуть его индекс\n            return m;\n    }\n    // Целевой элемент не найден, вернуть -1\n    return -1;\n}\n
        binary_search.java
        /* Бинарный поиск (лево замкнутый, право открытый интервал) */\nint binarySearchLCRO(int[] nums, int target) {\n    // Инициализировать лево замкнутый, право открытый интервал [0, n), то есть i и j указывают на первый элемент массива и позицию сразу за последним элементом соответственно\n    int i = 0, j = nums.length;\n    // Цикл завершается, когда диапазон поиска пуст (при i = j диапазон пуст)\n    while (i < j) {\n        int m = i + (j - i) / 2; // Вычислить индекс середины m\n        if (nums[m] < target) // Это означает, что target находится в интервале [m+1, j)\n            i = m + 1;\n        else if (nums[m] > target) // Это означает, что target находится в интервале [i, m)\n            j = m;\n        else // Целевой элемент найден, вернуть его индекс\n            return m;\n    }\n    // Целевой элемент не найден, вернуть -1\n    return -1;\n}\n
        binary_search.cs
        /* Бинарный поиск (лево замкнутый, право открытый интервал) */\nint BinarySearchLCRO(int[] nums, int target) {\n    // Инициализировать лево замкнутый, право открытый интервал [0, n), то есть i и j указывают на первый элемент массива и позицию сразу за последним элементом соответственно\n    int i = 0, j = nums.Length;\n    // Цикл завершается, когда диапазон поиска пуст (при i = j диапазон пуст)\n    while (i < j) {\n        int m = i + (j - i) / 2;   // Вычислить индекс середины m\n        if (nums[m] < target)      // Это означает, что target находится в интервале [m+1, j)\n            i = m + 1;\n        else if (nums[m] > target) // Это означает, что target находится в интервале [i, m)\n            j = m;\n        else                       // Целевой элемент найден, вернуть его индекс\n            return m;\n    }\n    // Целевой элемент не найден, вернуть -1\n    return -1;\n}\n
        binary_search.go
        /* Бинарный поиск (лево замкнутый, право открытый интервал) */\nfunc binarySearchLCRO(nums []int, target int) int {\n    // Инициализировать лево замкнутый, право открытый интервал [0, n), то есть i и j указывают на первый элемент массива и позицию сразу за последним элементом соответственно\n    i, j := 0, len(nums)\n    // Цикл завершается, когда диапазон поиска пуст (при i = j диапазон пуст)\n    for i < j {\n        m := i + (j-i)/2      // Вычислить индекс середины m\n        if nums[m] < target { // Это означает, что target находится в интервале [m+1, j)\n            i = m + 1\n        } else if nums[m] > target { // Это означает, что target находится в интервале [i, m)\n            j = m\n        } else { // Целевой элемент найден, вернуть его индекс\n            return m\n        }\n    }\n    // Целевой элемент не найден, вернуть -1\n    return -1\n}\n
        binary_search.swift
        /* Бинарный поиск (лево замкнутый, право открытый интервал) */\nfunc binarySearchLCRO(nums: [Int], target: Int) -> Int {\n    // Инициализировать лево замкнутый, право открытый интервал [0, n), то есть i и j указывают на первый элемент массива и позицию сразу за последним элементом соответственно\n    var i = nums.startIndex\n    var j = nums.endIndex\n    // Цикл завершается, когда диапазон поиска пуст (при i = j диапазон пуст)\n    while i < j {\n        let m = i + (j - i) / 2 // Вычислить индекс середины m\n        if nums[m] < target { // Это означает, что target находится в интервале [m+1, j)\n            i = m + 1\n        } else if nums[m] > target { // Это означает, что target находится в интервале [i, m)\n            j = m\n        } else { // Целевой элемент найден, вернуть его индекс\n            return m\n        }\n    }\n    // Целевой элемент не найден, вернуть -1\n    return -1\n}\n
        binary_search.js
        /* Бинарный поиск (лево замкнутый, право открытый интервал) */\nfunction binarySearchLCRO(nums, target) {\n    // Инициализировать лево замкнутый, право открытый интервал [0, n), то есть i и j указывают на первый элемент массива и позицию сразу за последним элементом соответственно\n    let i = 0,\n        j = nums.length;\n    // Цикл завершается, когда диапазон поиска пуст (при i = j диапазон пуст)\n    while (i < j) {\n        // Вычислить индекс середины m, используя parseInt() для округления вниз\n        const m = parseInt(i + (j - i) / 2);\n        if (nums[m] < target)\n            // Это означает, что target находится в интервале [m+1, j)\n            i = m + 1;\n        else if (nums[m] > target)\n            // Это означает, что target находится в интервале [i, m)\n            j = m;\n        // Целевой элемент найден, вернуть его индекс\n        else return m;\n    }\n    // Целевой элемент не найден, вернуть -1\n    return -1;\n}\n
        binary_search.ts
        /* Бинарный поиск (лево замкнутый, право открытый интервал) */\nfunction binarySearchLCRO(nums: number[], target: number): number {\n    // Инициализировать лево замкнутый, право открытый интервал [0, n), то есть i и j указывают на первый элемент массива и позицию сразу за последним элементом соответственно\n    let i = 0,\n        j = nums.length;\n    // Цикл завершается, когда диапазон поиска пуст (при i = j диапазон пуст)\n    while (i < j) {\n        // Вычислить индекс середины m\n        const m = Math.floor(i + (j - i) / 2);\n        if (nums[m] < target) {\n            // Это означает, что target находится в интервале [m+1, j)\n            i = m + 1;\n        } else if (nums[m] > target) {\n            // Это означает, что target находится в интервале [i, m)\n            j = m;\n        } else {\n            // Целевой элемент найден, вернуть его индекс\n            return m;\n        }\n    }\n    return -1; // Целевой элемент не найден, вернуть -1\n}\n
        binary_search.dart
        /* Бинарный поиск (лево замкнутый, право открытый интервал) */\nint binarySearchLCRO(List<int> nums, int target) {\n  // Инициализировать лево замкнутый, право открытый интервал [0, n), то есть i и j указывают на первый элемент массива и позицию сразу за последним элементом соответственно\n  int i = 0, j = nums.length;\n  // Цикл завершается, когда диапазон поиска пуст (при i = j диапазон пуст)\n  while (i < j) {\n    int m = i + (j - i) ~/ 2; // Вычислить индекс середины m\n    if (nums[m] < target) {\n      // Это означает, что target находится в интервале [m+1, j)\n      i = m + 1;\n    } else if (nums[m] > target) {\n      // Это означает, что target находится в интервале [i, m)\n      j = m;\n    } else {\n      // Целевой элемент найден, вернуть его индекс\n      return m;\n    }\n  }\n  // Целевой элемент не найден, вернуть -1\n  return -1;\n}\n
        binary_search.rs
        /* Бинарный поиск (лево замкнутый, право открытый интервал) */\nfn binary_search_lcro(nums: &[i32], target: i32) -> i32 {\n    // Инициализировать лево замкнутый, право открытый интервал [0, n), то есть i и j указывают на первый элемент массива и позицию сразу за последним элементом соответственно\n    let mut i = 0;\n    let mut j = nums.len() as i32;\n    // Цикл завершается, когда диапазон поиска пуст (при i = j диапазон пуст)\n    while i < j {\n        let m = i + (j - i) / 2; // Вычислить индекс середины m\n        if nums[m as usize] < target {\n            // Это означает, что target находится в интервале [m+1, j)\n            i = m + 1;\n        } else if nums[m as usize] > target {\n            // Это означает, что target находится в интервале [i, m)\n            j = m;\n        } else {\n            // Целевой элемент найден, вернуть его индекс\n            return m;\n        }\n    }\n    // Целевой элемент не найден, вернуть -1\n    return -1;\n}\n
        binary_search.c
        /* Бинарный поиск (лево замкнутый, право открытый интервал) */\nint binarySearchLCRO(int *nums, int len, int target) {\n    // Инициализировать лево замкнутый, право открытый интервал [0, n), то есть i и j указывают на первый элемент массива и позицию сразу за последним элементом соответственно\n    int i = 0, j = len;\n    // Цикл завершается, когда диапазон поиска пуст (при i = j диапазон пуст)\n    while (i < j) {\n        int m = i + (j - i) / 2; // Вычислить индекс середины m\n        if (nums[m] < target)    // Это означает, что target находится в интервале [m+1, j)\n            i = m + 1;\n        else if (nums[m] > target) // Это означает, что target находится в интервале [i, m)\n            j = m;\n        else // Целевой элемент найден, вернуть его индекс\n            return m;\n    }\n    // Целевой элемент не найден, вернуть -1\n    return -1;\n}\n
        binary_search.kt
        /* Бинарный поиск (лево замкнутый, право открытый интервал) */\nfun binarySearchLCRO(nums: IntArray, target: Int): Int {\n    // Инициализировать лево замкнутый, право открытый интервал [0, n), то есть i и j указывают на первый элемент массива и позицию сразу за последним элементом соответственно\n    var i = 0\n    var j = nums.size\n    // Цикл завершается, когда диапазон поиска пуст (при i = j диапазон пуст)\n    while (i < j) {\n        val m = i + (j - i) / 2 // Вычислить индекс середины m\n        if (nums[m] < target) // Это означает, что target находится в интервале [m+1, j)\n            i = m + 1\n        else if (nums[m] > target) // Это означает, что target находится в интервале [i, m)\n            j = m\n        else  // Целевой элемент найден, вернуть его индекс\n            return m\n    }\n    // Целевой элемент не найден, вернуть -1\n    return -1\n}\n
        binary_search.rb
        ### Бинарный поиск (лево замкнутый, право открытый интервал) ###\ndef binary_search_lcro(nums, target)\n  # Инициализировать лево замкнутый, право открытый интервал [0, n), то есть i и j указывают на первый элемент массива и позицию сразу за последним элементом соответственно\n  i, j = 0, nums.length\n\n  # Цикл завершается, когда диапазон поиска пуст (при i = j диапазон пуст)\n  while i < j\n    # Вычислить индекс середины m\n    m = (i + j) / 2\n\n    if nums[m] < target\n      i = m + 1 # Это означает, что target находится в интервале [m+1, j)\n    elsif nums[m] > target\n      j = m - 1 # Это означает, что target находится в интервале [i, m)\n    else\n      return m  # Целевой элемент найден, вернуть его индекс\n    end\n  end\n\n  -1  # Целевой элемент не найден, вернуть -1\nend\n
        Визуализация кода

        Во весь экран >

        Как показано на рисунке 10-3, в этих двух вариантах представления интервала различаются инициализация, условие цикла и операция сужения интервала в алгоритме двоичного поиска.

        Поскольку в записи \"двойной замкнутый интервал\" обе границы являются закрытыми, операции сужения интервала при помощи указателей \\(i\\) и \\(j\\) тоже получаются симметричными. Из-за этого в таком варианте сложнее допустить ошибку, поэтому обычно рекомендуется использовать именно запись \"двойной замкнутый интервал\".

        Рисунок 10-3   Два определения интервалов

        ","path":["Глава 10. Поиск","10.1   Двоичный поиск"],"tags":[]},{"location":"chapter_searching/binary_search/#1012","level":2,"title":"10.1.2   Преимущества и ограничения","text":"

        Двоичный поиск показывает хорошие результаты и по времени, и по памяти.

        • Двоичный поиск очень эффективен по времени. На больших объемах данных логарифмическая временная сложность дает заметное преимущество. Например, когда размер данных \\(n = 2^{20}\\) , линейный поиск потребует \\(2^{20} = 1048576\\) итераций, тогда как двоичный поиск выполнится всего за \\(\\log_2 2^{20} = 20\\) итераций.
        • Двоичный поиск не требует дополнительной памяти. По сравнению с алгоритмами поиска, которым нужно внешнее пространство (например, с хеш-поиском), двоичный поиск заметно экономнее по памяти.

        Однако двоичный поиск подходит не для всех ситуаций, и основные причины таковы.

        • Двоичный поиск применим только к упорядоченным данным. Если входные данные неупорядочены, специально сортировать их ради двоичного поиска невыгодно. Это связано с тем, что временная сложность алгоритмов сортировки обычно составляет \\(O(n \\log n)\\) , что выше, чем у линейного и двоичного поиска. Если элементы приходится часто вставлять, то для сохранения порядка в массиве их нужно помещать в конкретные позиции, а это требует \\(O(n)\\) времени и тоже обходится дорого.
        • Двоичный поиск применим только к массивам. Для него нужен скачкообразный доступ к элементам, а в связном списке такой доступ малоэффективен, поэтому двоичный поиск не подходит для списков и структур данных, построенных на их основе.
        • При малом объеме данных линейный поиск работает лучше. В линейном поиске на каждом шаге нужна всего одна операция сравнения; в двоичном поиске требуется 1 сложение, 1 деление, от 1 до 3 сравнений и еще 1 сложение или вычитание, то есть всего от 4 до 6 элементарных операций. Поэтому при небольшом \\(n\\) линейный поиск может оказаться быстрее двоичного.
        ","path":["Глава 10. Поиск","10.1   Двоичный поиск"],"tags":[]},{"location":"chapter_searching/binary_search_edge/","level":1,"title":"10.3   Двоичный поиск границ","text":"","path":["Глава 10. Поиск","10.3   Двоичный поиск границ"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#1031","level":2,"title":"10.3.1   Поиск левой границы","text":"

        Question

        Дан упорядоченный массив nums длины \\(n\\), который может содержать повторяющиеся элементы. Верните индекс самого левого элемента target в массиве. Если массив не содержит этот элемент, верните \\(-1\\) .

        Вспомним метод поиска точки вставки при двоичном поиске: после завершения поиска указатель \\(i\\) указывает на самый левый target , поэтому поиск точки вставки по сути является поиском индекса самого левого target.

        Рассмотрим реализацию поиска левой границы через функцию поиска точки вставки. Обратите внимание: массив может не содержать target , и тогда возможны две ситуации.

        • Индекс точки вставки \\(i\\) выходит за границы массива.
        • Элемент nums[i] не равен target .

        Если возникает любая из этих ситуаций, достаточно сразу вернуть \\(-1\\) . Код приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_edge.py
        def binary_search_left_edge(nums: list[int], target: int) -> int:\n    \"\"\"Бинарный поиск самого левого target\"\"\"\n    # Эквивалентно поиску точки вставки target\n    i = binary_search_insertion(nums, target)\n    # target не найден, вернуть -1\n    if i == len(nums) or nums[i] != target:\n        return -1\n    # Найти target и вернуть индекс i\n    return i\n
        binary_search_edge.cpp
        /* Бинарный поиск самого левого target */\nint binarySearchLeftEdge(vector<int> &nums, int target) {\n    // Эквивалентно поиску точки вставки target\n    int i = binarySearchInsertion(nums, target);\n    // target не найден, вернуть -1\n    if (i == nums.size() || nums[i] != target) {\n        return -1;\n    }\n    // Найти target и вернуть индекс i\n    return i;\n}\n
        binary_search_edge.java
        /* Бинарный поиск самого левого target */\nint binarySearchLeftEdge(int[] nums, int target) {\n    // Эквивалентно поиску точки вставки target\n    int i = binary_search_insertion.binarySearchInsertion(nums, target);\n    // target не найден, вернуть -1\n    if (i == nums.length || nums[i] != target) {\n        return -1;\n    }\n    // Найти target и вернуть индекс i\n    return i;\n}\n
        binary_search_edge.cs
        /* Бинарный поиск самого левого target */\nint BinarySearchLeftEdge(int[] nums, int target) {\n    // Эквивалентно поиску точки вставки target\n    int i = binary_search_insertion.BinarySearchInsertion(nums, target);\n    // target не найден, вернуть -1\n    if (i == nums.Length || nums[i] != target) {\n        return -1;\n    }\n    // Найти target и вернуть индекс i\n    return i;\n}\n
        binary_search_edge.go
        /* Бинарный поиск самого левого target */\nfunc binarySearchLeftEdge(nums []int, target int) int {\n    // Эквивалентно поиску точки вставки target\n    i := binarySearchInsertion(nums, target)\n    // target не найден, вернуть -1\n    if i == len(nums) || nums[i] != target {\n        return -1\n    }\n    // Найти target и вернуть индекс i\n    return i\n}\n
        binary_search_edge.swift
        /* Бинарный поиск самого левого target */\nfunc binarySearchLeftEdge(nums: [Int], target: Int) -> Int {\n    // Эквивалентно поиску точки вставки target\n    let i = binarySearchInsertion(nums: nums, target: target)\n    // target не найден, вернуть -1\n    if i == nums.endIndex || nums[i] != target {\n        return -1\n    }\n    // Найти target и вернуть индекс i\n    return i\n}\n
        binary_search_edge.js
        /* Бинарный поиск самого левого target */\nfunction binarySearchLeftEdge(nums, target) {\n    // Эквивалентно поиску точки вставки target\n    const i = binarySearchInsertion(nums, target);\n    // target не найден, вернуть -1\n    if (i === nums.length || nums[i] !== target) {\n        return -1;\n    }\n    // Найти target и вернуть индекс i\n    return i;\n}\n
        binary_search_edge.ts
        /* Бинарный поиск самого левого target */\nfunction binarySearchLeftEdge(nums: Array<number>, target: number): number {\n    // Эквивалентно поиску точки вставки target\n    const i = binarySearchInsertion(nums, target);\n    // target не найден, вернуть -1\n    if (i === nums.length || nums[i] !== target) {\n        return -1;\n    }\n    // Найти target и вернуть индекс i\n    return i;\n}\n
        binary_search_edge.dart
        /* Бинарный поиск самого левого target */\nint binarySearchLeftEdge(List<int> nums, int target) {\n  // Эквивалентно поиску точки вставки target\n  int i = binarySearchInsertion(nums, target);\n  // target не найден, вернуть -1\n  if (i == nums.length || nums[i] != target) {\n    return -1;\n  }\n  // Найти target и вернуть индекс i\n  return i;\n}\n
        binary_search_edge.rs
        /* Бинарный поиск самого левого target */\nfn binary_search_left_edge(nums: &[i32], target: i32) -> i32 {\n    // Эквивалентно поиску точки вставки target\n    let i = binary_search_insertion(nums, target);\n    // target не найден, вернуть -1\n    if i == nums.len() as i32 || nums[i as usize] != target {\n        return -1;\n    }\n    // Найти target и вернуть индекс i\n    i\n}\n
        binary_search_edge.c
        /* Бинарный поиск самого левого target */\nint binarySearchLeftEdge(int *nums, int numSize, int target) {\n    // Эквивалентно поиску точки вставки target\n    int i = binarySearchInsertion(nums, numSize, target);\n    // target не найден, вернуть -1\n    if (i == numSize || nums[i] != target) {\n        return -1;\n    }\n    // Найти target и вернуть индекс i\n    return i;\n}\n
        binary_search_edge.kt
        /* Бинарный поиск самого левого target */\nfun binarySearchLeftEdge(nums: IntArray, target: Int): Int {\n    // Эквивалентно поиску точки вставки target\n    val i = binarySearchInsertion(nums, target)\n    // target не найден, вернуть -1\n    if (i == nums.size || nums[i] != target) {\n        return -1\n    }\n    // Найти target и вернуть индекс i\n    return i\n}\n
        binary_search_edge.rb
        ### Бинарный поиск самого левого target ###\ndef binary_search_left_edge(nums, target)\n  # Эквивалентно поиску точки вставки target\n  i = binary_search_insertion(nums, target)\n\n  # target не найден, вернуть -1\n  return -1 if i == nums.length || nums[i] != target\n\n  i # Найти target и вернуть индекс i\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 10. Поиск","10.3   Двоичный поиск границ"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#1032","level":2,"title":"10.3.2   Поиск правой границы","text":"

        Как тогда найти самый правый target ? Самый прямой способ - изменить код, заменив операцию сужения указателя в случае nums[m] == target . Мы не будем приводить этот код, заинтересованные читатели могут реализовать его самостоятельно.

        Ниже представлены два более изящных способа.

        ","path":["Глава 10. Поиск","10.3   Двоичный поиск границ"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#1","level":3,"title":"1.   Повторное использование поиска левой границы","text":"

        На самом деле функцию поиска самого левого элемента можно использовать и для поиска самого правого элемента. Конкретная идея такова: преобразовать поиск самого правого target в поиск самого левого target + 1.

        Как показано на рисунке 10-7, после завершения поиска указатель \\(i\\) указывает на самый левый target + 1 (если он существует), а указатель \\(j\\) указывает на самый правый target , поэтому достаточно вернуть \\(j\\).

        Рисунок 10-7   Преобразование поиска правой границы в поиск левой

        Обратите внимание: функция возвращает точку вставки \\(i\\) , поэтому из нее нужно вычесть \\(1\\) , чтобы получить \\(j\\) :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_edge.py
        def binary_search_right_edge(nums: list[int], target: int) -> int:\n    \"\"\"Бинарный поиск самого правого target\"\"\"\n    # Преобразовать задачу в поиск самого левого target + 1\n    i = binary_search_insertion(nums, target + 1)\n    # j указывает на самый правый target, а i — на первый элемент больше target\n    j = i - 1\n    # target не найден, вернуть -1\n    if j == -1 or nums[j] != target:\n        return -1\n    # Найти target и вернуть индекс j\n    return j\n
        binary_search_edge.cpp
        /* Бинарный поиск самого правого target */\nint binarySearchRightEdge(vector<int> &nums, int target) {\n    // Преобразовать задачу в поиск самого левого target + 1\n    int i = binarySearchInsertion(nums, target + 1);\n    // j указывает на самый правый target, а i — на первый элемент больше target\n    int j = i - 1;\n    // target не найден, вернуть -1\n    if (j == -1 || nums[j] != target) {\n        return -1;\n    }\n    // Найти target и вернуть индекс j\n    return j;\n}\n
        binary_search_edge.java
        /* Бинарный поиск самого правого target */\nint binarySearchRightEdge(int[] nums, int target) {\n    // Преобразовать задачу в поиск самого левого target + 1\n    int i = binary_search_insertion.binarySearchInsertion(nums, target + 1);\n    // j указывает на самый правый target, а i — на первый элемент больше target\n    int j = i - 1;\n    // target не найден, вернуть -1\n    if (j == -1 || nums[j] != target) {\n        return -1;\n    }\n    // Найти target и вернуть индекс j\n    return j;\n}\n
        binary_search_edge.cs
        /* Бинарный поиск самого правого target */\nint BinarySearchRightEdge(int[] nums, int target) {\n    // Преобразовать задачу в поиск самого левого target + 1\n    int i = binary_search_insertion.BinarySearchInsertion(nums, target + 1);\n    // j указывает на самый правый target, а i — на первый элемент больше target\n    int j = i - 1;\n    // target не найден, вернуть -1\n    if (j == -1 || nums[j] != target) {\n        return -1;\n    }\n    // Найти target и вернуть индекс j\n    return j;\n}\n
        binary_search_edge.go
        /* Бинарный поиск самого правого target */\nfunc binarySearchRightEdge(nums []int, target int) int {\n    // Преобразовать задачу в поиск самого левого target + 1\n    i := binarySearchInsertion(nums, target+1)\n    // j указывает на самый правый target, а i — на первый элемент больше target\n    j := i - 1\n    // target не найден, вернуть -1\n    if j == -1 || nums[j] != target {\n        return -1\n    }\n    // Найти target и вернуть индекс j\n    return j\n}\n
        binary_search_edge.swift
        /* Бинарный поиск самого правого target */\nfunc binarySearchRightEdge(nums: [Int], target: Int) -> Int {\n    // Преобразовать задачу в поиск самого левого target + 1\n    let i = binarySearchInsertion(nums: nums, target: target + 1)\n    // j указывает на самый правый target, а i — на первый элемент больше target\n    let j = i - 1\n    // target не найден, вернуть -1\n    if j == -1 || nums[j] != target {\n        return -1\n    }\n    // Найти target и вернуть индекс j\n    return j\n}\n
        binary_search_edge.js
        /* Бинарный поиск самого правого target */\nfunction binarySearchRightEdge(nums, target) {\n    // Преобразовать задачу в поиск самого левого target + 1\n    const i = binarySearchInsertion(nums, target + 1);\n    // j указывает на самый правый target, а i — на первый элемент больше target\n    const j = i - 1;\n    // target не найден, вернуть -1\n    if (j === -1 || nums[j] !== target) {\n        return -1;\n    }\n    // Найти target и вернуть индекс j\n    return j;\n}\n
        binary_search_edge.ts
        /* Бинарный поиск самого правого target */\nfunction binarySearchRightEdge(nums: Array<number>, target: number): number {\n    // Преобразовать задачу в поиск самого левого target + 1\n    const i = binarySearchInsertion(nums, target + 1);\n    // j указывает на самый правый target, а i — на первый элемент больше target\n    const j = i - 1;\n    // target не найден, вернуть -1\n    if (j === -1 || nums[j] !== target) {\n        return -1;\n    }\n    // Найти target и вернуть индекс j\n    return j;\n}\n
        binary_search_edge.dart
        /* Бинарный поиск самого правого target */\nint binarySearchRightEdge(List<int> nums, int target) {\n  // Преобразовать задачу в поиск самого левого target + 1\n  int i = binarySearchInsertion(nums, target + 1);\n  // j указывает на самый правый target, а i — на первый элемент больше target\n  int j = i - 1;\n  // target не найден, вернуть -1\n  if (j == -1 || nums[j] != target) {\n    return -1;\n  }\n  // Найти target и вернуть индекс j\n  return j;\n}\n
        binary_search_edge.rs
        /* Бинарный поиск самого правого target */\nfn binary_search_right_edge(nums: &[i32], target: i32) -> i32 {\n    // Преобразовать задачу в поиск самого левого target + 1\n    let i = binary_search_insertion(nums, target + 1);\n    // j указывает на самый правый target, а i — на первый элемент больше target\n    let j = i - 1;\n    // target не найден, вернуть -1\n    if j == -1 || nums[j as usize] != target {\n        return -1;\n    }\n    // Найти target и вернуть индекс j\n    j\n}\n
        binary_search_edge.c
        /* Бинарный поиск самого правого target */\nint binarySearchRightEdge(int *nums, int numSize, int target) {\n    // Преобразовать задачу в поиск самого левого target + 1\n    int i = binarySearchInsertion(nums, numSize, target + 1);\n    // j указывает на самый правый target, а i — на первый элемент больше target\n    int j = i - 1;\n    // target не найден, вернуть -1\n    if (j == -1 || nums[j] != target) {\n        return -1;\n    }\n    // Найти target и вернуть индекс j\n    return j;\n}\n
        binary_search_edge.kt
        /* Бинарный поиск самого правого target */\nfun binarySearchRightEdge(nums: IntArray, target: Int): Int {\n    // Преобразовать задачу в поиск самого левого target + 1\n    val i = binarySearchInsertion(nums, target + 1)\n    // j указывает на самый правый target, а i — на первый элемент больше target\n    val j = i - 1\n    // target не найден, вернуть -1\n    if (j == -1 || nums[j] != target) {\n        return -1\n    }\n    // Найти target и вернуть индекс j\n    return j\n}\n
        binary_search_edge.rb
        ### Бинарный поиск самого правого target ###\ndef binary_search_right_edge(nums, target)\n  # Преобразовать задачу в поиск самого левого target + 1\n  i = binary_search_insertion(nums, target + 1)\n\n  # j указывает на самый правый target, а i — на первый элемент больше target\n  j = i - 1\n\n  # target не найден, вернуть -1\n  return -1 if j == -1 || nums[j] != target\n\n  j # Найти target и вернуть индекс j\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 10. Поиск","10.3   Двоичный поиск границ"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#2","level":3,"title":"2.   Преобразование в поиск элемента","text":"

        Мы знаем, что если массив не содержит target , то в конце поиска указатели \\(i\\) и \\(j\\) будут указывать соответственно на первый элемент, больший target , и на первый элемент, меньший target .

        Следовательно, как показано на рисунке 10-8, для поиска левой и правой границы можно сконструировать элемент, которого нет в массиве.

        • Поиск самого левого target : можно преобразовать в поиск target - 0.5 и вернуть указатель \\(i\\) .
        • Поиск самого правого target : можно преобразовать в поиск target + 0.5 и вернуть указатель \\(j\\) .

        Рисунок 10-8   Преобразование поиска границ в поиск элемента

        Код здесь опущен, но стоит обратить внимание на два момента.

        • По условию массив не содержит дробных чисел, поэтому нам не нужно беспокоиться о том, как обрабатывать случай равенства другим элементам массива.
        • Поскольку этот метод вводит дробные числа, переменную target в функции нужно изменить на тип с плавающей запятой (в Python менять ничего не требуется).
        ","path":["Глава 10. Поиск","10.3   Двоичный поиск границ"],"tags":[]},{"location":"chapter_searching/binary_search_insertion/","level":1,"title":"10.2   Двоичный поиск точки вставки","text":"

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

        ","path":["Глава 10. Поиск","10.2   Двоичный поиск точки вставки"],"tags":[]},{"location":"chapter_searching/binary_search_insertion/#1021","level":2,"title":"10.2.1   Случай без повторяющихся элементов","text":"

        Question

        Дан упорядоченный массив nums длины \\(n\\) и элемент target , причем в массиве нет повторяющихся элементов. Нужно вставить target в массив nums , сохранив порядок. Если элемент target уже присутствует в массиве, вставьте его слева от него. Верните индекс, который будет иметь target после вставки. Пример показан на рисунке 10-4.

        Рисунок 10-4   Пример данных для точки вставки

        Если мы хотим переиспользовать код двоичного поиска из предыдущего раздела, нужно ответить на два вопроса.

        Вопрос 1: если массив содержит target , будет ли индекс вставки совпадать с индексом этого элемента?

        По условию target нужно вставить слева от равного элемента, а это означает, что новый target занимает место старого target . Иначе говоря, если массив содержит target , то индекс вставки совпадает с индексом этого target.

        Вопрос 2: если массив не содержит target , индекс какого элемента будет точкой вставки?

        Рассмотрим процесс двоичного поиска подробнее: когда nums[m] < target , указатель \\(i\\) сдвигается вправо и тем самым приближается к элементу, который больше либо равен target . Аналогично указатель \\(j\\) постепенно приближается к элементу, который меньше либо равен target .

        Следовательно, после завершения двоичного поиска обязательно выполняется следующее: указатель \\(i\\) указывает на первый элемент, больший target , а указатель \\(j\\) указывает на первый элемент, меньший target . Нетрудно сделать вывод, что если массив не содержит target , то индекс вставки равен \\(i\\) . Код приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_insertion.py
        def binary_search_insertion_simple(nums: list[int], target: int) -> int:\n    \"\"\"Бинарный поиск точки вставки (без повторяющихся элементов)\"\"\"\n    i, j = 0, len(nums) - 1  # Инициализировать двусторонне замкнутый интервал [0, n-1]\n    while i <= j:\n        m = (i + j) // 2  # Вычислить индекс середины m\n        if nums[m] < target:\n            i = m + 1  # target находится в интервале [m+1, j]\n        elif nums[m] > target:\n            j = m - 1  # target находится в интервале [i, m-1]\n        else:\n            return m  # Найти target и вернуть точку вставки m\n    # target не найден, вернуть точку вставки i\n    return i\n
        binary_search_insertion.cpp
        /* Бинарный поиск точки вставки (без повторяющихся элементов) */\nint binarySearchInsertionSimple(vector<int> &nums, int target) {\n    int i = 0, j = nums.size() - 1; // Инициализировать двусторонне замкнутый интервал [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Вычислить индекс середины m\n        if (nums[m] < target) {\n            i = m + 1; // target находится в интервале [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target находится в интервале [i, m-1]\n        } else {\n            return m; // Найти target и вернуть точку вставки m\n        }\n    }\n    // target не найден, вернуть точку вставки i\n    return i;\n}\n
        binary_search_insertion.java
        /* Бинарный поиск точки вставки (без повторяющихся элементов) */\nint binarySearchInsertionSimple(int[] nums, int target) {\n    int i = 0, j = nums.length - 1; // Инициализировать двусторонне замкнутый интервал [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Вычислить индекс середины m\n        if (nums[m] < target) {\n            i = m + 1; // target находится в интервале [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target находится в интервале [i, m-1]\n        } else {\n            return m; // Найти target и вернуть точку вставки m\n        }\n    }\n    // target не найден, вернуть точку вставки i\n    return i;\n}\n
        binary_search_insertion.cs
        /* Бинарный поиск точки вставки (без повторяющихся элементов) */\nint BinarySearchInsertionSimple(int[] nums, int target) {\n    int i = 0, j = nums.Length - 1; // Инициализировать двусторонне замкнутый интервал [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Вычислить индекс середины m\n        if (nums[m] < target) {\n            i = m + 1; // target находится в интервале [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target находится в интервале [i, m-1]\n        } else {\n            return m; // Найти target и вернуть точку вставки m\n        }\n    }\n    // target не найден, вернуть точку вставки i\n    return i;\n}\n
        binary_search_insertion.go
        /* Бинарный поиск точки вставки (без повторяющихся элементов) */\nfunc binarySearchInsertionSimple(nums []int, target int) int {\n    // Инициализировать двусторонне замкнутый интервал [0, n-1]\n    i, j := 0, len(nums)-1\n    for i <= j {\n        // Вычислить индекс середины m\n        m := i + (j-i)/2\n        if nums[m] < target {\n            // target находится в интервале [m+1, j]\n            i = m + 1\n        } else if nums[m] > target {\n            // target находится в интервале [i, m-1]\n            j = m - 1\n        } else {\n            // Найти target и вернуть точку вставки m\n            return m\n        }\n    }\n    // target не найден, вернуть точку вставки i\n    return i\n}\n
        binary_search_insertion.swift
        /* Бинарный поиск точки вставки (без повторяющихся элементов) */\nfunc binarySearchInsertionSimple(nums: [Int], target: Int) -> Int {\n    // Инициализировать двусторонне замкнутый интервал [0, n-1]\n    var i = nums.startIndex\n    var j = nums.endIndex - 1\n    while i <= j {\n        let m = i + (j - i) / 2 // Вычислить индекс середины m\n        if nums[m] < target {\n            i = m + 1 // target находится в интервале [m+1, j]\n        } else if nums[m] > target {\n            j = m - 1 // target находится в интервале [i, m-1]\n        } else {\n            return m // Найти target и вернуть точку вставки m\n        }\n    }\n    // target не найден, вернуть точку вставки i\n    return i\n}\n
        binary_search_insertion.js
        /* Бинарный поиск точки вставки (без повторяющихся элементов) */\nfunction binarySearchInsertionSimple(nums, target) {\n    let i = 0,\n        j = nums.length - 1; // Инициализировать двусторонне замкнутый интервал [0, n-1]\n    while (i <= j) {\n        const m = Math.floor(i + (j - i) / 2); // Вычислить индекс середины m, используя Math.floor() для округления вниз\n        if (nums[m] < target) {\n            i = m + 1; // target находится в интервале [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target находится в интервале [i, m-1]\n        } else {\n            return m; // Найти target и вернуть точку вставки m\n        }\n    }\n    // target не найден, вернуть точку вставки i\n    return i;\n}\n
        binary_search_insertion.ts
        /* Бинарный поиск точки вставки (без повторяющихся элементов) */\nfunction binarySearchInsertionSimple(\n    nums: Array<number>,\n    target: number\n): number {\n    let i = 0,\n        j = nums.length - 1; // Инициализировать двусторонне замкнутый интервал [0, n-1]\n    while (i <= j) {\n        const m = Math.floor(i + (j - i) / 2); // Вычислить индекс середины m, используя Math.floor() для округления вниз\n        if (nums[m] < target) {\n            i = m + 1; // target находится в интервале [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target находится в интервале [i, m-1]\n        } else {\n            return m; // Найти target и вернуть точку вставки m\n        }\n    }\n    // target не найден, вернуть точку вставки i\n    return i;\n}\n
        binary_search_insertion.dart
        /* Бинарный поиск точки вставки (без повторяющихся элементов) */\nint binarySearchInsertionSimple(List<int> nums, int target) {\n  int i = 0, j = nums.length - 1; // Инициализировать двусторонне замкнутый интервал [0, n-1]\n  while (i <= j) {\n    int m = i + (j - i) ~/ 2; // Вычислить индекс середины m\n    if (nums[m] < target) {\n      i = m + 1; // target находится в интервале [m+1, j]\n    } else if (nums[m] > target) {\n      j = m - 1; // target находится в интервале [i, m-1]\n    } else {\n      return m; // Найти target и вернуть точку вставки m\n    }\n  }\n  // target не найден, вернуть точку вставки i\n  return i;\n}\n
        binary_search_insertion.rs
        /* Бинарный поиск точки вставки (без повторяющихся элементов) */\nfn binary_search_insertion_simple(nums: &[i32], target: i32) -> i32 {\n    let (mut i, mut j) = (0, nums.len() as i32 - 1); // Инициализировать двусторонне замкнутый интервал [0, n-1]\n    while i <= j {\n        let m = i + (j - i) / 2; // Вычислить индекс середины m\n        if nums[m as usize] < target {\n            i = m + 1; // target находится в интервале [m+1, j]\n        } else if nums[m as usize] > target {\n            j = m - 1; // target находится в интервале [i, m-1]\n        } else {\n            return m;\n        }\n    }\n    // target не найден, вернуть точку вставки i\n    i\n}\n
        binary_search_insertion.c
        /* Бинарный поиск точки вставки (без повторяющихся элементов) */\nint binarySearchInsertionSimple(int *nums, int numSize, int target) {\n    int i = 0, j = numSize - 1; // Инициализировать двусторонне замкнутый интервал [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Вычислить индекс середины m\n        if (nums[m] < target) {\n            i = m + 1; // target находится в интервале [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target находится в интервале [i, m-1]\n        } else {\n            return m; // Найти target и вернуть точку вставки m\n        }\n    }\n    // target не найден, вернуть точку вставки i\n    return i;\n}\n
        binary_search_insertion.kt
        /* Бинарный поиск точки вставки (без повторяющихся элементов) */\nfun binarySearchInsertionSimple(nums: IntArray, target: Int): Int {\n    var i = 0\n    var j = nums.size - 1 // Инициализировать двусторонне замкнутый интервал [0, n-1]\n    while (i <= j) {\n        val m = i + (j - i) / 2 // Вычислить индекс середины m\n        if (nums[m] < target) {\n            i = m + 1 // target находится в интервале [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1 // target находится в интервале [i, m-1]\n        } else {\n            return m // Найти target и вернуть точку вставки m\n        }\n    }\n    // target не найден, вернуть точку вставки i\n    return i\n}\n
        binary_search_insertion.rb
        ### Бинарный поиск точки вставки (без повторяющихся элементов) ###\ndef binary_search_insertion_simple(nums, target)\n  # Инициализировать двусторонне замкнутый интервал [0, n-1]\n  i, j = 0, nums.length - 1\n\n  while i <= j\n    # Вычислить индекс середины m\n    m = (i + j) / 2\n\n    if nums[m] < target\n      i = m + 1 # target находится в интервале [m+1, j]\n    elsif nums[m] > target\n      j = m - 1 # target находится в интервале [i, m-1]\n    else\n      return m  # Найти target и вернуть точку вставки m\n    end\n  end\n\n  i # target не найден, вернуть точку вставки i\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 10. Поиск","10.2   Двоичный поиск точки вставки"],"tags":[]},{"location":"chapter_searching/binary_search_insertion/#1022","level":2,"title":"10.2.2   Случай с повторяющимися элементами","text":"

        Question

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

        Если в массиве есть несколько элементов target , то обычный двоичный поиск сможет вернуть индекс только одного из них, но не позволит определить, сколько элементов target находится слева и справа от него.

        По условию целевой элемент нужно вставить в самую левую позицию, поэтому нам нужно найти индекс самого левого target в массиве. На первом этапе можно рассмотреть решение, показанное на рисунке 10-5.

        1. Выполнить двоичный поиск и получить индекс любого элемента target , обозначив его как \\(k\\) .
        2. Начиная с индекса \\(k\\) , линейно двигаться влево и вернуть результат, когда будет найден самый левый target .

        Рисунок 10-5   Линейный поиск точки вставки среди повторяющихся элементов

        Этот метод применим на практике, однако в нем есть линейный поиск, поэтому его временная сложность равна \\(O(n)\\) . Когда в массиве имеется много повторяющихся target , такой подход работает неэффективно.

        Теперь рассмотрим расширение кода двоичного поиска. Как показано на рисунке 10-6, общий процесс остается прежним: на каждом шаге мы сначала вычисляем индекс середины \\(m\\) , а затем сравниваем target и nums[m] , после чего возможны следующие случаи.

        • Когда nums[m] < target или nums[m] > target , это означает, что target еще не найден, поэтому используется стандартная операция сужения интервала в двоичном поиске, благодаря чему указатели \\(i\\) и \\(j\\) приближаются к target.
        • Когда nums[m] == target , это означает, что элементы меньше target находятся в интервале \\([i, m - 1]\\) , поэтому мы используем \\(j = m - 1\\) для сужения интервала, тем самым приближая указатель \\(j\\) к элементам, меньшим target.

        После завершения цикла указатель \\(i\\) будет указывать на самый левый target , а указатель \\(j\\) - на первый элемент, меньший target , поэтому индекс \\(i\\) и является точкой вставки.

        <1><2><3><4><5><6><7><8>

        Рисунок 10-6   Шаги поиска точки вставки для повторяющихся элементов

        Если посмотреть на следующий код, то видно, что действия в ветвях nums[m] > target и nums[m] == target совпадают, поэтому эти две ветви можно объединить.

        Даже в этом случае можно оставить условия развернутыми, потому что так логика выглядит более ясной и код легче читать.

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_insertion.py
        def binary_search_insertion(nums: list[int], target: int) -> int:\n    \"\"\"Бинарный поиск точки вставки (с повторяющимися элементами)\"\"\"\n    i, j = 0, len(nums) - 1  # Инициализировать двусторонне замкнутый интервал [0, n-1]\n    while i <= j:\n        m = (i + j) // 2  # Вычислить индекс середины m\n        if nums[m] < target:\n            i = m + 1  # target находится в интервале [m+1, j]\n        elif nums[m] > target:\n            j = m - 1  # target находится в интервале [i, m-1]\n        else:\n            j = m - 1  # Первый элемент меньше target находится в интервале [i, m-1]\n    # Вернуть точку вставки i\n    return i\n
        binary_search_insertion.cpp
        /* Бинарный поиск точки вставки (с повторяющимися элементами) */\nint binarySearchInsertion(vector<int> &nums, int target) {\n    int i = 0, j = nums.size() - 1; // Инициализировать двусторонне замкнутый интервал [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Вычислить индекс середины m\n        if (nums[m] < target) {\n            i = m + 1; // target находится в интервале [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target находится в интервале [i, m-1]\n        } else {\n            j = m - 1; // Первый элемент меньше target находится в интервале [i, m-1]\n        }\n    }\n    // Вернуть точку вставки i\n    return i;\n}\n
        binary_search_insertion.java
        /* Бинарный поиск точки вставки (с повторяющимися элементами) */\nint binarySearchInsertion(int[] nums, int target) {\n    int i = 0, j = nums.length - 1; // Инициализировать двусторонне замкнутый интервал [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Вычислить индекс середины m\n        if (nums[m] < target) {\n            i = m + 1; // target находится в интервале [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target находится в интервале [i, m-1]\n        } else {\n            j = m - 1; // Первый элемент меньше target находится в интервале [i, m-1]\n        }\n    }\n    // Вернуть точку вставки i\n    return i;\n}\n
        binary_search_insertion.cs
        /* Бинарный поиск точки вставки (с повторяющимися элементами) */\nint BinarySearchInsertion(int[] nums, int target) {\n    int i = 0, j = nums.Length - 1; // Инициализировать двусторонне замкнутый интервал [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Вычислить индекс середины m\n        if (nums[m] < target) {\n            i = m + 1; // target находится в интервале [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target находится в интервале [i, m-1]\n        } else {\n            j = m - 1; // Первый элемент меньше target находится в интервале [i, m-1]\n        }\n    }\n    // Вернуть точку вставки i\n    return i;\n}\n
        binary_search_insertion.go
        /* Бинарный поиск точки вставки (с повторяющимися элементами) */\nfunc binarySearchInsertion(nums []int, target int) int {\n    // Инициализировать двусторонне замкнутый интервал [0, n-1]\n    i, j := 0, len(nums)-1\n    for i <= j {\n        // Вычислить индекс середины m\n        m := i + (j-i)/2\n        if nums[m] < target {\n            // target находится в интервале [m+1, j]\n            i = m + 1\n        } else if nums[m] > target {\n            // target находится в интервале [i, m-1]\n            j = m - 1\n        } else {\n            // Первый элемент меньше target находится в интервале [i, m-1]\n            j = m - 1\n        }\n    }\n    // Вернуть точку вставки i\n    return i\n}\n
        binary_search_insertion.swift
        /* Бинарный поиск точки вставки (с повторяющимися элементами) */\nfunc binarySearchInsertion(nums: [Int], target: Int) -> Int {\n    // Инициализировать двусторонне замкнутый интервал [0, n-1]\n    var i = nums.startIndex\n    var j = nums.endIndex - 1\n    while i <= j {\n        let m = i + (j - i) / 2 // Вычислить индекс середины m\n        if nums[m] < target {\n            i = m + 1 // target находится в интервале [m+1, j]\n        } else if nums[m] > target {\n            j = m - 1 // target находится в интервале [i, m-1]\n        } else {\n            j = m - 1 // Первый элемент меньше target находится в интервале [i, m-1]\n        }\n    }\n    // Вернуть точку вставки i\n    return i\n}\n
        binary_search_insertion.js
        /* Бинарный поиск точки вставки (с повторяющимися элементами) */\nfunction binarySearchInsertion(nums, target) {\n    let i = 0,\n        j = nums.length - 1; // Инициализировать двусторонне замкнутый интервал [0, n-1]\n    while (i <= j) {\n        const m = Math.floor(i + (j - i) / 2); // Вычислить индекс середины m, используя Math.floor() для округления вниз\n        if (nums[m] < target) {\n            i = m + 1; // target находится в интервале [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target находится в интервале [i, m-1]\n        } else {\n            j = m - 1; // Первый элемент меньше target находится в интервале [i, m-1]\n        }\n    }\n    // Вернуть точку вставки i\n    return i;\n}\n
        binary_search_insertion.ts
        /* Бинарный поиск точки вставки (с повторяющимися элементами) */\nfunction binarySearchInsertion(nums: Array<number>, target: number): number {\n    let i = 0,\n        j = nums.length - 1; // Инициализировать двусторонне замкнутый интервал [0, n-1]\n    while (i <= j) {\n        const m = Math.floor(i + (j - i) / 2); // Вычислить индекс середины m, используя Math.floor() для округления вниз\n        if (nums[m] < target) {\n            i = m + 1; // target находится в интервале [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target находится в интервале [i, m-1]\n        } else {\n            j = m - 1; // Первый элемент меньше target находится в интервале [i, m-1]\n        }\n    }\n    // Вернуть точку вставки i\n    return i;\n}\n
        binary_search_insertion.dart
        /* Бинарный поиск точки вставки (с повторяющимися элементами) */\nint binarySearchInsertion(List<int> nums, int target) {\n  int i = 0, j = nums.length - 1; // Инициализировать двусторонне замкнутый интервал [0, n-1]\n  while (i <= j) {\n    int m = i + (j - i) ~/ 2; // Вычислить индекс середины m\n    if (nums[m] < target) {\n      i = m + 1; // target находится в интервале [m+1, j]\n    } else if (nums[m] > target) {\n      j = m - 1; // target находится в интервале [i, m-1]\n    } else {\n      j = m - 1; // Первый элемент меньше target находится в интервале [i, m-1]\n    }\n  }\n  // Вернуть точку вставки i\n  return i;\n}\n
        binary_search_insertion.rs
        /* Бинарный поиск точки вставки (с повторяющимися элементами) */\npub fn binary_search_insertion(nums: &[i32], target: i32) -> i32 {\n    let (mut i, mut j) = (0, nums.len() as i32 - 1); // Инициализировать двусторонне замкнутый интервал [0, n-1]\n    while i <= j {\n        let m = i + (j - i) / 2; // Вычислить индекс середины m\n        if nums[m as usize] < target {\n            i = m + 1; // target находится в интервале [m+1, j]\n        } else if nums[m as usize] > target {\n            j = m - 1; // target находится в интервале [i, m-1]\n        } else {\n            j = m - 1; // Первый элемент меньше target находится в интервале [i, m-1]\n        }\n    }\n    // Вернуть точку вставки i\n    i\n}\n
        binary_search_insertion.c
        /* Бинарный поиск точки вставки (с повторяющимися элементами) */\nint binarySearchInsertion(int *nums, int numSize, int target) {\n    int i = 0, j = numSize - 1; // Инициализировать двусторонне замкнутый интервал [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Вычислить индекс середины m\n        if (nums[m] < target) {\n            i = m + 1; // target находится в интервале [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target находится в интервале [i, m-1]\n        } else {\n            j = m - 1; // Первый элемент меньше target находится в интервале [i, m-1]\n        }\n    }\n    // Вернуть точку вставки i\n    return i;\n}\n
        binary_search_insertion.kt
        /* Бинарный поиск точки вставки (с повторяющимися элементами) */\nfun binarySearchInsertion(nums: IntArray, target: Int): Int {\n    var i = 0\n    var j = nums.size - 1 // Инициализировать двусторонне замкнутый интервал [0, n-1]\n    while (i <= j) {\n        val m = i + (j - i) / 2 // Вычислить индекс середины m\n        if (nums[m] < target) {\n            i = m + 1 // target находится в интервале [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1 // target находится в интервале [i, m-1]\n        } else {\n            j = m - 1 // Первый элемент меньше target находится в интервале [i, m-1]\n        }\n    }\n    // Вернуть точку вставки i\n    return i\n}\n
        binary_search_insertion.rb
        ### Бинарный поиск точки вставки (с повторяющимися элементами) ###\ndef binary_search_insertion(nums, target)\n  # Инициализировать двусторонне замкнутый интервал [0, n-1]\n  i, j = 0, nums.length - 1\n\n  while i <= j\n    # Вычислить индекс середины m\n    m = (i + j) / 2\n\n    if nums[m] < target\n      i = m + 1 # target находится в интервале [m+1, j]\n    elsif nums[m] > target\n      j = m - 1 # target находится в интервале [i, m-1]\n    else\n      j = m - 1 # Первый элемент меньше target находится в интервале [i, m-1]\n    end\n  end\n\n  i # Вернуть точку вставки i\nend\n
        Визуализация кода

        Во весь экран >

        Tip

        Код в этом разделе записан в стиле \"двойного замкнутого интервала\". При желании можно самостоятельно реализовать вариант \"слева закрыт, справа открыт\".

        Если смотреть в целом, суть двоичного поиска сводится к тому, что для указателей \\(i\\) и \\(j\\) заранее задаются ориентиры поиска; целью может быть конкретный элемент, например target , а может быть и диапазон элементов, например все элементы, меньшие target .

        В ходе непрерывного двоичного деления указатели \\(i\\) и \\(j\\) постепенно приближаются к заранее заданной цели. В конце они либо успешно находят ответ, либо останавливаются после выхода за границы.

        ","path":["Глава 10. Поиск","10.2   Двоичный поиск точки вставки"],"tags":[]},{"location":"chapter_searching/replace_linear_by_hashing/","level":1,"title":"10.4   Стратегии оптимизации хеширования","text":"

        В алгоритмических задачах мы часто заменяем линейный поиск на хеш-поиск, чтобы уменьшить временную сложность алгоритма. Разберем одну задачу, чтобы лучше понять этот прием.

        Question

        Дан массив целых чисел nums и целевой элемент target . Найдите в массиве два элемента, сумма которых равна target , и верните их индексы. Подойдет любой корректный ответ.

        ","path":["Глава 10. Поиск","10.4   Стратегии оптимизации хеширования"],"tags":[]},{"location":"chapter_searching/replace_linear_by_hashing/#1041","level":2,"title":"10.4.1   Линейный поиск: обмен времени на пространство","text":"

        Рассмотрим прямой перебор всех возможных комбинаций. Как показано на рисунке 10-9, мы запускаем два вложенных цикла и на каждом шаге проверяем, равна ли сумма двух целых чисел target ; если да, то возвращаем их индексы.

        Рисунок 10-9   Линейный поиск для задачи о двух суммах

        Код приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby two_sum.py
        def two_sum_brute_force(nums: list[int], target: int) -> list[int]:\n    \"\"\"Метод 1: полный перебор\"\"\"\n    # Два вложенных цикла, временная сложность O(n^2)\n    for i in range(len(nums) - 1):\n        for j in range(i + 1, len(nums)):\n            if nums[i] + nums[j] == target:\n                return [i, j]\n    return []\n
        two_sum.cpp
        /* Метод 1: полный перебор */\nvector<int> twoSumBruteForce(vector<int> &nums, int target) {\n    int size = nums.size();\n    // Два вложенных цикла, временная сложность O(n^2)\n    for (int i = 0; i < size - 1; i++) {\n        for (int j = i + 1; j < size; j++) {\n            if (nums[i] + nums[j] == target)\n                return {i, j};\n        }\n    }\n    return {};\n}\n
        two_sum.java
        /* Метод 1: полный перебор */\nint[] twoSumBruteForce(int[] nums, int target) {\n    int size = nums.length;\n    // Два вложенных цикла, временная сложность O(n^2)\n    for (int i = 0; i < size - 1; i++) {\n        for (int j = i + 1; j < size; j++) {\n            if (nums[i] + nums[j] == target)\n                return new int[] { i, j };\n        }\n    }\n    return new int[0];\n}\n
        two_sum.cs
        /* Метод 1: полный перебор */\nint[] TwoSumBruteForce(int[] nums, int target) {\n    int size = nums.Length;\n    // Два вложенных цикла, временная сложность O(n^2)\n    for (int i = 0; i < size - 1; i++) {\n        for (int j = i + 1; j < size; j++) {\n            if (nums[i] + nums[j] == target)\n                return [i, j];\n        }\n    }\n    return [];\n}\n
        two_sum.go
        /* Метод 1: полный перебор */\nfunc twoSumBruteForce(nums []int, target int) []int {\n    size := len(nums)\n    // Два вложенных цикла, временная сложность O(n^2)\n    for i := 0; i < size-1; i++ {\n        for j := i + 1; j < size; j++ {\n            if nums[i]+nums[j] == target {\n                return []int{i, j}\n            }\n        }\n    }\n    return nil\n}\n
        two_sum.swift
        /* Метод 1: полный перебор */\nfunc twoSumBruteForce(nums: [Int], target: Int) -> [Int] {\n    // Два вложенных цикла, временная сложность O(n^2)\n    for i in nums.indices.dropLast() {\n        for j in nums.indices.dropFirst(i + 1) {\n            if nums[i] + nums[j] == target {\n                return [i, j]\n            }\n        }\n    }\n    return [0]\n}\n
        two_sum.js
        /* Метод 1: полный перебор */\nfunction twoSumBruteForce(nums, target) {\n    const n = nums.length;\n    // Два вложенных цикла, временная сложность O(n^2)\n    for (let i = 0; i < n; i++) {\n        for (let j = i + 1; j < n; j++) {\n            if (nums[i] + nums[j] === target) {\n                return [i, j];\n            }\n        }\n    }\n    return [];\n}\n
        two_sum.ts
        /* Метод 1: полный перебор */\nfunction twoSumBruteForce(nums: number[], target: number): number[] {\n    const n = nums.length;\n    // Два вложенных цикла, временная сложность O(n^2)\n    for (let i = 0; i < n; i++) {\n        for (let j = i + 1; j < n; j++) {\n            if (nums[i] + nums[j] === target) {\n                return [i, j];\n            }\n        }\n    }\n    return [];\n}\n
        two_sum.dart
        /* Способ 1: полный перебор */\nList<int> twoSumBruteForce(List<int> nums, int target) {\n  int size = nums.length;\n  // Два вложенных цикла, временная сложность O(n^2)\n  for (var i = 0; i < size - 1; i++) {\n    for (var j = i + 1; j < size; j++) {\n      if (nums[i] + nums[j] == target) return [i, j];\n    }\n  }\n  return [0];\n}\n
        two_sum.rs
        /* Метод 1: полный перебор */\npub fn two_sum_brute_force(nums: &Vec<i32>, target: i32) -> Option<Vec<i32>> {\n    let size = nums.len();\n    // Два вложенных цикла, временная сложность O(n^2)\n    for i in 0..size - 1 {\n        for j in i + 1..size {\n            if nums[i] + nums[j] == target {\n                return Some(vec![i as i32, j as i32]);\n            }\n        }\n    }\n    None\n}\n
        two_sum.c
        /* Метод 1: полный перебор */\nint *twoSumBruteForce(int *nums, int numsSize, int target, int *returnSize) {\n    for (int i = 0; i < numsSize; ++i) {\n        for (int j = i + 1; j < numsSize; ++j) {\n            if (nums[i] + nums[j] == target) {\n                int *res = malloc(sizeof(int) * 2);\n                res[0] = i, res[1] = j;\n                *returnSize = 2;\n                return res;\n            }\n        }\n    }\n    *returnSize = 0;\n    return NULL;\n}\n
        two_sum.kt
        /* Метод 1: полный перебор */\nfun twoSumBruteForce(nums: IntArray, target: Int): IntArray {\n    val size = nums.size\n    // Два вложенных цикла, временная сложность O(n^2)\n    for (i in 0..<size - 1) {\n        for (j in i + 1..<size) {\n            if (nums[i] + nums[j] == target) return intArrayOf(i, j)\n        }\n    }\n    return IntArray(0)\n}\n
        two_sum.rb
        ### Метод 1: полный перебор ###\ndef two_sum_brute_force(nums, target)\n  # Два вложенных цикла, временная сложность O(n^2)\n  for i in 0...(nums.length - 1)\n    for j in (i + 1)...nums.length\n      return [i, j] if nums[i] + nums[j] == target\n    end\n  end\n\n  []\nend\n
        Визуализация кода

        Во весь экран >

        Временная сложность этого метода равна \\(O(n^2)\\) , а пространственная сложность равна \\(O(1)\\) , поэтому на больших объемах данных он очень медленный.

        ","path":["Глава 10. Поиск","10.4   Стратегии оптимизации хеширования"],"tags":[]},{"location":"chapter_searching/replace_linear_by_hashing/#1042-","level":2,"title":"10.4.2   Хеш-поиск: обмен пространства на время","text":"

        Рассмотрим вариант с использованием хеш-таблицы, где ключами и значениями будут элементы массива и их индексы. При циклическом обходе массива на каждом шаге выполняются действия, показанные на рисунке 10-10.

        1. Проверить, находится ли число target - nums[i] в хеш-таблице; если да, то сразу вернуть индексы этих двух элементов.
        2. Добавить в хеш-таблицу пару из ключа nums[i] и индекса i .
        <1><2><3>

        Рисунок 10-10   Вспомогательная хеш-таблица для задачи о двух суммах

        Код реализации показан ниже, и для него достаточно одного цикла:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby two_sum.py
        def two_sum_hash_table(nums: list[int], target: int) -> list[int]:\n    \"\"\"Метод 2: вспомогательная хеш-таблица\"\"\"\n    # Вспомогательная хеш-таблица, пространственная сложность O(n)\n    dic = {}\n    # Один цикл, временная сложность O(n)\n    for i in range(len(nums)):\n        if target - nums[i] in dic:\n            return [dic[target - nums[i]], i]\n        dic[nums[i]] = i\n    return []\n
        two_sum.cpp
        /* Метод 2: вспомогательная хеш-таблица */\nvector<int> twoSumHashTable(vector<int> &nums, int target) {\n    int size = nums.size();\n    // Вспомогательная хеш-таблица, пространственная сложность O(n)\n    unordered_map<int, int> dic;\n    // Один цикл, временная сложность O(n)\n    for (int i = 0; i < size; i++) {\n        if (dic.find(target - nums[i]) != dic.end()) {\n            return {dic[target - nums[i]], i};\n        }\n        dic.emplace(nums[i], i);\n    }\n    return {};\n}\n
        two_sum.java
        /* Метод 2: вспомогательная хеш-таблица */\nint[] twoSumHashTable(int[] nums, int target) {\n    int size = nums.length;\n    // Вспомогательная хеш-таблица, пространственная сложность O(n)\n    Map<Integer, Integer> dic = new HashMap<>();\n    // Один цикл, временная сложность O(n)\n    for (int i = 0; i < size; i++) {\n        if (dic.containsKey(target - nums[i])) {\n            return new int[] { dic.get(target - nums[i]), i };\n        }\n        dic.put(nums[i], i);\n    }\n    return new int[0];\n}\n
        two_sum.cs
        /* Метод 2: вспомогательная хеш-таблица */\nint[] TwoSumHashTable(int[] nums, int target) {\n    int size = nums.Length;\n    // Вспомогательная хеш-таблица, пространственная сложность O(n)\n    Dictionary<int, int> dic = [];\n    // Один цикл, временная сложность O(n)\n    for (int i = 0; i < size; i++) {\n        if (dic.ContainsKey(target - nums[i])) {\n            return [dic[target - nums[i]], i];\n        }\n        dic.Add(nums[i], i);\n    }\n    return [];\n}\n
        two_sum.go
        /* Метод 2: вспомогательная хеш-таблица */\nfunc twoSumHashTable(nums []int, target int) []int {\n    // Вспомогательная хеш-таблица, пространственная сложность O(n)\n    hashTable := map[int]int{}\n    // Один цикл, временная сложность O(n)\n    for idx, val := range nums {\n        if preIdx, ok := hashTable[target-val]; ok {\n            return []int{preIdx, idx}\n        }\n        hashTable[val] = idx\n    }\n    return nil\n}\n
        two_sum.swift
        /* Метод 2: вспомогательная хеш-таблица */\nfunc twoSumHashTable(nums: [Int], target: Int) -> [Int] {\n    // Вспомогательная хеш-таблица, пространственная сложность O(n)\n    var dic: [Int: Int] = [:]\n    // Один цикл, временная сложность O(n)\n    for i in nums.indices {\n        if let j = dic[target - nums[i]] {\n            return [j, i]\n        }\n        dic[nums[i]] = i\n    }\n    return [0]\n}\n
        two_sum.js
        /* Метод 2: вспомогательная хеш-таблица */\nfunction twoSumHashTable(nums, target) {\n    // Вспомогательная хеш-таблица, пространственная сложность O(n)\n    let m = {};\n    // Один цикл, временная сложность O(n)\n    for (let i = 0; i < nums.length; i++) {\n        if (m[target - nums[i]] !== undefined) {\n            return [m[target - nums[i]], i];\n        } else {\n            m[nums[i]] = i;\n        }\n    }\n    return [];\n}\n
        two_sum.ts
        /* Метод 2: вспомогательная хеш-таблица */\nfunction twoSumHashTable(nums: number[], target: number): number[] {\n    // Вспомогательная хеш-таблица, пространственная сложность O(n)\n    let m: Map<number, number> = new Map();\n    // Один цикл, временная сложность O(n)\n    for (let i = 0; i < nums.length; i++) {\n        let index = m.get(target - nums[i]);\n        if (index !== undefined) {\n            return [index, i];\n        } else {\n            m.set(nums[i], i);\n        }\n    }\n    return [];\n}\n
        two_sum.dart
        /* Способ 2: вспомогательная хеш-таблица */\nList<int> twoSumHashTable(List<int> nums, int target) {\n  int size = nums.length;\n  // Вспомогательная хеш-таблица, пространственная сложность O(n)\n  Map<int, int> dic = HashMap();\n  // Один цикл, временная сложность O(n)\n  for (var i = 0; i < size; i++) {\n    if (dic.containsKey(target - nums[i])) {\n      return [dic[target - nums[i]]!, i];\n    }\n    dic.putIfAbsent(nums[i], () => i);\n  }\n  return [0];\n}\n
        two_sum.rs
        /* Метод 2: вспомогательная хеш-таблица */\npub fn two_sum_hash_table(nums: &Vec<i32>, target: i32) -> Option<Vec<i32>> {\n    // Вспомогательная хеш-таблица, пространственная сложность O(n)\n    let mut dic = HashMap::new();\n    // Один цикл, временная сложность O(n)\n    for (i, num) in nums.iter().enumerate() {\n        match dic.get(&(target - num)) {\n            Some(v) => return Some(vec![*v as i32, i as i32]),\n            None => dic.insert(num, i as i32),\n        };\n    }\n    None\n}\n
        two_sum.c
        /* Хеш-таблица */\ntypedef struct {\n    int key;\n    int val;\n    UT_hash_handle hh; // Реализовано на основе uthash.h\n} HashTable;\n\n/* Поиск в хеш-таблице */\nHashTable *find(HashTable *h, int key) {\n    HashTable *tmp;\n    HASH_FIND_INT(h, &key, tmp);\n    return tmp;\n}\n\n/* Вставка элемента в хеш-таблицу */\nvoid insert(HashTable **h, int key, int val) {\n    HashTable *t = find(*h, key);\n    if (t == NULL) {\n        HashTable *tmp = malloc(sizeof(HashTable));\n        tmp->key = key, tmp->val = val;\n        HASH_ADD_INT(*h, key, tmp);\n    } else {\n        t->val = val;\n    }\n}\n\n/* Метод 2: вспомогательная хеш-таблица */\nint *twoSumHashTable(int *nums, int numsSize, int target, int *returnSize) {\n    HashTable *hashtable = NULL;\n    for (int i = 0; i < numsSize; i++) {\n        HashTable *t = find(hashtable, target - nums[i]);\n        if (t != NULL) {\n            int *res = malloc(sizeof(int) * 2);\n            res[0] = t->val, res[1] = i;\n            *returnSize = 2;\n            return res;\n        }\n        insert(&hashtable, nums[i], i);\n    }\n    *returnSize = 0;\n    return NULL;\n}\n
        two_sum.kt
        /* Метод 2: вспомогательная хеш-таблица */\nfun twoSumHashTable(nums: IntArray, target: Int): IntArray {\n    val size = nums.size\n    // Вспомогательная хеш-таблица, пространственная сложность O(n)\n    val dic = HashMap<Int, Int>()\n    // Один цикл, временная сложность O(n)\n    for (i in 0..<size) {\n        if (dic.containsKey(target - nums[i])) {\n            return intArrayOf(dic[target - nums[i]]!!, i)\n        }\n        dic[nums[i]] = i\n    }\n    return IntArray(0)\n}\n
        two_sum.rb
        ### Метод 2: вспомогательная хеш-таблица ###\ndef two_sum_hash_table(nums, target)\n  # Вспомогательная хеш-таблица, пространственная сложность O(n)\n  dic = {}\n  # Один цикл, временная сложность O(n)\n  for i in 0...nums.length\n    return [dic[target - nums[i]], i] if dic.has_key?(target - nums[i])\n\n    dic[nums[i]] = i\n  end\n\n  []\nend\n
        Визуализация кода

        Во весь экран >

        Благодаря хеш-поиску этот метод снижает временную сложность с \\(O(n^2)\\) до \\(O(n)\\) , существенно повышая эффективность работы.

        Поскольку требуется поддерживать дополнительную хеш-таблицу, пространственная сложность составляет \\(O(n)\\) . Несмотря на это, в целом данный метод лучше сбалансирован по времени и памяти, поэтому именно он является оптимальным решением этой задачи.

        ","path":["Глава 10. Поиск","10.4   Стратегии оптимизации хеширования"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/","level":1,"title":"10.5   Переосмысление алгоритмов поиска","text":"

        Алгоритмы поиска (searching algorithm) используются для того, чтобы находить один или несколько элементов, удовлетворяющих определенным условиям, в структурах данных, таких как массивы, списки, деревья или графы.

        Алгоритмы поиска можно разделить на две категории по способу реализации.

        • Поиск целевого элемента путем обхода структуры данных, например обход массива, списка, дерева или графа.
        • Эффективный поиск элементов с использованием структуры организации данных или априорной информации, например двоичный поиск, хеш-поиск и поиск в двоичном дереве поиска.

        Нетрудно заметить, что эти темы уже рассматривались в предыдущих главах, поэтому алгоритмы поиска нам уже знакомы. В этом разделе мы систематизируем полученные ранее знания и еще раз посмотрим на них как на единую группу методов.

        ","path":["Глава 10. Поиск","10.5   Переосмысление алгоритмов поиска"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/#1051","level":2,"title":"10.5.1   Полный перебор","text":"

        Полный перебор заключается в том, что мы обходим каждый элемент структуры данных, чтобы найти целевой элемент.

        • \"Линейный поиск\" применяется к линейным структурам данных, таким как массивы и списки. Он начинается с одного конца структуры данных и последовательно проверяет элементы, пока не найдет целевой элемент или пока не достигнет другого конца структуры данных.
        • \"Обход в ширину\" и \"обход в глубину\" - это две стратегии обхода графов и деревьев. Обход в ширину стартует из начального узла и исследует все узлы текущего уровня, прежде чем переходить к следующему. Обход в глубину стартует из начального узла, проходит один путь до конца, затем возвращается назад и пробует другие пути, пока не будет полностью пройдена вся структура данных.

        Преимущество полного перебора состоит в его простоте и универсальности, поскольку он не требует предварительной обработки данных и использования дополнительных структур данных.

        Однако временная сложность таких алгоритмов равна \\(O(n)\\) , где \\(n\\) - число элементов, поэтому при больших объемах данных их производительность невысока.

        ","path":["Глава 10. Поиск","10.5   Переосмысление алгоритмов поиска"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/#1052","level":2,"title":"10.5.2   Адаптивный поиск","text":"

        Адаптивный поиск использует специфические свойства данных (например, упорядоченность), чтобы оптимизировать процесс поиска и тем самым эффективнее находить целевой элемент.

        • \"Двоичный поиск\" использует упорядоченность данных для эффективного поиска и применим только к массивам.
        • \"Хеш-поиск\" использует хеш-таблицу для построения отображения между поисковыми данными и целевыми данными, благодаря чему запросы выполняются эффективно.
        • \"Поиск в дереве\" ведется в конкретной древовидной структуре (например, в двоичном дереве поиска) и позволяет быстро отсекать узлы на основе сравнения значений, чтобы найти цель.

        Преимущество этих алгоритмов заключается в высокой эффективности: их временная сложность может достигать \\(O(\\log n)\\) и даже \\(O(1)\\) .

        Однако для использования таких алгоритмов обычно требуется предварительная обработка данных. Например, для двоичного поиска нужно заранее отсортировать массив, а хеш-поиск и поиск в дереве требуют дополнительных структур данных, поддержание которых тоже отнимает время и память.

        Tip

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

        ","path":["Глава 10. Поиск","10.5   Переосмысление алгоритмов поиска"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/#1053","level":2,"title":"10.5.3   Выбор метода поиска","text":"

        Для поиска целевого элемента в наборе данных размера \\(n\\) можно использовать линейный поиск, двоичный поиск, поиск в дереве, хеш-поиск и другие методы. Принципы работы этих методов показаны на рисунке 10-11.

        Рисунок 10-11   Различные стратегии поиска

        Эффективность и особенности перечисленных методов приведены в таблице 10-1.

        Таблица 10-1   Сравнение эффективности алгоритмов поиска

        Линейный поиск Двоичный поиск Поиск в дереве Хеш-поиск Поиск элемента \\(O(n)\\) \\(O(\\log n)\\) \\(O(\\log n)\\) \\(O(1)\\) Вставка элемента \\(O(1)\\) \\(O(n)\\) \\(O(\\log n)\\) \\(O(1)\\) Удаление элемента \\(O(n)\\) \\(O(n)\\) \\(O(\\log n)\\) \\(O(1)\\) Дополнительное пространство \\(O(1)\\) \\(O(1)\\) \\(O(n)\\) \\(O(n)\\) Предварительная обработка / Сортировка \\(O(n \\log n)\\) Построение дерева \\(O(n \\log n)\\) Построение хеш-таблицы \\(O(n)\\) Упорядоченность данных Не требуется Требуется Требуется Не требуется

        Выбор алгоритма поиска также зависит от масштаба данных, требований к производительности поиска, а также частоты запросов и обновлений данных.

        Линейный поиск

        • Обладает хорошей универсальностью и не требует никакой предварительной обработки данных. Если нужно выполнить только один запрос, то время предварительной обработки для остальных трех методов окажется больше, чем время линейного поиска.
        • Подходит для небольших объемов данных, потому что в этом случае влияние временной сложности на эффективность невелико.
        • Подходит для сценариев с высокой частотой обновления данных, поскольку этот метод не требует никакого дополнительного обслуживания данных.

        Двоичный поиск

        • Подходит для больших наборов данных и демонстрирует стабильную эффективность; его худшая временная сложность равна \\(O(\\log n)\\) .
        • Объем данных не должен быть слишком большим, потому что массив требует непрерывного участка памяти.
        • Не подходит для сценариев с частыми вставками и удалениями данных, так как поддержание массива в отсортированном виде требует больших затрат.

        Хеш-поиск

        • Подходит для сценариев, в которых требования к скорости запросов очень высоки; средняя временная сложность равна \\(O(1)\\) .
        • Не подходит для сценариев, где требуется упорядоченность данных или поиск по диапазону, потому что хеш-таблица не умеет поддерживать порядок данных.
        • Сильно зависит от хеш-функции и стратегии обработки коллизий, поэтому риск деградации производительности сравнительно велик.
        • Не подходит для слишком больших объемов данных, так как хеш-таблице требуется дополнительное пространство, чтобы максимально снизить число коллизий и обеспечить хорошую производительность поиска.

        Поиск в дереве

        • Подходит для очень больших объемов данных, потому что узлы дерева распределены в памяти и не требуют непрерывного хранения.
        • Подходит для сценариев, где нужно поддерживать упорядоченные данные или выполнять поиск по диапазону.
        • В процессе постоянных вставок и удалений узлов двоичное дерево поиска может перекоситься, и тогда временная сложность деградирует до \\(O(n)\\) .
        • Если использовать AVL-дерево или красно-черное дерево, то все операции могут стабильно выполняться за \\(O(\\log n)\\) , но поддержание баланса дерева увеличивает дополнительные накладные расходы.
        ","path":["Глава 10. Поиск","10.5   Переосмысление алгоритмов поиска"],"tags":[]},{"location":"chapter_searching/summary/","level":1,"title":"10.6   Резюме","text":"","path":["Глава 10. Поиск","10.6   Резюме"],"tags":[]},{"location":"chapter_searching/summary/#1","level":3,"title":"1.   Ключевые выводы","text":"
        • Двоичный поиск опирается на упорядоченность данных и выполняет поиск путем циклического сокращения интервала вдвое. Он требует упорядоченных входных данных и подходит только для массивов или структур данных, реализованных на их основе.
        • Полный перебор находит данные путем обхода структуры данных. Линейный поиск подходит для массивов и списков, а обход в ширину и обход в глубину подходят для графов и деревьев. Эти алгоритмы универсальны и не требуют предварительной обработки данных, но их временная сложность \\(O(n)\\) сравнительно велика.
        • Хеш-поиск, поиск в дереве и двоичный поиск относятся к эффективным методам поиска и позволяют быстро находить целевой элемент в конкретных структурах данных. Такие алгоритмы обладают высокой эффективностью, их временная сложность может достигать \\(O(\\log n)\\) и даже \\(O(1)\\) , но обычно им нужны дополнительные структуры данных.
        • На практике нужно анализировать размер данных, требования к производительности поиска, а также частоту запросов и обновлений данных, чтобы выбрать подходящий метод поиска.
        • Линейный поиск подходит для небольших или часто обновляемых наборов данных; двоичный поиск - для больших отсортированных данных; хеш-поиск - для сценариев с высокими требованиями к скорости запросов и без необходимости поиска по диапазону; поиск в дереве - для больших динамических данных, где нужно поддерживать порядок и выполнять диапазонные запросы.
        • Замена линейного поиска на хеш-поиск - это распространенная стратегия ускорения, которая позволяет снизить временную сложность с \\(O(n)\\) до \\(O(1)\\) .
        ","path":["Глава 10. Поиск","10.6   Резюме"],"tags":[]},{"location":"chapter_sorting/","level":1,"title":"Глава 11.   Сортировка","text":"

        Abstract

        Сортировка упорядочивает хаотичные данные и позволяет быстрее находить закономерности.

        За кажущейся простотой скрывается целая группа алгоритмов с разными достоинствами и ограничениями.

        ","path":["Глава 11. Сортировка","Глава 11.   Сортировка"],"tags":[]},{"location":"chapter_sorting/#_1","level":2,"title":"Содержание главы","text":"
        • 11.1   Алгоритмы сортировки
        • 11.2   Сортировка выбором
        • 11.3   Сортировка пузырьком
        • 11.4   Сортировка вставками
        • 11.5   Быстрая сортировка
        • 11.6   Сортировка слиянием
        • 11.7   Пирамидальная сортировка
        • 11.8   Блочная сортировка
        • 11.9   Сортировка подсчетом
        • 11.10   Поразрядная сортировка
        • 11.11   Резюме
        ","path":["Глава 11. Сортировка","Глава 11.   Сортировка"],"tags":[]},{"location":"chapter_sorting/bubble_sort/","level":1,"title":"11.3   Сортировка пузырьком","text":"

        Сортировка пузырьком (bubble sort) реализует сортировку путем последовательного сравнения и обмена соседних элементов. Этот процесс напоминает всплытие пузырьков снизу вверх, откуда и произошло название алгоритма.

        Как показано на рисунке 11-4, процесс \"всплытия\" можно смоделировать через операцию обмена элементов: начиная от левого края массива и двигаясь вправо, мы последовательно сравниваем соседние элементы и, если \"левый элемент > правый элемент\", меняем их местами. После завершения прохода максимальный элемент будет перемещен в самый правый конец массива.

        <1><2><3><4><5><6><7>

        Рисунок 11-4   Моделирование пузырька через обмен элементов

        ","path":["Глава 11. Сортировка","11.3   Сортировка пузырьком"],"tags":[]},{"location":"chapter_sorting/bubble_sort/#1131","level":2,"title":"11.3.1   Алгоритм","text":"

        Пусть длина массива равна \\(n\\) ; тогда шаги сортировки пузырьком показаны на рисунке 11-5.

        1. Сначала выполнить один проход \"всплытия\" по \\(n\\) элементам, переместив максимальный элемент массива на правильную позицию.
        2. Затем выполнить \"всплытие\" по оставшимся \\(n - 1\\) элементам, переместив второй по величине элемент на правильную позицию.
        3. Продолжать по аналогии; после \\(n - 1\\) раундов \"всплытия\" первые \\(n - 1\\) по величине элементы окажутся на правильных позициях.
        4. Оставшийся единственный элемент обязательно является минимальным, сортировать его уже не нужно, поэтому сортировка завершена.

        Рисунок 11-5   Процесс сортировки пузырьком

        Пример кода:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby bubble_sort.py
        def bubble_sort(nums: list[int]):\n    \"\"\"Пузырьковая сортировка\"\"\"\n    n = len(nums)\n    # Внешний цикл: неотсортированный диапазон [0, i]\n    for i in range(n - 1, 0, -1):\n        # Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for j in range(i):\n            if nums[j] > nums[j + 1]:\n                # Поменять местами nums[j] и nums[j + 1]\n                nums[j], nums[j + 1] = nums[j + 1], nums[j]\n
        bubble_sort.cpp
        /* Пузырьковая сортировка */\nvoid bubbleSort(vector<int> &nums) {\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (int i = nums.size() - 1; i > 0; i--) {\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Поменять местами nums[j] и nums[j + 1]\n                // Здесь используется функция std::swap()\n                swap(nums[j], nums[j + 1]);\n            }\n        }\n    }\n}\n
        bubble_sort.java
        /* Пузырьковая сортировка */\nvoid bubbleSort(int[] nums) {\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (int i = nums.length - 1; i > 0; i--) {\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Поменять местами nums[j] и nums[j + 1]\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n            }\n        }\n    }\n}\n
        bubble_sort.cs
        /* Пузырьковая сортировка */\nvoid BubbleSort(int[] nums) {\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (int i = nums.Length - 1; i > 0; i--) {\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Поменять местами nums[j] и nums[j + 1]\n                (nums[j + 1], nums[j]) = (nums[j], nums[j + 1]);\n            }\n        }\n    }\n}\n
        bubble_sort.go
        /* Пузырьковая сортировка */\nfunc bubbleSort(nums []int) {\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for i := len(nums) - 1; i > 0; i-- {\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for j := 0; j < i; j++ {\n            if nums[j] > nums[j+1] {\n                // Поменять местами nums[j] и nums[j + 1]\n                nums[j], nums[j+1] = nums[j+1], nums[j]\n            }\n        }\n    }\n}\n
        bubble_sort.swift
        /* Пузырьковая сортировка */\nfunc bubbleSort(nums: inout [Int]) {\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for i in nums.indices.dropFirst().reversed() {\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for j in 0 ..< i {\n            if nums[j] > nums[j + 1] {\n                // Поменять местами nums[j] и nums[j + 1]\n                nums.swapAt(j, j + 1)\n            }\n        }\n    }\n}\n
        bubble_sort.js
        /* Пузырьковая сортировка */\nfunction bubbleSort(nums) {\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Поменять местами nums[j] и nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n            }\n        }\n    }\n}\n
        bubble_sort.ts
        /* Пузырьковая сортировка */\nfunction bubbleSort(nums: number[]): void {\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Поменять местами nums[j] и nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n            }\n        }\n    }\n}\n
        bubble_sort.dart
        /* Пузырьковая сортировка */\nvoid bubbleSort(List<int> nums) {\n  // Внешний цикл: неотсортированный диапазон [0, i]\n  for (int i = nums.length - 1; i > 0; i--) {\n    // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n    for (int j = 0; j < i; j++) {\n      if (nums[j] > nums[j + 1]) {\n        // Поменять местами nums[j] и nums[j + 1]\n        int tmp = nums[j];\n        nums[j] = nums[j + 1];\n        nums[j + 1] = tmp;\n      }\n    }\n  }\n}\n
        bubble_sort.rs
        /* Пузырьковая сортировка */\nfn bubble_sort(nums: &mut [i32]) {\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for i in (1..nums.len()).rev() {\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for j in 0..i {\n            if nums[j] > nums[j + 1] {\n                // Поменять местами nums[j] и nums[j + 1]\n                nums.swap(j, j + 1);\n            }\n        }\n    }\n}\n
        bubble_sort.c
        /* Пузырьковая сортировка */\nvoid bubbleSort(int nums[], int size) {\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (int i = size - 1; i > 0; i--) {\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                int temp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = temp;\n            }\n        }\n    }\n}\n
        bubble_sort.kt
        /* Пузырьковая сортировка */\nfun bubbleSort(nums: IntArray) {\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (i in nums.size - 1 downTo 1) {\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (j in 0..<i) {\n            if (nums[j] > nums[j + 1]) {\n                // Поменять местами nums[j] и nums[j + 1]\n                val temp = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = temp\n            }\n        }\n    }\n}\n
        bubble_sort.rb
        ### Пузырьковая сортировка ###\ndef bubble_sort(nums)\n  n = nums.length\n  # Внешний цикл: неотсортированный диапазон [0, i]\n  for i in (n - 1).downto(1)\n    # Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # Поменять местами nums[j] и nums[j + 1]\n        nums[j], nums[j + 1] = nums[j + 1], nums[j]\n      end\n    end\n  end\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 11. Сортировка","11.3   Сортировка пузырьком"],"tags":[]},{"location":"chapter_sorting/bubble_sort/#1132","level":2,"title":"11.3.2   Оптимизация эффективности","text":"

        Если в каком-либо раунде \"всплытия\" не произошло ни одного обмена, значит, массив уже отсортирован и можно сразу вернуть результат. Поэтому можно добавить флаг flag для отслеживания этой ситуации и немедленного выхода.

        После такой оптимизации худшая и средняя временные сложности сортировки пузырьком по-прежнему равны \\(O(n^2)\\) ; однако если входной массив уже полностью упорядочен, достигается лучшая временная сложность \\(O(n)\\) .

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby bubble_sort.py
        def bubble_sort_with_flag(nums: list[int]):\n    \"\"\"Пузырьковая сортировка (оптимизация флагом)\"\"\"\n    n = len(nums)\n    # Внешний цикл: неотсортированный диапазон [0, i]\n    for i in range(n - 1, 0, -1):\n        flag = False  # Инициализировать флаг\n        # Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for j in range(i):\n            if nums[j] > nums[j + 1]:\n                # Поменять местами nums[j] и nums[j + 1]\n                nums[j], nums[j + 1] = nums[j + 1], nums[j]\n                flag = True  # Записать обмен элементов\n        if not flag:\n            break  # На этой итерации «всплытия» не было ни одного обмена, сразу выйти\n
        bubble_sort.cpp
        /* Пузырьковая сортировка (оптимизация флагом) */\nvoid bubbleSortWithFlag(vector<int> &nums) {\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (int i = nums.size() - 1; i > 0; i--) {\n        bool flag = false; // Инициализировать флаг\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Поменять местами nums[j] и nums[j + 1]\n                // Здесь используется функция std::swap()\n                swap(nums[j], nums[j + 1]);\n                flag = true; // Записать обмен элементов\n            }\n        }\n        if (!flag)\n            break; // На этой итерации «всплытия» не было ни одного обмена, сразу выйти\n    }\n}\n
        bubble_sort.java
        /* Пузырьковая сортировка (оптимизация флагом) */\nvoid bubbleSortWithFlag(int[] nums) {\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (int i = nums.length - 1; i > 0; i--) {\n        boolean flag = false; // Инициализировать флаг\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Поменять местами nums[j] и nums[j + 1]\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                flag = true; // Записать обмен элементов\n            }\n        }\n        if (!flag)\n            break; // На этой итерации «всплытия» не было ни одного обмена, сразу выйти\n    }\n}\n
        bubble_sort.cs
        /* Пузырьковая сортировка (оптимизация флагом) */\nvoid BubbleSortWithFlag(int[] nums) {\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (int i = nums.Length - 1; i > 0; i--) {\n        bool flag = false; // Инициализировать флаг\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Поменять местами nums[j] и nums[j + 1]\n                (nums[j + 1], nums[j]) = (nums[j], nums[j + 1]);\n                flag = true;  // Записать обмен элементов\n            }\n        }\n        if (!flag) break;     // На этой итерации «всплытия» не было ни одного обмена, сразу выйти\n    }\n}\n
        bubble_sort.go
        /* Пузырьковая сортировка (оптимизация флагом) */\nfunc bubbleSortWithFlag(nums []int) {\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for i := len(nums) - 1; i > 0; i-- {\n        flag := false // Инициализировать флаг\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for j := 0; j < i; j++ {\n            if nums[j] > nums[j+1] {\n                // Поменять местами nums[j] и nums[j + 1]\n                nums[j], nums[j+1] = nums[j+1], nums[j]\n                flag = true // Записать обмен элементов\n            }\n        }\n        if flag == false { // На этой итерации «всплытия» не было ни одного обмена, сразу выйти\n            break\n        }\n    }\n}\n
        bubble_sort.swift
        /* Пузырьковая сортировка (оптимизация флагом) */\nfunc bubbleSortWithFlag(nums: inout [Int]) {\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for i in nums.indices.dropFirst().reversed() {\n        var flag = false // Инициализировать флаг\n        for j in 0 ..< i {\n            if nums[j] > nums[j + 1] {\n                // Поменять местами nums[j] и nums[j + 1]\n                nums.swapAt(j, j + 1)\n                flag = true // Записать обмен элементов\n            }\n        }\n        if !flag { // На этой итерации «всплытия» не было ни одного обмена, сразу выйти\n            break\n        }\n    }\n}\n
        bubble_sort.js
        /* Пузырьковая сортировка (оптимизация флагом) */\nfunction bubbleSortWithFlag(nums) {\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        let flag = false; // Инициализировать флаг\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Поменять местами nums[j] и nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                flag = true; // Записать обмен элементов\n            }\n        }\n        if (!flag) break; // На этой итерации «всплытия» не было ни одного обмена, сразу выйти\n    }\n}\n
        bubble_sort.ts
        /* Пузырьковая сортировка (оптимизация флагом) */\nfunction bubbleSortWithFlag(nums: number[]): void {\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        let flag = false; // Инициализировать флаг\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Поменять местами nums[j] и nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                flag = true; // Записать обмен элементов\n            }\n        }\n        if (!flag) break; // На этой итерации «всплытия» не было ни одного обмена, сразу выйти\n    }\n}\n
        bubble_sort.dart
        /* Пузырьковая сортировка (оптимизация флагом) */\nvoid bubbleSortWithFlag(List<int> nums) {\n  // Внешний цикл: неотсортированный диапазон [0, i]\n  for (int i = nums.length - 1; i > 0; i--) {\n    bool flag = false; // Инициализировать флаг\n    // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n    for (int j = 0; j < i; j++) {\n      if (nums[j] > nums[j + 1]) {\n        // Поменять местами nums[j] и nums[j + 1]\n        int tmp = nums[j];\n        nums[j] = nums[j + 1];\n        nums[j + 1] = tmp;\n        flag = true; // Записать обмен элементов\n      }\n    }\n    if (!flag) break; // На этой итерации «всплытия» не было ни одного обмена, сразу выйти\n  }\n}\n
        bubble_sort.rs
        /* Пузырьковая сортировка (оптимизация флагом) */\nfn bubble_sort_with_flag(nums: &mut [i32]) {\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for i in (1..nums.len()).rev() {\n        let mut flag = false; // Инициализировать флаг\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for j in 0..i {\n            if nums[j] > nums[j + 1] {\n                // Поменять местами nums[j] и nums[j + 1]\n                nums.swap(j, j + 1);\n                flag = true; // Записать обмен элементов\n            }\n        }\n        if !flag {\n            break; // На этой итерации «всплытия» не было ни одного обмена, сразу выйти\n        };\n    }\n}\n
        bubble_sort.c
        /* Пузырьковая сортировка (оптимизация флагом) */\nvoid bubbleSortWithFlag(int nums[], int size) {\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (int i = size - 1; i > 0; i--) {\n        bool flag = false;\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                int temp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = temp;\n                flag = true;\n            }\n        }\n        if (!flag)\n            break;\n    }\n}\n
        bubble_sort.kt
        /* Пузырьковая сортировка (оптимизация флагом) */\nfun bubbleSortWithFlag(nums: IntArray) {\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (i in nums.size - 1 downTo 1) {\n        var flag = false // Инициализировать флаг\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (j in 0..<i) {\n            if (nums[j] > nums[j + 1]) {\n                // Поменять местами nums[j] и nums[j + 1]\n                val temp = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = temp\n                flag = true // Записать обмен элементов\n            }\n        }\n        if (!flag) break // На этой итерации «всплытия» не было ни одного обмена, сразу выйти\n    }\n}\n
        bubble_sort.rb
        ### Пузырьковая сортировка ###\ndef bubble_sort(nums)\n  n = nums.length\n  # Внешний цикл: неотсортированный диапазон [0, i]\n  for i in (n - 1).downto(1)\n    # Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # Поменять местами nums[j] и nums[j + 1]\n        nums[j], nums[j + 1] = nums[j + 1], nums[j]\n      end\n    end\n  end\nend\n\n# ## Пузырьковая сортировка (оптимизация флагом) ###\ndef bubble_sort_with_flag(nums)\n  n = nums.length\n  # Внешний цикл: неотсортированный диапазон [0, i]\n  for i in (n - 1).downto(1)\n    flag = false # Инициализировать флаг\n\n    # Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # Поменять местами nums[j] и nums[j + 1]\n        nums[j], nums[j + 1] = nums[j + 1], nums[j]\n        flag = true # Записать обмен элементов\n      end\n    end\n\n    break unless flag # На этой итерации «всплытия» не было ни одного обмена, сразу выйти\n  end\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 11. Сортировка","11.3   Сортировка пузырьком"],"tags":[]},{"location":"chapter_sorting/bubble_sort/#1133","level":2,"title":"11.3.3   Характеристики алгоритма","text":"
        • Временная сложность равна \\(O(n^2)\\), алгоритм адаптивен: длины диапазонов, проходящих \"всплытие\" в разных раундах, последовательно равны \\(n - 1\\), \\(n - 2\\), \\(\\dots\\), \\(2\\), \\(1\\) , а их сумма равна \\((n - 1) n / 2\\) . После добавления оптимизации с flag лучшая временная сложность может достигать \\(O(n)\\) .
        • Пространственная сложность равна \\(O(1)\\), сортировка выполняется на месте: указатели \\(i\\) и \\(j\\) используют константный объем дополнительной памяти.
        • Стабильная сортировка: поскольку при \"всплытии\" равные элементы не обмениваются местами.
        ","path":["Глава 11. Сортировка","11.3   Сортировка пузырьком"],"tags":[]},{"location":"chapter_sorting/bucket_sort/","level":1,"title":"11.8   Блочная сортировка","text":"

        Рассмотренные выше алгоритмы сортировки относятся к \"сортировкам на основе сравнений\": они упорядочивают данные, сравнивая элементы друг с другом. Временная сложность таких алгоритмов не может быть лучше \\(O(n \\log n)\\) . Далее мы рассмотрим несколько \"сортировок без сравнений\", чья временная сложность может достигать линейного порядка.

        Блочная сортировка (bucket sort) является типичным применением стратегии \"разделяй и властвуй\". Она создает набор упорядоченных по величине блоков, где каждый блок соответствует определенному диапазону данных; затем элементы равномерно распределяются по этим блокам, внутри каждого блока отдельно выполняется сортировка, а в конце результаты объединяются в порядке блоков.

        ","path":["Глава 11. Сортировка","11.8   Блочная сортировка"],"tags":[]},{"location":"chapter_sorting/bucket_sort/#1181","level":2,"title":"11.8.1   Алгоритм","text":"

        Рассмотрим массив длины \\(n\\), элементы которого являются числами с плавающей запятой из диапазона \\([0, 1)\\) . Процесс блочной сортировки показан на рисунке 11-13.

        1. Инициализировать \\(k\\) блоков и распределить \\(n\\) элементов по этим \\(k\\) блокам.
        2. Отсортировать каждый блок по отдельности (здесь используется встроенная функция сортировки языка программирования).
        3. Объединить результаты в порядке следования блоков от меньшего к большему.

        Рисунок 11-13   Процесс блочной сортировки

        Код приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby bucket_sort.py
        def bucket_sort(nums: list[float]):\n    \"\"\"Сортировка корзинами\"\"\"\n    # Инициализировать k = n/2 корзин, предполагая распределение 2 элементов в каждую корзину\n    k = len(nums) // 2\n    buckets = [[] for _ in range(k)]\n    # 1. Распределить элементы массива по корзинам\n    for num in nums:\n        # Входные данные лежат в диапазоне [0, 1); использовать num * k для отображения в диапазон индексов [0, k-1]\n        i = int(num * k)\n        # Добавить num в корзину i\n        buckets[i].append(num)\n    # 2. Выполнить сортировку внутри каждой корзины\n    for bucket in buckets:\n        # Использовать встроенную функцию сортировки; ее также можно заменить другим алгоритмом сортировки\n        bucket.sort()\n    # 3. Обойти корзины и объединить результаты\n    i = 0\n    for bucket in buckets:\n        for num in bucket:\n            nums[i] = num\n            i += 1\n
        bucket_sort.cpp
        /* Сортировка корзинами */\nvoid bucketSort(vector<float> &nums) {\n    // Инициализировать k = n/2 корзин, предполагая распределение 2 элементов в каждую корзину\n    int k = nums.size() / 2;\n    vector<vector<float>> buckets(k);\n    // 1. Распределить элементы массива по корзинам\n    for (float num : nums) {\n        // Входные данные лежат в диапазоне [0, 1); использовать num * k для отображения в диапазон индексов [0, k-1]\n        int i = num * k;\n        // Добавить num в корзину bucket_idx\n        buckets[i].push_back(num);\n    }\n    // 2. Выполнить сортировку внутри каждой корзины\n    for (vector<float> &bucket : buckets) {\n        // Использовать встроенную функцию сортировки; ее также можно заменить другим алгоритмом сортировки\n        sort(bucket.begin(), bucket.end());\n    }\n    // 3. Обойти корзины и объединить результаты\n    int i = 0;\n    for (vector<float> &bucket : buckets) {\n        for (float num : bucket) {\n            nums[i++] = num;\n        }\n    }\n}\n
        bucket_sort.java
        /* Сортировка корзинами */\nvoid bucketSort(float[] nums) {\n    // Инициализировать k = n/2 корзин, предполагая распределение 2 элементов в каждую корзину\n    int k = nums.length / 2;\n    List<List<Float>> buckets = new ArrayList<>();\n    for (int i = 0; i < k; i++) {\n        buckets.add(new ArrayList<>());\n    }\n    // 1. Распределить элементы массива по корзинам\n    for (float num : nums) {\n        // Входные данные лежат в диапазоне [0, 1); использовать num * k для отображения в диапазон индексов [0, k-1]\n        int i = (int) (num * k);\n        // Добавить num в корзину i\n        buckets.get(i).add(num);\n    }\n    // 2. Выполнить сортировку внутри каждой корзины\n    for (List<Float> bucket : buckets) {\n        // Использовать встроенную функцию сортировки; ее также можно заменить другим алгоритмом сортировки\n        Collections.sort(bucket);\n    }\n    // 3. Обойти корзины и объединить результаты\n    int i = 0;\n    for (List<Float> bucket : buckets) {\n        for (float num : bucket) {\n            nums[i++] = num;\n        }\n    }\n}\n
        bucket_sort.cs
        /* Сортировка корзинами */\nvoid BucketSort(float[] nums) {\n    // Инициализировать k = n/2 корзин, предполагая распределение 2 элементов в каждую корзину\n    int k = nums.Length / 2;\n    List<List<float>> buckets = [];\n    for (int i = 0; i < k; i++) {\n        buckets.Add([]);\n    }\n    // 1. Распределить элементы массива по корзинам\n    foreach (float num in nums) {\n        // Входные данные лежат в диапазоне [0, 1); использовать num * k для отображения в диапазон индексов [0, k-1]\n        int i = (int)(num * k);\n        // Добавить num в корзину i\n        buckets[i].Add(num);\n    }\n    // 2. Выполнить сортировку внутри каждой корзины\n    foreach (List<float> bucket in buckets) {\n        // Использовать встроенную функцию сортировки; ее также можно заменить другим алгоритмом сортировки\n        bucket.Sort();\n    }\n    // 3. Обойти корзины и объединить результаты\n    int j = 0;\n    foreach (List<float> bucket in buckets) {\n        foreach (float num in bucket) {\n            nums[j++] = num;\n        }\n    }\n}\n
        bucket_sort.go
        /* Сортировка корзинами */\nfunc bucketSort(nums []float64) {\n    // Инициализировать k = n/2 корзин, предполагая распределение 2 элементов в каждую корзину\n    k := len(nums) / 2\n    buckets := make([][]float64, k)\n    for i := 0; i < k; i++ {\n        buckets[i] = make([]float64, 0)\n    }\n    // 1. Распределить элементы массива по корзинам\n    for _, num := range nums {\n        // Входные данные лежат в диапазоне [0, 1); использовать num * k для отображения в диапазон индексов [0, k-1]\n        i := int(num * float64(k))\n        // Добавить num в корзину i\n        buckets[i] = append(buckets[i], num)\n    }\n    // 2. Выполнить сортировку внутри каждой корзины\n    for i := 0; i < k; i++ {\n        // Использовать встроенную функцию сортировки среза; ее также можно заменить другим алгоритмом сортировки\n        sort.Float64s(buckets[i])\n    }\n    // 3. Обойти корзины и объединить результаты\n    i := 0\n    for _, bucket := range buckets {\n        for _, num := range bucket {\n            nums[i] = num\n            i++\n        }\n    }\n}\n
        bucket_sort.swift
        /* Сортировка корзинами */\nfunc bucketSort(nums: inout [Double]) {\n    // Инициализировать k = n/2 корзин, предполагая распределение 2 элементов в каждую корзину\n    let k = nums.count / 2\n    var buckets = (0 ..< k).map { _ in [Double]() }\n    // 1. Распределить элементы массива по корзинам\n    for num in nums {\n        // Входные данные лежат в диапазоне [0, 1); использовать num * k для отображения в диапазон индексов [0, k-1]\n        let i = Int(num * Double(k))\n        // Добавить num в корзину i\n        buckets[i].append(num)\n    }\n    // 2. Выполнить сортировку внутри каждой корзины\n    for i in buckets.indices {\n        // Использовать встроенную функцию сортировки; ее также можно заменить другим алгоритмом сортировки\n        buckets[i].sort()\n    }\n    // 3. Обойти корзины и объединить результаты\n    var i = nums.startIndex\n    for bucket in buckets {\n        for num in bucket {\n            nums[i] = num\n            i += 1\n        }\n    }\n}\n
        bucket_sort.js
        /* Сортировка корзинами */\nfunction bucketSort(nums) {\n    // Инициализировать k = n/2 корзин, предполагая распределение 2 элементов в каждую корзину\n    const k = nums.length / 2;\n    const buckets = [];\n    for (let i = 0; i < k; i++) {\n        buckets.push([]);\n    }\n    // 1. Распределить элементы массива по корзинам\n    for (const num of nums) {\n        // Входные данные лежат в диапазоне [0, 1); использовать num * k для отображения в диапазон индексов [0, k-1]\n        const i = Math.floor(num * k);\n        // Добавить num в корзину i\n        buckets[i].push(num);\n    }\n    // 2. Выполнить сортировку внутри каждой корзины\n    for (const bucket of buckets) {\n        // Использовать встроенную функцию сортировки; ее также можно заменить другим алгоритмом сортировки\n        bucket.sort((a, b) => a - b);\n    }\n    // 3. Обойти корзины и объединить результаты\n    let i = 0;\n    for (const bucket of buckets) {\n        for (const num of bucket) {\n            nums[i++] = num;\n        }\n    }\n}\n
        bucket_sort.ts
        /* Сортировка корзинами */\nfunction bucketSort(nums: number[]): void {\n    // Инициализировать k = n/2 корзин, предполагая распределение 2 элементов в каждую корзину\n    const k = nums.length / 2;\n    const buckets: number[][] = [];\n    for (let i = 0; i < k; i++) {\n        buckets.push([]);\n    }\n    // 1. Распределить элементы массива по корзинам\n    for (const num of nums) {\n        // Входные данные лежат в диапазоне [0, 1); использовать num * k для отображения в диапазон индексов [0, k-1]\n        const i = Math.floor(num * k);\n        // Добавить num в корзину i\n        buckets[i].push(num);\n    }\n    // 2. Выполнить сортировку внутри каждой корзины\n    for (const bucket of buckets) {\n        // Использовать встроенную функцию сортировки; ее также можно заменить другим алгоритмом сортировки\n        bucket.sort((a, b) => a - b);\n    }\n    // 3. Обойти корзины и объединить результаты\n    let i = 0;\n    for (const bucket of buckets) {\n        for (const num of bucket) {\n            nums[i++] = num;\n        }\n    }\n}\n
        bucket_sort.dart
        /* Сортировка корзинами */\nvoid bucketSort(List<double> nums) {\n  // Инициализировать k = n/2 корзин, предполагая распределение 2 элементов в каждую корзину\n  int k = nums.length ~/ 2;\n  List<List<double>> buckets = List.generate(k, (index) => []);\n\n  // 1. Распределить элементы массива по корзинам\n  for (double _num in nums) {\n    // Входные данные находятся в диапазоне [0, 1), используем _num * k для отображения в диапазон индексов [0, k-1]\n    int i = (_num * k).toInt();\n    // Добавить _num в корзину bucket_idx\n    buckets[i].add(_num);\n  }\n  // 2. Выполнить сортировку внутри каждой корзины\n  for (List<double> bucket in buckets) {\n    bucket.sort();\n  }\n  // 3. Обойти корзины и объединить результаты\n  int i = 0;\n  for (List<double> bucket in buckets) {\n    for (double _num in bucket) {\n      nums[i++] = _num;\n    }\n  }\n}\n
        bucket_sort.rs
        /* Сортировка корзинами */\nfn bucket_sort(nums: &mut [f64]) {\n    // Инициализировать k = n/2 корзин, предполагая распределение 2 элементов в каждую корзину\n    let k = nums.len() / 2;\n    let mut buckets = vec![vec![]; k];\n    // 1. Распределить элементы массива по корзинам\n    for &num in nums.iter() {\n        // Входные данные лежат в диапазоне [0, 1); использовать num * k для отображения в диапазон индексов [0, k-1]\n        let i = (num * k as f64) as usize;\n        // Добавить num в корзину i\n        buckets[i].push(num);\n    }\n    // 2. Выполнить сортировку внутри каждой корзины\n    for bucket in &mut buckets {\n        // Использовать встроенную функцию сортировки; ее также можно заменить другим алгоритмом сортировки\n        bucket.sort_by(|a, b| a.partial_cmp(b).unwrap());\n    }\n    // 3. Обойти корзины и объединить результаты\n    let mut i = 0;\n    for bucket in buckets.iter() {\n        for &num in bucket.iter() {\n            nums[i] = num;\n            i += 1;\n        }\n    }\n}\n
        bucket_sort.c
        /* Сортировка корзинами */\nvoid bucketSort(float nums[], int n) {\n    int k = n / 2;                                 // Инициализировать k = n/2 корзин\n    int *sizes = malloc(k * sizeof(int));          // Записать размер каждой корзины\n    float **buckets = malloc(k * sizeof(float *)); // Массив динамических массивов (корзины)\n    // Предварительно выделить достаточно места для каждой корзины\n    for (int i = 0; i < k; ++i) {\n        buckets[i] = (float *)malloc(n * sizeof(float));\n        sizes[i] = 0;\n    }\n    // 1. Распределить элементы массива по корзинам\n    for (int i = 0; i < n; ++i) {\n        int idx = (int)(nums[i] * k);\n        buckets[idx][sizes[idx]++] = nums[i];\n    }\n    // 2. Выполнить сортировку внутри каждой корзины\n    for (int i = 0; i < k; ++i) {\n        qsort(buckets[i], sizes[i], sizeof(float), compare);\n    }\n    // 3. Объединить отсортированные корзины\n    int idx = 0;\n    for (int i = 0; i < k; ++i) {\n        for (int j = 0; j < sizes[i]; ++j) {\n            nums[idx++] = buckets[i][j];\n        }\n        // Освободить память\n        free(buckets[i]);\n    }\n}\n
        bucket_sort.kt
        /* Сортировка корзинами */\nfun bucketSort(nums: FloatArray) {\n    // Инициализировать k = n/2 корзин, предполагая распределение 2 элементов в каждую корзину\n    val k = nums.size / 2\n    val buckets = mutableListOf<MutableList<Float>>()\n    for (i in 0..<k) {\n        buckets.add(mutableListOf())\n    }\n    // 1. Распределить элементы массива по корзинам\n    for (num in nums) {\n        // Входные данные лежат в диапазоне [0, 1); использовать num * k для отображения в диапазон индексов [0, k-1]\n        val i = (num * k).toInt()\n        // Добавить num в корзину i\n        buckets[i].add(num)\n    }\n    // 2. Выполнить сортировку внутри каждой корзины\n    for (bucket in buckets) {\n        // Использовать встроенную функцию сортировки; ее также можно заменить другим алгоритмом сортировки\n        bucket.sort()\n    }\n    // 3. Обойти корзины и объединить результаты\n    var i = 0\n    for (bucket in buckets) {\n        for (num in bucket) {\n            nums[i++] = num\n        }\n    }\n}\n
        bucket_sort.rb
        ### Сортировка корзинами ###\ndef bucket_sort(nums)\n  # Инициализировать k = n/2 корзин, предполагая распределение 2 элементов в каждую корзину\n  k = nums.length / 2\n  buckets = Array.new(k) { [] }\n\n  # 1. Распределить элементы массива по корзинам\n  nums.each do |num|\n    # Входные данные лежат в диапазоне [0, 1); использовать num * k для отображения в диапазон индексов [0, k-1]\n    i = (num * k).to_i\n    # Добавить num в корзину i\n    buckets[i] << num\n  end\n\n  # 2. Выполнить сортировку внутри каждой корзины\n  buckets.each do |bucket|\n    # Использовать встроенную функцию сортировки; ее также можно заменить другим алгоритмом сортировки\n    bucket.sort!\n  end\n\n  # 3. Обойти корзины и объединить результаты\n  i = 0\n  buckets.each do |bucket|\n    bucket.each do |num|\n      nums[i] = num\n      i += 1\n    end\n  end\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 11. Сортировка","11.8   Блочная сортировка"],"tags":[]},{"location":"chapter_sorting/bucket_sort/#1182","level":2,"title":"11.8.2   Характеристики алгоритма","text":"

        Блочная сортировка подходит для обработки очень больших объемов данных. Например, если вход содержит 1 миллион элементов и из-за ограничений памяти система не может загрузить их все сразу, можно разбить данные на 1000 блоков, отсортировать каждый блок отдельно, а затем объединить результаты.

        • Временная сложность равна \\(O(n + k)\\) : если элементы распределены по блокам равномерно, то в каждом блоке будет \\(\\frac{n}{k}\\) элементов. Если сортировка одного блока требует \\(O(\\frac{n}{k} \\log\\frac{n}{k})\\) времени, то сортировка всех блоков потребует \\(O(n \\log\\frac{n}{k})\\) времени. Когда число блоков \\(k\\) достаточно велико, временная сложность приближается к \\(O(n)\\) . На объединение результатов требуется \\(O(n + k)\\) времени, потому что нужно пройти по всем блокам и элементам. В худшем случае все данные попадут в один блок, и если сортировка этого блока использует \\(O(n^2)\\) времени, общая сложность также станет \\(O(n^2)\\) .
        • Пространственная сложность равна \\(O(n + k)\\), сортировка не выполняется на месте: требуются дополнительные блоки в количестве \\(k\\) и место для всех \\(n\\) элементов внутри них.
        • Является ли блочная сортировка стабильной, зависит от того, стабилен ли алгоритм сортировки внутри каждого блока.
        ","path":["Глава 11. Сортировка","11.8   Блочная сортировка"],"tags":[]},{"location":"chapter_sorting/bucket_sort/#1183","level":2,"title":"11.8.3   Как добиться равномерного распределения","text":"

        Теоретически временная сложность блочной сортировки может достигать \\(O(n)\\) ; ключ к этому - как можно более равномерно распределить элементы по блокам. На практике данные часто распределены неравномерно. Например, если нужно распределить все товары на маркетплейсе по 10 ценовым блокам, количество товаров дешевле 100 рублей может быть очень большим, а товаров дороже 1000 рублей - очень маленьким. Если просто разбить диапазон цен на 10 равных частей, число товаров в каждом блоке будет сильно различаться.

        Чтобы добиться более равномерного распределения, можно сначала задать грубую линию раздела и приблизительно распределить данные по 3 блокам. После этого блоки с большим числом товаров можно снова делить на 3 блока и продолжать процесс до тех пор, пока число элементов в каждом блоке не станет примерно одинаковым.

        Как показано на рисунке 11-14, по сути этот метод строит рекурсивное дерево, цель которого - сделать значения в листьях как можно более равномерными. Конечно, совсем не обязательно каждый раз делить данные именно на 3 блока; конкретную схему разбиения можно выбирать в зависимости от свойств данных.

        Рисунок 11-14   Рекурсивное разбиение по блокам

        Если нам заранее известна вероятностная модель распределения цен товаров, то границы цен для каждого блока можно задавать в соответствии с этим распределением. Важно отметить, что фактическое распределение данных не обязательно специально измерять - его можно приблизить некоторой вероятностной моделью исходя из свойств данных.

        Как показано на рисунке 11-15, если предположить, что цены товаров подчиняются нормальному распределению, то можно разумно задать интервалы цен и тем самым распределить товары по блокам достаточно равномерно.

        Рисунок 11-15   Разбиение блоков по вероятностному распределению

        ","path":["Глава 11. Сортировка","11.8   Блочная сортировка"],"tags":[]},{"location":"chapter_sorting/counting_sort/","level":1,"title":"11.9   Сортировка подсчетом","text":"

        Сортировка подсчетом (counting sort) реализует сортировку за счет подсчета количества вхождений элементов и обычно используется для массивов целых чисел.

        ","path":["Глава 11. Сортировка","11.9   Сортировка подсчетом"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1191","level":2,"title":"11.9.1   Простая реализация","text":"

        Сначала рассмотрим простой пример. Дан массив nums длины \\(n\\) , элементы которого являются \"неотрицательными целыми числами\". Общий процесс сортировки подсчетом показан на рисунке 11-16.

        1. Пройти по массиву, найти в нем максимальное число, обозначить его как \\(m\\) , а затем создать вспомогательный массив counter длины \\(m + 1\\) .
        2. С помощью counter подсчитать, сколько раз каждое число встречается в nums; при этом counter[num] хранит число вхождений значения num . Делается это просто: достаточно пройти по nums (пусть текущее число равно num ) и на каждом шаге увеличить counter[num] на \\(1\\) .
        3. Поскольку индексы массива counter изначально упорядочены, можно считать, что все числа уже отсортированы. Далее остается пройти по counter и в соответствии с числом вхождений записать значения обратно в nums в порядке возрастания.

        Рисунок 11-16   Процесс сортировки подсчетом

        Код приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby counting_sort.py
        def counting_sort_naive(nums: list[int]):\n    \"\"\"Сортировка подсчетом\"\"\"\n    # Простая реализация, не подходит для сортировки объектов\n    # 1. Найти максимальный элемент массива m\n    m = max(nums)\n    # 2. Подсчитать число появлений каждой цифры\n    # counter[num] обозначает число появлений num\n    counter = [0] * (m + 1)\n    for num in nums:\n        counter[num] += 1\n    # 3. Обойти counter и заполнить исходный массив nums элементами\n    i = 0\n    for num in range(m + 1):\n        for _ in range(counter[num]):\n            nums[i] = num\n            i += 1\n
        counting_sort.cpp
        /* Сортировка подсчетом */\n// Простая реализация, не подходит для сортировки объектов\nvoid countingSortNaive(vector<int> &nums) {\n    // 1. Найти максимальный элемент массива m\n    int m = 0;\n    for (int num : nums) {\n        m = max(m, num);\n    }\n    // 2. Подсчитать число появлений каждой цифры\n    // counter[num] обозначает число появлений num\n    vector<int> counter(m + 1, 0);\n    for (int num : nums) {\n        counter[num]++;\n    }\n    // 3. Обойти counter и заполнить исходный массив nums элементами\n    int i = 0;\n    for (int num = 0; num < m + 1; num++) {\n        for (int j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n}\n
        counting_sort.java
        /* Сортировка подсчетом */\n// Простая реализация, не подходит для сортировки объектов\nvoid countingSortNaive(int[] nums) {\n    // 1. Найти максимальный элемент массива m\n    int m = 0;\n    for (int num : nums) {\n        m = Math.max(m, num);\n    }\n    // 2. Подсчитать число появлений каждой цифры\n    // counter[num] обозначает число появлений num\n    int[] counter = new int[m + 1];\n    for (int num : nums) {\n        counter[num]++;\n    }\n    // 3. Обойти counter и заполнить исходный массив nums элементами\n    int i = 0;\n    for (int num = 0; num < m + 1; num++) {\n        for (int j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n}\n
        counting_sort.cs
        /* Сортировка подсчетом */\n// Простая реализация, не подходит для сортировки объектов\nvoid CountingSortNaive(int[] nums) {\n    // 1. Найти максимальный элемент массива m\n    int m = 0;\n    foreach (int num in nums) {\n        m = Math.Max(m, num);\n    }\n    // 2. Подсчитать число появлений каждой цифры\n    // counter[num] обозначает число появлений num\n    int[] counter = new int[m + 1];\n    foreach (int num in nums) {\n        counter[num]++;\n    }\n    // 3. Обойти counter и заполнить исходный массив nums элементами\n    int i = 0;\n    for (int num = 0; num < m + 1; num++) {\n        for (int j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n}\n
        counting_sort.go
        /* Сортировка подсчетом */\n// Простая реализация, не подходит для сортировки объектов\nfunc countingSortNaive(nums []int) {\n    // 1. Найти максимальный элемент массива m\n    m := 0\n    for _, num := range nums {\n        if num > m {\n            m = num\n        }\n    }\n    // 2. Подсчитать число появлений каждой цифры\n    // counter[num] обозначает число появлений num\n    counter := make([]int, m+1)\n    for _, num := range nums {\n        counter[num]++\n    }\n    // 3. Обойти counter и заполнить исходный массив nums элементами\n    for i, num := 0, 0; num < m+1; num++ {\n        for j := 0; j < counter[num]; j++ {\n            nums[i] = num\n            i++\n        }\n    }\n}\n
        counting_sort.swift
        /* Сортировка подсчетом */\n// Простая реализация, не подходит для сортировки объектов\nfunc countingSortNaive(nums: inout [Int]) {\n    // 1. Найти максимальный элемент массива m\n    let m = nums.max()!\n    // 2. Подсчитать число появлений каждой цифры\n    // counter[num] обозначает число появлений num\n    var counter = Array(repeating: 0, count: m + 1)\n    for num in nums {\n        counter[num] += 1\n    }\n    // 3. Обойти counter и заполнить исходный массив nums элементами\n    var i = 0\n    for num in 0 ..< m + 1 {\n        for _ in 0 ..< counter[num] {\n            nums[i] = num\n            i += 1\n        }\n    }\n}\n
        counting_sort.js
        /* Сортировка подсчетом */\n// Простая реализация, не подходит для сортировки объектов\nfunction countingSortNaive(nums) {\n    // 1. Найти максимальный элемент массива m\n    let m = Math.max(...nums);\n    // 2. Подсчитать число появлений каждой цифры\n    // counter[num] обозначает число появлений num\n    const counter = new Array(m + 1).fill(0);\n    for (const num of nums) {\n        counter[num]++;\n    }\n    // 3. Обойти counter и заполнить исходный массив nums элементами\n    let i = 0;\n    for (let num = 0; num < m + 1; num++) {\n        for (let j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n}\n
        counting_sort.ts
        /* Сортировка подсчетом */\n// Простая реализация, не подходит для сортировки объектов\nfunction countingSortNaive(nums: number[]): void {\n    // 1. Найти максимальный элемент массива m\n    let m: number = Math.max(...nums);\n    // 2. Подсчитать число появлений каждой цифры\n    // counter[num] обозначает число появлений num\n    const counter: number[] = new Array<number>(m + 1).fill(0);\n    for (const num of nums) {\n        counter[num]++;\n    }\n    // 3. Обойти counter и заполнить исходный массив nums элементами\n    let i = 0;\n    for (let num = 0; num < m + 1; num++) {\n        for (let j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n}\n
        counting_sort.dart
        /* Сортировка подсчетом */\n// Простая реализация, не подходит для сортировки объектов\nvoid countingSortNaive(List<int> nums) {\n  // 1. Найти максимальный элемент массива m\n  int m = 0;\n  for (int _num in nums) {\n    m = max(m, _num);\n  }\n  // 2. Подсчитать число появлений каждой цифры\n  // counter[_num] обозначает число появлений _num\n  List<int> counter = List.filled(m + 1, 0);\n  for (int _num in nums) {\n    counter[_num]++;\n  }\n  // 3. Обойти counter и заполнить исходный массив nums элементами\n  int i = 0;\n  for (int _num = 0; _num < m + 1; _num++) {\n    for (int j = 0; j < counter[_num]; j++, i++) {\n      nums[i] = _num;\n    }\n  }\n}\n
        counting_sort.rs
        /* Сортировка подсчетом */\n// Простая реализация, не подходит для сортировки объектов\nfn counting_sort_naive(nums: &mut [i32]) {\n    // 1. Найти максимальный элемент массива m\n    let m = *nums.iter().max().unwrap();\n    // 2. Подсчитать число появлений каждой цифры\n    // counter[num] обозначает число появлений num\n    let mut counter = vec![0; m as usize + 1];\n    for &num in nums.iter() {\n        counter[num as usize] += 1;\n    }\n    // 3. Обойти counter и заполнить исходный массив nums элементами\n    let mut i = 0;\n    for num in 0..m + 1 {\n        for _ in 0..counter[num as usize] {\n            nums[i] = num;\n            i += 1;\n        }\n    }\n}\n
        counting_sort.c
        /* Сортировка подсчетом */\n// Простая реализация, не подходит для сортировки объектов\nvoid countingSortNaive(int nums[], int size) {\n    // 1. Найти максимальный элемент массива m\n    int m = 0;\n    for (int i = 0; i < size; i++) {\n        if (nums[i] > m) {\n            m = nums[i];\n        }\n    }\n    // 2. Подсчитать число появлений каждой цифры\n    // counter[num] обозначает число появлений num\n    int *counter = calloc(m + 1, sizeof(int));\n    for (int i = 0; i < size; i++) {\n        counter[nums[i]]++;\n    }\n    // 3. Обойти counter и заполнить исходный массив nums элементами\n    int i = 0;\n    for (int num = 0; num < m + 1; num++) {\n        for (int j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n    // 4. Освободить память\n    free(counter);\n}\n
        counting_sort.kt
        /* Сортировка подсчетом */\n// Простая реализация, не подходит для сортировки объектов\nfun countingSortNaive(nums: IntArray) {\n    // 1. Найти максимальный элемент массива m\n    var m = 0\n    for (num in nums) {\n        m = max(m, num)\n    }\n    // 2. Подсчитать число появлений каждой цифры\n    // counter[num] обозначает число появлений num\n    val counter = IntArray(m + 1)\n    for (num in nums) {\n        counter[num]++\n    }\n    // 3. Обойти counter и заполнить исходный массив nums элементами\n    var i = 0\n    for (num in 0..<m + 1) {\n        var j = 0\n        while (j < counter[num]) {\n            nums[i] = num\n            j++\n            i++\n        }\n    }\n}\n
        counting_sort.rb
        ### Сортировка подсчетом ###\ndef counting_sort_naive(nums)\n  # Простая реализация, не подходит для сортировки объектов\n  # 1. Найти максимальный элемент массива m\n  m = 0\n  nums.each { |num| m = [m, num].max }\n  # 2. Подсчитать число появлений каждой цифры\n  # counter[num] обозначает число появлений num\n  counter = Array.new(m + 1, 0)\n  nums.each { |num| counter[num] += 1 }\n  # 3. Обойти counter и заполнить исходный массив nums элементами\n  i = 0\n  for num in 0...(m + 1)\n    (0...counter[num]).each do\n      nums[i] = num\n      i += 1\n    end\n  end\nend\n
        Визуализация кода

        Во весь экран >

        Связь между сортировкой подсчетом и блочной сортировкой

        Если посмотреть на сортировку подсчетом с точки зрения блочной сортировки, то каждый индекс массива counter можно рассматривать как отдельный блок, а процесс подсчета - как распределение элементов по соответствующим блокам. Иными словами, сортировка подсчетом является частным случаем блочной сортировки для целочисленных данных.

        ","path":["Глава 11. Сортировка","11.9   Сортировка подсчетом"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1192","level":2,"title":"11.9.2   Полная реализация","text":"

        Внимательный читатель мог заметить, что если входные данные представлены объектами, то описанный выше шаг 3. перестает работать. Например, если входными данными являются объекты товаров и мы хотим отсортировать их по цене (полю класса), то описанный алгоритм сможет выдать только отсортированный ряд цен, но не исходные объекты в нужном порядке.

        Как же получить корректный порядок исходных данных? Сначала вычислим \"префиксную сумму\" массива counter . Как следует из названия, префиксная сумма в индексе i , обозначаемая как prefix[i] , равна сумме первых i элементов массива:

        \\[ \\text{prefix}[i] = \\sum_{j=0}^i \\text{counter[j]} \\]

        Префиксная сумма имеет четкий смысл: prefix[num] - 1 обозначает индекс последнего вхождения элемента num в результирующем массиве res. Это очень важная информация, потому что она указывает, в какую позицию результирующего массива должен попасть каждый элемент. Далее мы проходим исходный массив nums в обратном порядке и на каждой итерации для очередного элемента num выполняем два действия.

        1. Записать num в массив res по индексу prefix[num] - 1 .
        2. Уменьшить префиксную сумму prefix[num] на \\(1\\) , чтобы получить индекс следующего размещения элемента num .

        После завершения прохода массив res будет содержать отсортированный результат; остается только переписать res обратно в nums . Полный процесс сортировки подсчетом показан на рисунке 11-17.

        <1><2><3><4><5><6><7><8>

        Рисунок 11-17   Шаги сортировки подсчетом

        Код реализации сортировки подсчетом приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby counting_sort.py
        def counting_sort(nums: list[int]):\n    \"\"\"Сортировка подсчетом\"\"\"\n    # Полная реализация, позволяет сортировать объекты и является стабильной сортировкой\n    # 1. Найти максимальный элемент массива m\n    m = max(nums)\n    # 2. Подсчитать число появлений каждой цифры\n    # counter[num] обозначает число появлений num\n    counter = [0] * (m + 1)\n    for num in nums:\n        counter[num] += 1\n    # 3. Вычислить префиксные суммы counter и преобразовать «число появлений» в «конечный индекс»\n    # То есть counter[num]-1 — это индекс последнего появления num в res\n    for i in range(m):\n        counter[i + 1] += counter[i]\n    # 4. Обойти nums в обратном порядке и поместить элементы в результирующий массив res\n    # Инициализировать массив res для хранения результата\n    n = len(nums)\n    res = [0] * n\n    for i in range(n - 1, -1, -1):\n        num = nums[i]\n        res[counter[num] - 1] = num  # Поместить num по соответствующему индексу\n        counter[num] -= 1  # Уменьшить префиксную сумму на 1, чтобы получить индекс следующего размещения num\n    # Перезаписать исходный массив nums массивом результата res\n    for i in range(n):\n        nums[i] = res[i]\n
        counting_sort.cpp
        /* Сортировка подсчетом */\n// Полная реализация, позволяет сортировать объекты и является стабильной сортировкой\nvoid countingSort(vector<int> &nums) {\n    // 1. Найти максимальный элемент массива m\n    int m = 0;\n    for (int num : nums) {\n        m = max(m, num);\n    }\n    // 2. Подсчитать число появлений каждой цифры\n    // counter[num] обозначает число появлений num\n    vector<int> counter(m + 1, 0);\n    for (int num : nums) {\n        counter[num]++;\n    }\n    // 3. Вычислить префиксные суммы counter и преобразовать «число появлений» в «конечный индекс»\n    // То есть counter[num]-1 — это индекс последнего появления num в res\n    for (int i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Обойти nums в обратном порядке и поместить элементы в результирующий массив res\n    // Инициализировать массив res для хранения результата\n    int n = nums.size();\n    vector<int> res(n);\n    for (int i = n - 1; i >= 0; i--) {\n        int num = nums[i];\n        res[counter[num] - 1] = num; // Поместить num по соответствующему индексу\n        counter[num]--;              // Уменьшить префиксную сумму на 1, чтобы получить индекс следующего размещения num\n    }\n    // Перезаписать исходный массив nums массивом результата res\n    nums = res;\n}\n
        counting_sort.java
        /* Сортировка подсчетом */\n// Полная реализация, позволяет сортировать объекты и является стабильной сортировкой\nvoid countingSort(int[] nums) {\n    // 1. Найти максимальный элемент массива m\n    int m = 0;\n    for (int num : nums) {\n        m = Math.max(m, num);\n    }\n    // 2. Подсчитать число появлений каждой цифры\n    // counter[num] обозначает число появлений num\n    int[] counter = new int[m + 1];\n    for (int num : nums) {\n        counter[num]++;\n    }\n    // 3. Вычислить префиксные суммы counter и преобразовать «число появлений» в «конечный индекс»\n    // То есть counter[num]-1 — это индекс последнего появления num в res\n    for (int i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Обойти nums в обратном порядке и поместить элементы в результирующий массив res\n    // Инициализировать массив res для хранения результата\n    int n = nums.length;\n    int[] res = new int[n];\n    for (int i = n - 1; i >= 0; i--) {\n        int num = nums[i];\n        res[counter[num] - 1] = num; // Поместить num по соответствующему индексу\n        counter[num]--; // Уменьшить префиксную сумму на 1, чтобы получить индекс следующего размещения num\n    }\n    // Перезаписать исходный массив nums массивом результата res\n    for (int i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n
        counting_sort.cs
        /* Сортировка подсчетом */\n// Полная реализация, позволяет сортировать объекты и является стабильной сортировкой\nvoid CountingSort(int[] nums) {\n    // 1. Найти максимальный элемент массива m\n    int m = 0;\n    foreach (int num in nums) {\n        m = Math.Max(m, num);\n    }\n    // 2. Подсчитать число появлений каждой цифры\n    // counter[num] обозначает число появлений num\n    int[] counter = new int[m + 1];\n    foreach (int num in nums) {\n        counter[num]++;\n    }\n    // 3. Вычислить префиксные суммы counter и преобразовать «число появлений» в «конечный индекс»\n    // То есть counter[num]-1 — это индекс последнего появления num в res\n    for (int i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Обойти nums в обратном порядке и поместить элементы в результирующий массив res\n    // Инициализировать массив res для хранения результата\n    int n = nums.Length;\n    int[] res = new int[n];\n    for (int i = n - 1; i >= 0; i--) {\n        int num = nums[i];\n        res[counter[num] - 1] = num; // Поместить num по соответствующему индексу\n        counter[num]--; // Уменьшить префиксную сумму на 1, чтобы получить индекс следующего размещения num\n    }\n    // Перезаписать исходный массив nums массивом результата res\n    for (int i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n
        counting_sort.go
        /* Сортировка подсчетом */\n// Полная реализация, позволяет сортировать объекты и является стабильной сортировкой\nfunc countingSort(nums []int) {\n    // 1. Найти максимальный элемент массива m\n    m := 0\n    for _, num := range nums {\n        if num > m {\n            m = num\n        }\n    }\n    // 2. Подсчитать число появлений каждой цифры\n    // counter[num] обозначает число появлений num\n    counter := make([]int, m+1)\n    for _, num := range nums {\n        counter[num]++\n    }\n    // 3. Вычислить префиксные суммы counter и преобразовать «число появлений» в «конечный индекс»\n    // То есть counter[num]-1 — это индекс последнего появления num в res\n    for i := 0; i < m; i++ {\n        counter[i+1] += counter[i]\n    }\n    // 4. Обойти nums в обратном порядке и поместить элементы в результирующий массив res\n    // Инициализировать массив res для хранения результата\n    n := len(nums)\n    res := make([]int, n)\n    for i := n - 1; i >= 0; i-- {\n        num := nums[i]\n        // Поместить num по соответствующему индексу\n        res[counter[num]-1] = num\n        // Уменьшить префиксную сумму на 1, чтобы получить индекс следующего размещения num\n        counter[num]--\n    }\n    // Перезаписать исходный массив nums массивом результата res\n    copy(nums, res)\n}\n
        counting_sort.swift
        /* Сортировка подсчетом */\n// Полная реализация, позволяет сортировать объекты и является стабильной сортировкой\nfunc countingSort(nums: inout [Int]) {\n    // 1. Найти максимальный элемент массива m\n    let m = nums.max()!\n    // 2. Подсчитать число появлений каждой цифры\n    // counter[num] обозначает число появлений num\n    var counter = Array(repeating: 0, count: m + 1)\n    for num in nums {\n        counter[num] += 1\n    }\n    // 3. Вычислить префиксные суммы counter и преобразовать «число появлений» в «конечный индекс»\n    // То есть counter[num]-1 — это индекс последнего появления num в res\n    for i in 0 ..< m {\n        counter[i + 1] += counter[i]\n    }\n    // 4. Обойти nums в обратном порядке и поместить элементы в результирующий массив res\n    // Инициализировать массив res для хранения результата\n    var res = Array(repeating: 0, count: nums.count)\n    for i in nums.indices.reversed() {\n        let num = nums[i]\n        res[counter[num] - 1] = num // Поместить num по соответствующему индексу\n        counter[num] -= 1 // Уменьшить префиксную сумму на 1, чтобы получить индекс следующего размещения num\n    }\n    // Перезаписать исходный массив nums массивом результата res\n    for i in nums.indices {\n        nums[i] = res[i]\n    }\n}\n
        counting_sort.js
        /* Сортировка подсчетом */\n// Полная реализация, позволяет сортировать объекты и является стабильной сортировкой\nfunction countingSort(nums) {\n    // 1. Найти максимальный элемент массива m\n    let m = Math.max(...nums);\n    // 2. Подсчитать число появлений каждой цифры\n    // counter[num] обозначает число появлений num\n    const counter = new Array(m + 1).fill(0);\n    for (const num of nums) {\n        counter[num]++;\n    }\n    // 3. Вычислить префиксные суммы counter и преобразовать «число появлений» в «конечный индекс»\n    // То есть counter[num]-1 — это индекс последнего появления num в res\n    for (let i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Обойти nums в обратном порядке и поместить элементы в результирующий массив res\n    // Инициализировать массив res для хранения результата\n    const n = nums.length;\n    const res = new Array(n);\n    for (let i = n - 1; i >= 0; i--) {\n        const num = nums[i];\n        res[counter[num] - 1] = num; // Поместить num по соответствующему индексу\n        counter[num]--; // Уменьшить префиксную сумму на 1, чтобы получить индекс следующего размещения num\n    }\n    // Перезаписать исходный массив nums массивом результата res\n    for (let i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n
        counting_sort.ts
        /* Сортировка подсчетом */\n// Полная реализация, позволяет сортировать объекты и является стабильной сортировкой\nfunction countingSort(nums: number[]): void {\n    // 1. Найти максимальный элемент массива m\n    let m: number = Math.max(...nums);\n    // 2. Подсчитать число появлений каждой цифры\n    // counter[num] обозначает число появлений num\n    const counter: number[] = new Array<number>(m + 1).fill(0);\n    for (const num of nums) {\n        counter[num]++;\n    }\n    // 3. Вычислить префиксные суммы counter и преобразовать «число появлений» в «конечный индекс»\n    // То есть counter[num]-1 — это индекс последнего появления num в res\n    for (let i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Обойти nums в обратном порядке и поместить элементы в результирующий массив res\n    // Инициализировать массив res для хранения результата\n    const n = nums.length;\n    const res: number[] = new Array<number>(n);\n    for (let i = n - 1; i >= 0; i--) {\n        const num = nums[i];\n        res[counter[num] - 1] = num; // Поместить num по соответствующему индексу\n        counter[num]--; // Уменьшить префиксную сумму на 1, чтобы получить индекс следующего размещения num\n    }\n    // Перезаписать исходный массив nums массивом результата res\n    for (let i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n
        counting_sort.dart
        /* Сортировка подсчетом */\n// Полная реализация, позволяет сортировать объекты и является стабильной сортировкой\nvoid countingSort(List<int> nums) {\n  // 1. Найти максимальный элемент массива m\n  int m = 0;\n  for (int _num in nums) {\n    m = max(m, _num);\n  }\n  // 2. Подсчитать число появлений каждой цифры\n  // counter[_num] обозначает число появлений _num\n  List<int> counter = List.filled(m + 1, 0);\n  for (int _num in nums) {\n    counter[_num]++;\n  }\n  // 3. Вычислить префиксные суммы counter и преобразовать «число появлений» в «конечный индекс»\n  // То есть counter[_num]-1 — это индекс последнего появления _num в res\n  for (int i = 0; i < m; i++) {\n    counter[i + 1] += counter[i];\n  }\n  // 4. Обойти nums в обратном порядке и поместить элементы в результирующий массив res\n  // Инициализировать массив res для хранения результата\n  int n = nums.length;\n  List<int> res = List.filled(n, 0);\n  for (int i = n - 1; i >= 0; i--) {\n    int _num = nums[i];\n    res[counter[_num] - 1] = _num; // Поместить _num по соответствующему индексу\n    counter[_num]--; // Уменьшить префиксную сумму на 1, чтобы получить индекс следующего размещения _num\n  }\n  // Перезаписать исходный массив nums массивом результата res\n  nums.setAll(0, res);\n}\n
        counting_sort.rs
        /* Сортировка подсчетом */\n// Полная реализация, позволяет сортировать объекты и является стабильной сортировкой\nfn counting_sort(nums: &mut [i32]) {\n    // 1. Найти максимальный элемент массива m\n    let m = *nums.iter().max().unwrap() as usize;\n    // 2. Подсчитать число появлений каждой цифры\n    // counter[num] обозначает число появлений num\n    let mut counter = vec![0; m + 1];\n    for &num in nums.iter() {\n        counter[num as usize] += 1;\n    }\n    // 3. Вычислить префиксные суммы counter и преобразовать «число появлений» в «конечный индекс»\n    // То есть counter[num]-1 — это индекс последнего появления num в res\n    for i in 0..m {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Обойти nums в обратном порядке и поместить элементы в результирующий массив res\n    // Инициализировать массив res для хранения результата\n    let n = nums.len();\n    let mut res = vec![0; n];\n    for i in (0..n).rev() {\n        let num = nums[i];\n        res[counter[num as usize] - 1] = num; // Поместить num по соответствующему индексу\n        counter[num as usize] -= 1; // Уменьшить префиксную сумму на 1, чтобы получить индекс следующего размещения num\n    }\n    // Перезаписать исходный массив nums массивом результата res\n    nums.copy_from_slice(&res)\n}\n
        counting_sort.c
        /* Сортировка подсчетом */\n// Полная реализация, позволяет сортировать объекты и является стабильной сортировкой\nvoid countingSort(int nums[], int size) {\n    // 1. Найти максимальный элемент массива m\n    int m = 0;\n    for (int i = 0; i < size; i++) {\n        if (nums[i] > m) {\n            m = nums[i];\n        }\n    }\n    // 2. Подсчитать число появлений каждой цифры\n    // counter[num] обозначает число появлений num\n    int *counter = calloc(m, sizeof(int));\n    for (int i = 0; i < size; i++) {\n        counter[nums[i]]++;\n    }\n    // 3. Вычислить префиксные суммы counter и преобразовать «число появлений» в «конечный индекс»\n    // То есть counter[num]-1 — это индекс последнего появления num в res\n    for (int i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Обойти nums в обратном порядке и поместить элементы в результирующий массив res\n    // Инициализировать массив res для хранения результата\n    int *res = malloc(sizeof(int) * size);\n    for (int i = size - 1; i >= 0; i--) {\n        int num = nums[i];\n        res[counter[num] - 1] = num; // Поместить num по соответствующему индексу\n        counter[num]--;              // Уменьшить префиксную сумму на 1, чтобы получить индекс следующего размещения num\n    }\n    // Перезаписать исходный массив nums массивом результата res\n    memcpy(nums, res, size * sizeof(int));\n    // 5. Освободить память\n    free(res);\n    free(counter);\n}\n
        counting_sort.kt
        /* Сортировка подсчетом */\n// Полная реализация, позволяет сортировать объекты и является стабильной сортировкой\nfun countingSort(nums: IntArray) {\n    // 1. Найти максимальный элемент массива m\n    var m = 0\n    for (num in nums) {\n        m = max(m, num)\n    }\n    // 2. Подсчитать число появлений каждой цифры\n    // counter[num] обозначает число появлений num\n    val counter = IntArray(m + 1)\n    for (num in nums) {\n        counter[num]++\n    }\n    // 3. Вычислить префиксные суммы counter и преобразовать «число появлений» в «конечный индекс»\n    // То есть counter[num]-1 — это индекс последнего появления num в res\n    for (i in 0..<m) {\n        counter[i + 1] += counter[i]\n    }\n    // 4. Обойти nums в обратном порядке и поместить элементы в результирующий массив res\n    // Инициализировать массив res для хранения результата\n    val n = nums.size\n    val res = IntArray(n)\n    for (i in n - 1 downTo 0) {\n        val num = nums[i]\n        res[counter[num] - 1] = num // Поместить num по соответствующему индексу\n        counter[num]-- // Уменьшить префиксную сумму на 1, чтобы получить индекс следующего размещения num\n    }\n    // Перезаписать исходный массив nums массивом результата res\n    for (i in 0..<n) {\n        nums[i] = res[i]\n    }\n}\n
        counting_sort.rb
        ### Сортировка подсчетом ###\ndef counting_sort(nums)\n  # Полная реализация, позволяет сортировать объекты и является стабильной сортировкой\n  # 1. Найти максимальный элемент массива m\n  m = nums.max\n  # 2. Подсчитать число появлений каждой цифры\n  # counter[num] обозначает число появлений num\n  counter = Array.new(m + 1, 0)\n  nums.each { |num| counter[num] += 1 }\n  # 3. Вычислить префиксные суммы counter и преобразовать «число появлений» в «конечный индекс»\n  # То есть counter[num]-1 — это индекс последнего появления num в res\n  (0...m).each { |i| counter[i + 1] += counter[i] }\n  # 4. Обойти nums в обратном порядке и поместить элементы в результирующий массив res\n  # Инициализировать массив res для хранения результата\n  n = nums.length\n  res = Array.new(n, 0)\n  (n - 1).downto(0).each do |i|\n    num = nums[i]\n    res[counter[num] - 1] = num # Поместить num по соответствующему индексу\n    counter[num] -= 1 # Уменьшить префиксную сумму на 1, чтобы получить индекс следующего размещения num\n  end\n  # Перезаписать исходный массив nums массивом результата res\n  (0...n).each { |i| nums[i] = res[i] }\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 11. Сортировка","11.9   Сортировка подсчетом"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1193","level":2,"title":"11.9.3   Характеристики алгоритма","text":"
        • Временная сложность равна \\(O(n + m)\\), алгоритм не является адаптивным : необходимо пройти по nums и по counter , а оба этих прохода занимают линейное время. Обычно выполняется \\(n \\gg m\\) , поэтому временная сложность стремится к \\(O(n)\\) .
        • Пространственная сложность равна \\(O(n + m)\\), сортировка не выполняется на месте: используются массивы res и counter длины \\(n\\) и \\(m\\) соответственно.
        • Стабильная сортировка: порядок заполнения res идет \"справа налево\", поэтому обратный проход по nums позволяет сохранить относительный порядок равных элементов и тем самым реализовать стабильную сортировку. Вообще говоря, прямой проход по nums тоже даст правильный результат сортировки, но он будет нестабильным.
        ","path":["Глава 11. Сортировка","11.9   Сортировка подсчетом"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1194","level":2,"title":"11.9.4   Ограничения","text":"

        На этом этапе сортировка подсчетом может показаться очень изящной: она позволяет эффективно сортировать данные, опираясь только на подсчет числа вхождений. Однако условия ее применения довольно строгие.

        Сортировка подсчетом применима только к неотрицательным целым числам. Чтобы использовать ее для других типов данных, нужно убедиться, что эти данные можно преобразовать в неотрицательные целые числа и что при преобразовании относительный порядок элементов не изменится. Например, для массива целых чисел с отрицательными значениями можно сначала прибавить ко всем числам константу, превратив их в положительные, затем выполнить сортировку и после этого преобразовать значения обратно.

        Сортировка подсчетом подходит для случаев, когда объем данных велик, но диапазон значений невелик. Например, в приведенном выше примере \\(m\\) не должно быть слишком большим, иначе будет занято слишком много памяти. А когда \\(n \\ll m\\) , сортировка подсчетом использует \\(O(m)\\) времени и может оказаться медленнее, чем алгоритмы сортировки с \\(O(n \\log n)\\) .

        ","path":["Глава 11. Сортировка","11.9   Сортировка подсчетом"],"tags":[]},{"location":"chapter_sorting/heap_sort/","level":1,"title":"11.7   Пирамидальная сортировка","text":"

        Tip

        Перед чтением этого раздела убедитесь, что вы уже изучили главу \"Куча\".

        Пирамидальная сортировка (heap sort) - это эффективный алгоритм сортировки, основанный на структуре данных \"куча\". Для его реализации можно использовать уже изученные нами \"построение кучи\" и \"извлечение элементов из кучи\".

        1. Подать на вход массив и построить из него мин-кучу; в этот момент минимальный элемент будет находиться в вершине кучи.
        2. Непрерывно выполнять извлечение из кучи и по порядку записывать извлеченные элементы - так получится последовательность, отсортированная по возрастанию.

        Хотя этот метод и работоспособен, он требует дополнительного массива для хранения извлеченных элементов и потому расходует лишнюю память. На практике обычно используют более изящную реализацию.

        ","path":["Глава 11. Сортировка","11.7   Пирамидальная сортировка"],"tags":[]},{"location":"chapter_sorting/heap_sort/#1171","level":2,"title":"11.7.1   Алгоритм","text":"

        Пусть длина массива равна \\(n\\) ; тогда процесс пирамидальной сортировки показан на рисунке 11-12.

        1. Подать на вход массив и построить из него макс-кучу. После этого максимальный элемент окажется в вершине кучи.
        2. Обменять элемент в вершине кучи (первый элемент) с элементом внизу кучи (последний элемент). После обмена длина кучи уменьшается на \\(1\\) , а число уже отсортированных элементов увеличивается на \\(1\\) .
        3. Начиная с вершины, выполнить операцию просеивания сверху вниз. После этого свойство кучи будет восстановлено.
        4. Циклически повторять шаг 2. и шаг 3. . После \\(n - 1\\) раундов массив будет полностью отсортирован.

        Tip

        На самом деле операция извлечения из кучи тоже включает шаг 2. и шаг 3. , только дополнительно содержит действие по удалению элемента.

        <1><2><3><4><5><6><7><8><9><10><11><12>

        Рисунок 11-12   Шаги пирамидальной сортировки

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

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby heap_sort.py
        def sift_down(nums: list[int], n: int, i: int):\n    \"\"\"Длина кучи равна n; начиная с узла i, выполнить просеивание сверху вниз\"\"\"\n    while True:\n        # Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n        l = 2 * i + 1\n        r = 2 * i + 2\n        ma = i\n        if l < n and nums[l] > nums[ma]:\n            ma = l\n        if r < n and nums[r] > nums[ma]:\n            ma = r\n        # Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if ma == i:\n            break\n        # Поменять два узла местами\n        nums[i], nums[ma] = nums[ma], nums[i]\n        # Циклическое просеивание вниз\n        i = ma\n\ndef heap_sort(nums: list[int]):\n    \"\"\"Сортировка кучей\"\"\"\n    # Построение кучи: выполнить heapify для всех узлов, кроме листовых\n    for i in range(len(nums) // 2 - 1, -1, -1):\n        sift_down(nums, len(nums), i)\n    # Извлекать максимальный элемент из кучи в течение n-1 итераций\n    for i in range(len(nums) - 1, 0, -1):\n        # Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n        nums[0], nums[i] = nums[i], nums[0]\n        # Начиная с корневого узла, выполнить просеивание сверху вниз\n        sift_down(nums, i, 0)\n
        heap_sort.cpp
        /* Длина кучи равна n; начиная с узла i, выполнить просеивание сверху вниз */\nvoid siftDown(vector<int> &nums, int n, int i) {\n    while (true) {\n        // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n        int l = 2 * i + 1;\n        int r = 2 * i + 2;\n        int ma = i;\n        if (l < n && nums[l] > nums[ma])\n            ma = l;\n        if (r < n && nums[r] > nums[ma])\n            ma = r;\n        // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if (ma == i) {\n            break;\n        }\n        // Поменять два узла местами\n        swap(nums[i], nums[ma]);\n        // Циклическое просеивание вниз\n        i = ma;\n    }\n}\n\n/* Сортировка кучей */\nvoid heapSort(vector<int> &nums) {\n    // Построение кучи: выполнить heapify для всех узлов, кроме листовых\n    for (int i = nums.size() / 2 - 1; i >= 0; --i) {\n        siftDown(nums, nums.size(), i);\n    }\n    // Извлекать максимальный элемент из кучи в течение n-1 итераций\n    for (int i = nums.size() - 1; i > 0; --i) {\n        // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n        swap(nums[0], nums[i]);\n        // Начиная с корневого узла, выполнить просеивание сверху вниз\n        siftDown(nums, i, 0);\n    }\n}\n
        heap_sort.java
        /* Длина кучи равна n; начиная с узла i, выполнить просеивание сверху вниз */\nvoid siftDown(int[] nums, int n, int i) {\n    while (true) {\n        // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n        int l = 2 * i + 1;\n        int r = 2 * i + 2;\n        int ma = i;\n        if (l < n && nums[l] > nums[ma])\n            ma = l;\n        if (r < n && nums[r] > nums[ma])\n            ma = r;\n        // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if (ma == i)\n            break;\n        // Поменять два узла местами\n        int temp = nums[i];\n        nums[i] = nums[ma];\n        nums[ma] = temp;\n        // Циклическое просеивание вниз\n        i = ma;\n    }\n}\n\n/* Сортировка кучей */\nvoid heapSort(int[] nums) {\n    // Построение кучи: выполнить heapify для всех узлов, кроме листовых\n    for (int i = nums.length / 2 - 1; i >= 0; i--) {\n        siftDown(nums, nums.length, i);\n    }\n    // Извлекать максимальный элемент из кучи в течение n-1 итераций\n    for (int i = nums.length - 1; i > 0; i--) {\n        // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n        int tmp = nums[0];\n        nums[0] = nums[i];\n        nums[i] = tmp;\n        // Начиная с корневого узла, выполнить просеивание сверху вниз\n        siftDown(nums, i, 0);\n    }\n}\n
        heap_sort.cs
        /* Длина кучи равна n; начиная с узла i, выполнить просеивание сверху вниз */\nvoid SiftDown(int[] nums, int n, int i) {\n    while (true) {\n        // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n        int l = 2 * i + 1;\n        int r = 2 * i + 2;\n        int ma = i;\n        if (l < n && nums[l] > nums[ma])\n            ma = l;\n        if (r < n && nums[r] > nums[ma])\n            ma = r;\n        // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if (ma == i)\n            break;\n        // Поменять два узла местами\n        (nums[ma], nums[i]) = (nums[i], nums[ma]);\n        // Циклическое просеивание вниз\n        i = ma;\n    }\n}\n\n/* Сортировка кучей */\nvoid HeapSort(int[] nums) {\n    // Построение кучи: выполнить heapify для всех узлов, кроме листовых\n    for (int i = nums.Length / 2 - 1; i >= 0; i--) {\n        SiftDown(nums, nums.Length, i);\n    }\n    // Извлекать максимальный элемент из кучи в течение n-1 итераций\n    for (int i = nums.Length - 1; i > 0; i--) {\n        // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n        (nums[i], nums[0]) = (nums[0], nums[i]);\n        // Начиная с корневого узла, выполнить просеивание сверху вниз\n        SiftDown(nums, i, 0);\n    }\n}\n
        heap_sort.go
        /* Длина кучи равна n; начиная с узла i, выполнить просеивание сверху вниз */\nfunc siftDown(nums *[]int, n, i int) {\n    for true {\n        // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n        l := 2*i + 1\n        r := 2*i + 2\n        ma := i\n        if l < n && (*nums)[l] > (*nums)[ma] {\n            ma = l\n        }\n        if r < n && (*nums)[r] > (*nums)[ma] {\n            ma = r\n        }\n        // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if ma == i {\n            break\n        }\n        // Поменять два узла местами\n        (*nums)[i], (*nums)[ma] = (*nums)[ma], (*nums)[i]\n        // Циклическое просеивание вниз\n        i = ma\n    }\n}\n\n/* Сортировка кучей */\nfunc heapSort(nums *[]int) {\n    // Построение кучи: выполнить heapify для всех узлов, кроме листовых\n    for i := len(*nums)/2 - 1; i >= 0; i-- {\n        siftDown(nums, len(*nums), i)\n    }\n    // Извлекать максимальный элемент из кучи в течение n-1 итераций\n    for i := len(*nums) - 1; i > 0; i-- {\n        // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n        (*nums)[0], (*nums)[i] = (*nums)[i], (*nums)[0]\n        // Начиная с корневого узла, выполнить просеивание сверху вниз\n        siftDown(nums, i, 0)\n    }\n}\n
        heap_sort.swift
        /* Длина кучи равна n; начиная с узла i, выполнить просеивание сверху вниз */\nfunc siftDown(nums: inout [Int], n: Int, i: Int) {\n    var i = i\n    while true {\n        // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n        let l = 2 * i + 1\n        let r = 2 * i + 2\n        var ma = i\n        if l < n, nums[l] > nums[ma] {\n            ma = l\n        }\n        if r < n, nums[r] > nums[ma] {\n            ma = r\n        }\n        // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if ma == i {\n            break\n        }\n        // Поменять два узла местами\n        nums.swapAt(i, ma)\n        // Циклическое просеивание вниз\n        i = ma\n    }\n}\n\n/* Сортировка кучей */\nfunc heapSort(nums: inout [Int]) {\n    // Построение кучи: выполнить heapify для всех узлов, кроме листовых\n    for i in stride(from: nums.count / 2 - 1, through: 0, by: -1) {\n        siftDown(nums: &nums, n: nums.count, i: i)\n    }\n    // Извлекать максимальный элемент из кучи в течение n-1 итераций\n    for i in nums.indices.dropFirst().reversed() {\n        // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n        nums.swapAt(0, i)\n        // Начиная с корневого узла, выполнить просеивание сверху вниз\n        siftDown(nums: &nums, n: i, i: 0)\n    }\n}\n
        heap_sort.js
        /* Длина кучи равна n; начиная с узла i, выполнить просеивание сверху вниз */\nfunction siftDown(nums, n, i) {\n    while (true) {\n        // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n        let l = 2 * i + 1;\n        let r = 2 * i + 2;\n        let ma = i;\n        if (l < n && nums[l] > nums[ma]) {\n            ma = l;\n        }\n        if (r < n && nums[r] > nums[ma]) {\n            ma = r;\n        }\n        // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if (ma === i) {\n            break;\n        }\n        // Поменять два узла местами\n        [nums[i], nums[ma]] = [nums[ma], nums[i]];\n        // Циклическое просеивание вниз\n        i = ma;\n    }\n}\n\n/* Сортировка кучей */\nfunction heapSort(nums) {\n    // Построение кучи: выполнить heapify для всех узлов, кроме листовых\n    for (let i = Math.floor(nums.length / 2) - 1; i >= 0; i--) {\n        siftDown(nums, nums.length, i);\n    }\n    // Извлекать максимальный элемент из кучи в течение n-1 итераций\n    for (let i = nums.length - 1; i > 0; i--) {\n        // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n        [nums[0], nums[i]] = [nums[i], nums[0]];\n        // Начиная с корневого узла, выполнить просеивание сверху вниз\n        siftDown(nums, i, 0);\n    }\n}\n
        heap_sort.ts
        /* Длина кучи равна n; начиная с узла i, выполнить просеивание сверху вниз */\nfunction siftDown(nums: number[], n: number, i: number): void {\n    while (true) {\n        // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n        let l = 2 * i + 1;\n        let r = 2 * i + 2;\n        let ma = i;\n        if (l < n && nums[l] > nums[ma]) {\n            ma = l;\n        }\n        if (r < n && nums[r] > nums[ma]) {\n            ma = r;\n        }\n        // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if (ma === i) {\n            break;\n        }\n        // Поменять два узла местами\n        [nums[i], nums[ma]] = [nums[ma], nums[i]];\n        // Циклическое просеивание вниз\n        i = ma;\n    }\n}\n\n/* Сортировка кучей */\nfunction heapSort(nums: number[]): void {\n    // Построение кучи: выполнить heapify для всех узлов, кроме листовых\n    for (let i = Math.floor(nums.length / 2) - 1; i >= 0; i--) {\n        siftDown(nums, nums.length, i);\n    }\n    // Извлекать максимальный элемент из кучи в течение n-1 итераций\n    for (let i = nums.length - 1; i > 0; i--) {\n        // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n        [nums[0], nums[i]] = [nums[i], nums[0]];\n        // Начиная с корневого узла, выполнить просеивание сверху вниз\n        siftDown(nums, i, 0);\n    }\n}\n
        heap_sort.dart
        /* Длина кучи равна n; начиная с узла i, выполнить просеивание сверху вниз */\nvoid siftDown(List<int> nums, int n, int i) {\n  while (true) {\n    // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n    int l = 2 * i + 1;\n    int r = 2 * i + 2;\n    int ma = i;\n    if (l < n && nums[l] > nums[ma]) ma = l;\n    if (r < n && nums[r] > nums[ma]) ma = r;\n    // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n    if (ma == i) break;\n    // Поменять два узла местами\n    int temp = nums[i];\n    nums[i] = nums[ma];\n    nums[ma] = temp;\n    // Циклическое просеивание вниз\n    i = ma;\n  }\n}\n\n/* Сортировка кучей */\nvoid heapSort(List<int> nums) {\n  // Построение кучи: выполнить heapify для всех узлов, кроме листовых\n  for (int i = nums.length ~/ 2 - 1; i >= 0; i--) {\n    siftDown(nums, nums.length, i);\n  }\n  // Извлекать максимальный элемент из кучи в течение n-1 итераций\n  for (int i = nums.length - 1; i > 0; i--) {\n    // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n    int tmp = nums[0];\n    nums[0] = nums[i];\n    nums[i] = tmp;\n    // Начиная с корневого узла, выполнить просеивание сверху вниз\n    siftDown(nums, i, 0);\n  }\n}\n
        heap_sort.rs
        /* Длина кучи равна n; начиная с узла i, выполнить просеивание сверху вниз */\nfn sift_down(nums: &mut [i32], n: usize, mut i: usize) {\n    loop {\n        // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n        let l = 2 * i + 1;\n        let r = 2 * i + 2;\n        let mut ma = i;\n        if l < n && nums[l] > nums[ma] {\n            ma = l;\n        }\n        if r < n && nums[r] > nums[ma] {\n            ma = r;\n        }\n        // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if ma == i {\n            break;\n        }\n        // Поменять два узла местами\n        nums.swap(i, ma);\n        // Циклическое просеивание вниз\n        i = ma;\n    }\n}\n\n/* Сортировка кучей */\nfn heap_sort(nums: &mut [i32]) {\n    // Построение кучи: выполнить heapify для всех узлов, кроме листовых\n    for i in (0..nums.len() / 2).rev() {\n        sift_down(nums, nums.len(), i);\n    }\n    // Извлекать максимальный элемент из кучи в течение n-1 итераций\n    for i in (1..nums.len()).rev() {\n        // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n        nums.swap(0, i);\n        // Начиная с корневого узла, выполнить просеивание сверху вниз\n        sift_down(nums, i, 0);\n    }\n}\n
        heap_sort.c
        /* Длина кучи равна n; начиная с узла i, выполнить просеивание сверху вниз */\nvoid siftDown(int nums[], int n, int i) {\n    while (1) {\n        // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n        int l = 2 * i + 1;\n        int r = 2 * i + 2;\n        int ma = i;\n        if (l < n && nums[l] > nums[ma])\n            ma = l;\n        if (r < n && nums[r] > nums[ma])\n            ma = r;\n        // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if (ma == i) {\n            break;\n        }\n        // Поменять два узла местами\n        int temp = nums[i];\n        nums[i] = nums[ma];\n        nums[ma] = temp;\n        // Циклическое просеивание вниз\n        i = ma;\n    }\n}\n\n/* Сортировка кучей */\nvoid heapSort(int nums[], int n) {\n    // Построение кучи: выполнить heapify для всех узлов, кроме листовых\n    for (int i = n / 2 - 1; i >= 0; --i) {\n        siftDown(nums, n, i);\n    }\n    // Извлекать максимальный элемент из кучи в течение n-1 итераций\n    for (int i = n - 1; i > 0; --i) {\n        // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n        int tmp = nums[0];\n        nums[0] = nums[i];\n        nums[i] = tmp;\n        // Начиная с корневого узла, выполнить просеивание сверху вниз\n        siftDown(nums, i, 0);\n    }\n}\n
        heap_sort.kt
        /* Длина кучи равна n; начиная с узла i, выполнить просеивание сверху вниз */\nfun siftDown(nums: IntArray, n: Int, li: Int) {\n    var i = li\n    while (true) {\n        // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n        val l = 2 * i + 1\n        val r = 2 * i + 2\n        var ma = i\n        if (l < n && nums[l] > nums[ma]) \n            ma = l\n        if (r < n && nums[r] > nums[ma]) \n            ma = r\n        // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if (ma == i) \n            break\n        // Поменять два узла местами\n        val temp = nums[i]\n        nums[i] = nums[ma]\n        nums[ma] = temp\n        // Циклическое просеивание вниз\n        i = ma\n    }\n}\n\n/* Сортировка кучей */\nfun heapSort(nums: IntArray) {\n    // Построение кучи: выполнить heapify для всех узлов, кроме листовых\n    for (i in nums.size / 2 - 1 downTo 0) {\n        siftDown(nums, nums.size, i)\n    }\n    // Извлекать максимальный элемент из кучи в течение n-1 итераций\n    for (i in nums.size - 1 downTo 1) {\n        // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n        val temp = nums[0]\n        nums[0] = nums[i]\n        nums[i] = temp\n        // Начиная с корневого узла, выполнить просеивание сверху вниз\n        siftDown(nums, i, 0)\n    }\n}\n
        heap_sort.rb
        ### Длина кучи равна n; начиная с узла i, выполнить просеивание сверху вниз ###\ndef sift_down(nums, n, i)\n  while true\n    # Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n    l = 2 * i + 1\n    r = 2 * i + 2\n    ma = i\n    ma = l if l < n && nums[l] > nums[ma]\n    ma = r if r < n && nums[r] > nums[ma]\n    # Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n    break if ma == i\n    # Поменять два узла местами\n    nums[i], nums[ma] = nums[ma], nums[i]\n    # Циклическое просеивание вниз\n    i = ma\n  end\nend\n\n### Сортировка кучей ###\ndef heap_sort(nums)\n  # Построение кучи: выполнить heapify для всех узлов, кроме листовых\n  (nums.length / 2 - 1).downto(0) do |i|\n    sift_down(nums, nums.length, i)\n  end\n  # Извлекать максимальный элемент из кучи в течение n-1 итераций\n  (nums.length - 1).downto(1) do |i|\n    # Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n    nums[0], nums[i] = nums[i], nums[0]\n    # Начиная с корневого узла, выполнить просеивание сверху вниз\n    sift_down(nums, i, 0)\n  end\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 11. Сортировка","11.7   Пирамидальная сортировка"],"tags":[]},{"location":"chapter_sorting/heap_sort/#1172","level":2,"title":"11.7.2   Характеристики алгоритма","text":"
        • Временная сложность равна \\(O(n \\log n)\\), алгоритм не является адаптивным: построение кучи занимает \\(O(n)\\) времени. Извлечение максимального элемента из кучи имеет временную сложность \\(O(\\log n)\\) и выполняется \\(n - 1\\) раз.
        • Пространственная сложность равна \\(O(1)\\), сортировка выполняется на месте: несколько переменных-указателей используют \\(O(1)\\) памяти. Обмен элементов и операции просеивания выполняются прямо в исходном массиве.
        • Нестабильная сортировка: при обмене вершины кучи и нижнего элемента относительный порядок равных элементов может измениться.
        ","path":["Глава 11. Сортировка","11.7   Пирамидальная сортировка"],"tags":[]},{"location":"chapter_sorting/insertion_sort/","level":1,"title":"11.4   Сортировка вставками","text":"

        Сортировка вставками (insertion sort) - это простой алгоритм сортировки, принцип которого очень похож на ручную сортировку карт в колоде.

        Точнее говоря, в неотсортированном диапазоне выбирается опорный элемент, после чего он сравнивается с элементами слева в уже отсортированном диапазоне и вставляется в правильную позицию.

        На рисунке 11-6 показан процесс вставки элемента в массив. Пусть опорный элемент обозначен как base ; нам нужно сдвинуть все элементы от целевого индекса до base на одну позицию вправо, а затем записать base в целевой индекс.

        Рисунок 11-6   Одна операция вставки

        ","path":["Глава 11. Сортировка","11.4   Сортировка вставками"],"tags":[]},{"location":"chapter_sorting/insertion_sort/#1141","level":2,"title":"11.4.1   Алгоритм","text":"

        Общий процесс сортировки вставками показан на рисунке 11-7.

        1. В начальном состоянии отсортирован только первый элемент массива.
        2. Выбрать второй элемент массива как base ; после вставки в правильную позицию первые два элемента массива окажутся отсортированными.
        3. Выбрать третий элемент как base ; после вставки в правильную позицию первые три элемента массива окажутся отсортированными.
        4. Продолжать по аналогии; в последнем раунде в качестве base берется последний элемент, и после его вставки все элементы массива будут отсортированы.

        Рисунок 11-7   Процесс сортировки вставками

        Пример кода:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby insertion_sort.py
        def insertion_sort(nums: list[int]):\n    \"\"\"Сортировка вставками\"\"\"\n    # Внешний цикл: отсортированный диапазон [0, i-1]\n    for i in range(1, len(nums)):\n        base = nums[i]\n        j = i - 1\n        # Внутренний цикл: вставить base в правильную позицию отсортированного диапазона [0, i-1]\n        while j >= 0 and nums[j] > base:\n            nums[j + 1] = nums[j]  # Сдвинуть nums[j] на одну позицию вправо\n            j -= 1\n        nums[j + 1] = base  # Поместить base в правильную позицию\n
        insertion_sort.cpp
        /* Сортировка вставками */\nvoid insertionSort(vector<int> &nums) {\n    // Внешний цикл: отсортированный диапазон [0, i-1]\n    for (int i = 1; i < nums.size(); i++) {\n        int base = nums[i], j = i - 1;\n        // Внутренний цикл: вставить base в правильную позицию отсортированного диапазона [0, i-1]\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j]; // Сдвинуть nums[j] на одну позицию вправо\n            j--;\n        }\n        nums[j + 1] = base; // Поместить base в правильную позицию\n    }\n}\n
        insertion_sort.java
        /* Сортировка вставками */\nvoid insertionSort(int[] nums) {\n    // Внешний цикл: отсортированный диапазон [0, i-1]\n    for (int i = 1; i < nums.length; i++) {\n        int base = nums[i], j = i - 1;\n        // Внутренний цикл: вставить base в правильную позицию отсортированного диапазона [0, i-1]\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j]; // Сдвинуть nums[j] на одну позицию вправо\n            j--;\n        }\n        nums[j + 1] = base;        // Поместить base в правильную позицию\n    }\n}\n
        insertion_sort.cs
        /* Сортировка вставками */\nvoid InsertionSort(int[] nums) {\n    // Внешний цикл: отсортированный диапазон [0, i-1]\n    for (int i = 1; i < nums.Length; i++) {\n        int bas = nums[i], j = i - 1;\n        // Внутренний цикл: вставить base в правильную позицию отсортированного диапазона [0, i-1]\n        while (j >= 0 && nums[j] > bas) {\n            nums[j + 1] = nums[j]; // Сдвинуть nums[j] на одну позицию вправо\n            j--;\n        }\n        nums[j + 1] = bas;         // Поместить base в правильную позицию\n    }\n}\n
        insertion_sort.go
        /* Сортировка вставками */\nfunc insertionSort(nums []int) {\n    // Внешний цикл: отсортированный диапазон [0, i-1]\n    for i := 1; i < len(nums); i++ {\n        base := nums[i]\n        j := i - 1\n        // Внутренний цикл: вставить base в правильную позицию отсортированного диапазона [0, i-1]\n        for j >= 0 && nums[j] > base {\n            nums[j+1] = nums[j] // Сдвинуть nums[j] на одну позицию вправо\n            j--\n        }\n        nums[j+1] = base // Поместить base в правильную позицию\n    }\n}\n
        insertion_sort.swift
        /* Сортировка вставками */\nfunc insertionSort(nums: inout [Int]) {\n    // Внешний цикл: отсортированный диапазон [0, i-1]\n    for i in nums.indices.dropFirst() {\n        let base = nums[i]\n        var j = i - 1\n        // Внутренний цикл: вставить base в правильную позицию отсортированного диапазона [0, i-1]\n        while j >= 0, nums[j] > base {\n            nums[j + 1] = nums[j] // Сдвинуть nums[j] на одну позицию вправо\n            j -= 1\n        }\n        nums[j + 1] = base // Поместить base в правильную позицию\n    }\n}\n
        insertion_sort.js
        /* Сортировка вставками */\nfunction insertionSort(nums) {\n    // Внешний цикл: отсортированный диапазон [0, i-1]\n    for (let i = 1; i < nums.length; i++) {\n        let base = nums[i],\n            j = i - 1;\n        // Внутренний цикл: вставить base в правильную позицию отсортированного диапазона [0, i-1]\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j]; // Сдвинуть nums[j] на одну позицию вправо\n            j--;\n        }\n        nums[j + 1] = base; // Поместить base в правильную позицию\n    }\n}\n
        insertion_sort.ts
        /* Сортировка вставками */\nfunction insertionSort(nums: number[]): void {\n    // Внешний цикл: отсортированный диапазон [0, i-1]\n    for (let i = 1; i < nums.length; i++) {\n        const base = nums[i];\n        let j = i - 1;\n        // Внутренний цикл: вставить base в правильную позицию отсортированного диапазона [0, i-1]\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j]; // Сдвинуть nums[j] на одну позицию вправо\n            j--;\n        }\n        nums[j + 1] = base; // Поместить base в правильную позицию\n    }\n}\n
        insertion_sort.dart
        /* Сортировка вставками */\nvoid insertionSort(List<int> nums) {\n  // Внешний цикл: отсортированный диапазон [0, i-1]\n  for (int i = 1; i < nums.length; i++) {\n    int base = nums[i], j = i - 1;\n    // Внутренний цикл: вставить base в правильную позицию отсортированного диапазона [0, i-1]\n    while (j >= 0 && nums[j] > base) {\n      nums[j + 1] = nums[j]; // Сдвинуть nums[j] на одну позицию вправо\n      j--;\n    }\n    nums[j + 1] = base; // Поместить base в правильную позицию\n  }\n}\n
        insertion_sort.rs
        /* Сортировка вставками */\nfn insertion_sort(nums: &mut [i32]) {\n    // Внешний цикл: отсортированный диапазон [0, i-1]\n    for i in 1..nums.len() {\n        let (base, mut j) = (nums[i], (i - 1) as i32);\n        // Внутренний цикл: вставить base в правильную позицию отсортированного диапазона [0, i-1]\n        while j >= 0 && nums[j as usize] > base {\n            nums[(j + 1) as usize] = nums[j as usize]; // Сдвинуть nums[j] на одну позицию вправо\n            j -= 1;\n        }\n        nums[(j + 1) as usize] = base; // Поместить base в правильную позицию\n    }\n}\n
        insertion_sort.c
        /* Сортировка вставками */\nvoid insertionSort(int nums[], int size) {\n    // Внешний цикл: отсортированный диапазон [0, i-1]\n    for (int i = 1; i < size; i++) {\n        int base = nums[i], j = i - 1;\n        // Внутренний цикл: вставить base в правильную позицию отсортированного диапазона [0, i-1]\n        while (j >= 0 && nums[j] > base) {\n            // Сдвинуть nums[j] на одну позицию вправо\n            nums[j + 1] = nums[j];\n            j--;\n        }\n        // Поместить base в правильную позицию\n        nums[j + 1] = base;\n    }\n}\n
        insertion_sort.kt
        /* Сортировка вставками */\nfun insertionSort(nums: IntArray) {\n    // Внешний цикл: отсортированные элементы равны 1, 2, ..., n\n    for (i in nums.indices) {\n        val base = nums[i]\n        var j = i - 1\n        // Внутренний цикл: вставить base в правильную позицию отсортированного диапазона [0, i-1]\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j] // Сдвинуть nums[j] на одну позицию вправо\n            j--\n        }\n        nums[j + 1] = base        // Поместить base в правильную позицию\n    }\n}\n
        insertion_sort.rb
        ### Сортировка вставками ###\ndef insertion_sort(nums)\n  n = nums.length\n  # Внешний цикл: отсортированный диапазон [0, i-1]\n  for i in 1...n\n    base = nums[i]\n    j = i - 1\n    # Внутренний цикл: вставить base в правильную позицию отсортированного диапазона [0, i-1]\n    while j >= 0 && nums[j] > base\n      nums[j + 1] = nums[j] # Сдвинуть nums[j] на одну позицию вправо\n      j -= 1\n    end\n    nums[j + 1] = base # Поместить base в правильную позицию\n  end\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 11. Сортировка","11.4   Сортировка вставками"],"tags":[]},{"location":"chapter_sorting/insertion_sort/#1142","level":2,"title":"11.4.2   Характеристики алгоритма","text":"
        • Временная сложность равна \\(O(n^2)\\), алгоритм адаптивен: в худшем случае каждой операции вставки требуется соответственно \\(n - 1\\), \\(n-2\\), \\(\\dots\\), \\(2\\), \\(1\\) итераций, а их сумма равна \\((n - 1) n / 2\\) , поэтому временная сложность равна \\(O(n^2)\\) . Если входные данные уже упорядочены, операция вставки завершается раньше. Когда входной массив полностью отсортирован, сортировка вставками достигает лучшей временной сложности \\(O(n)\\) .
        • Пространственная сложность равна \\(O(1)\\), сортировка выполняется на месте: указатели \\(i\\) и \\(j\\) используют константный объем дополнительной памяти.
        • Стабильная сортировка: в процессе вставки элементы помещаются справа от равных им элементов, поэтому их относительный порядок не меняется.
        ","path":["Глава 11. Сортировка","11.4   Сортировка вставками"],"tags":[]},{"location":"chapter_sorting/insertion_sort/#1143","level":2,"title":"11.4.3   Преимущества сортировки вставками","text":"

        Временная сложность сортировки вставками равна \\(O(n^2)\\) , а у быстрой сортировки, которую мы скоро изучим, временная сложность равна \\(O(n \\log n)\\) . Несмотря на более высокую асимптотическую сложность, на малых объемах данных сортировка вставками обычно работает быстрее.

        Этот вывод похож на сравнение линейного и двоичного поиска. Алгоритмы уровня \\(O(n \\log n)\\) , такие как быстрая сортировка, относятся к алгоритмам на основе стратегии \"разделяй и властвуй\" и обычно включают больше элементарных вычислений. Когда объем данных мал, значения \\(n^2\\) и \\(n \\log n\\) близки друг к другу, поэтому асимптотика не доминирует, а решающим становится число элементарных операций в каждом раунде.

        На практике встроенные функции сортировки во многих языках программирования (например, в Java) используют сортировку вставками. Общая идея такова: для длинных массивов применять алгоритмы сортировки на основе стратегии \"разделяй и властвуй\", например быструю сортировку; для коротких массивов сразу использовать сортировку вставками.

        Хотя сортировка пузырьком, выбором и вставками имеют одинаковую временную сложность \\(O(n^2)\\) , в реальных задачах сортировка вставками используется заметно чаще, чем сортировка пузырьком и сортировка выбором. Основные причины таковы.

        • Сортировка пузырьком основана на обмене элементов, для чего нужна временная переменная и суммарно выполняются 3 элементарные операции; сортировка вставками основана на присваивании элементов и требует всего 1 элементарной операции. Поэтому вычислительные затраты сортировки пузырьком обычно выше, чем у сортировки вставками.
        • Временная сложность сортировки выбором в любом случае равна \\(O(n^2)\\) . Если входные данные уже частично упорядочены, сортировка вставками обычно эффективнее сортировки выбором.
        • Сортировка выбором нестабильна, поэтому ее нельзя использовать для многоуровневой сортировки.
        ","path":["Глава 11. Сортировка","11.4   Сортировка вставками"],"tags":[]},{"location":"chapter_sorting/merge_sort/","level":1,"title":"11.6   Сортировка слиянием","text":"

        Сортировка слиянием (merge sort) - это алгоритм сортировки, основанный на стратегии \"разделяй и властвуй\", который включает этапы \"разделения\" и \"слияния\", показанные на рисунке 11-10.

        1. Этап разделения: массив рекурсивно делится пополам, и задача сортировки длинного массива превращается в задачи сортировки более коротких массивов.
        2. Этап слияния: когда длина подмассива становится равной 1, разделение завершается и начинается слияние; два коротких упорядоченных массива непрерывно объединяются в один более длинный упорядоченный массив, пока процесс не завершится.

        Рисунок 11-10   Этапы разделения и слияния в сортировке слиянием

        ","path":["Глава 11. Сортировка","11.6   Сортировка слиянием"],"tags":[]},{"location":"chapter_sorting/merge_sort/#1161","level":2,"title":"11.6.1   Алгоритм","text":"

        Как показано на рисунке 11-11, на этапе \"разделения\" массив рекурсивно разбивается сверху вниз по середине на два подмассива.

        1. Вычислить середину массива mid и рекурсивно разделить левый подмассив (интервал [left, mid] ) и правый подмассив (интервал [mid + 1, right] ).
        2. Рекурсивно повторять шаг 1. , пока длина подмассива не станет равной 1.

        Этап \"слияния\" снизу вверх объединяет левый и правый подмассивы в один упорядоченный массив. Следует заметить, что начиная с подмассивов длины 1, каждый подмассив в фазе слияния уже является упорядоченным.

        <1><2><3><4><5><6><7><8><9><10>

        Рисунок 11-11   Шаги сортировки слиянием

        Нетрудно заметить, что порядок рекурсии в сортировке слиянием совпадает с порядком обхода в глубину двоичного дерева.

        • Обход в глубину: сначала рекурсивно обходится левое поддерево, затем правое поддерево, а в конце обрабатывается корневой узел.
        • Сортировка слиянием: сначала рекурсивно разделяется левый подмассив, затем правый подмассив, а в конце выполняется слияние.

        Реализация сортировки слиянием показана в коде ниже. Обратите внимание: в nums объединяемый интервал равен [left, right] , а соответствующий интервал в tmp равен [0, right - left] .

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby merge_sort.py
        def merge(nums: list[int], left: int, mid: int, right: int):\n    \"\"\"Объединить левый и правый подмассивы\"\"\"\n    # Диапазон левого подмассива: [left, mid], диапазон правого подмассива: [mid+1, right]\n    # Создать временный массив tmp для хранения результата слияния\n    tmp = [0] * (right - left + 1)\n    # Инициализировать начальные индексы левого и правого подмассивов\n    i, j, k = left, mid + 1, 0\n    # Пока в левом и правом подмассивах еще есть элементы, сравнивать их и копировать меньший во временный массив\n    while i <= mid and j <= right:\n        if nums[i] <= nums[j]:\n            tmp[k] = nums[i]\n            i += 1\n        else:\n            tmp[k] = nums[j]\n            j += 1\n        k += 1\n    # Скопировать оставшиеся элементы левого и правого подмассивов во временный массив\n    while i <= mid:\n        tmp[k] = nums[i]\n        i += 1\n        k += 1\n    while j <= right:\n        tmp[k] = nums[j]\n        j += 1\n        k += 1\n    # Скопировать элементы временного массива tmp обратно в соответствующий диапазон исходного массива nums\n    for k in range(0, len(tmp)):\n        nums[left + k] = tmp[k]\n\ndef merge_sort(nums: list[int], left: int, right: int):\n    \"\"\"Сортировка слиянием\"\"\"\n    # Условие завершения\n    if left >= right:\n        return  # Завершить рекурсию, когда длина подмассива равна 1\n    # Этап разбиения\n    mid = (left + right) // 2 # Вычислить середину\n    merge_sort(nums, left, mid)  # Рекурсивно обработать левый подмассив\n    merge_sort(nums, mid + 1, right)  # Рекурсивно обработать правый подмассив\n    # Этап слияния\n    merge(nums, left, mid, right)\n
        merge_sort.cpp
        /* Объединить левый и правый подмассивы */\nvoid merge(vector<int> &nums, int left, int mid, int right) {\n    // Диапазон левого подмассива: [left, mid], диапазон правого подмассива: [mid+1, right]\n    // Создать временный массив tmp для хранения результата слияния\n    vector<int> tmp(right - left + 1);\n    // Инициализировать начальные индексы левого и правого подмассивов\n    int i = left, j = mid + 1, k = 0;\n    // Пока в левом и правом подмассивах еще есть элементы, сравнивать их и копировать меньший во временный массив\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j])\n            tmp[k++] = nums[i++];\n        else\n            tmp[k++] = nums[j++];\n    }\n    // Скопировать оставшиеся элементы левого и правого подмассивов во временный массив\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // Скопировать элементы временного массива tmp обратно в соответствующий диапазон исходного массива nums\n    for (k = 0; k < tmp.size(); k++) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* Сортировка слиянием */\nvoid mergeSort(vector<int> &nums, int left, int right) {\n    // Условие завершения\n    if (left >= right)\n        return; // Завершить рекурсию, когда длина подмассива равна 1\n    // Этап разбиения\n    int mid = left + (right - left) / 2;    // Вычислить середину\n    mergeSort(nums, left, mid);      // Рекурсивно обработать левый подмассив\n    mergeSort(nums, mid + 1, right); // Рекурсивно обработать правый подмассив\n    // Этап слияния\n    merge(nums, left, mid, right);\n}\n
        merge_sort.java
        /* Объединить левый и правый подмассивы */\nvoid merge(int[] nums, int left, int mid, int right) {\n    // Диапазон левого подмассива: [left, mid], диапазон правого подмассива: [mid+1, right]\n    // Создать временный массив tmp для хранения результата слияния\n    int[] tmp = new int[right - left + 1];\n    // Инициализировать начальные индексы левого и правого подмассивов\n    int i = left, j = mid + 1, k = 0;\n    // Пока в левом и правом подмассивах еще есть элементы, сравнивать их и копировать меньший во временный массив\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j])\n            tmp[k++] = nums[i++];\n        else\n            tmp[k++] = nums[j++];\n    }\n    // Скопировать оставшиеся элементы левого и правого подмассивов во временный массив\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // Скопировать элементы временного массива tmp обратно в соответствующий диапазон исходного массива nums\n    for (k = 0; k < tmp.length; k++) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* Сортировка слиянием */\nvoid mergeSort(int[] nums, int left, int right) {\n    // Условие завершения\n    if (left >= right)\n        return; // Завершить рекурсию, когда длина подмассива равна 1\n    // Этап разбиения\n    int mid = left + (right - left) / 2; // Вычислить середину\n    mergeSort(nums, left, mid); // Рекурсивно обработать левый подмассив\n    mergeSort(nums, mid + 1, right); // Рекурсивно обработать правый подмассив\n    // Этап слияния\n    merge(nums, left, mid, right);\n}\n
        merge_sort.cs
        /* Объединить левый и правый подмассивы */\nvoid Merge(int[] nums, int left, int mid, int right) {\n    // Диапазон левого подмассива: [left, mid], диапазон правого подмассива: [mid+1, right]\n    // Создать временный массив tmp для хранения результата слияния\n    int[] tmp = new int[right - left + 1];\n    // Инициализировать начальные индексы левого и правого подмассивов\n    int i = left, j = mid + 1, k = 0;\n    // Пока в левом и правом подмассивах еще есть элементы, сравнивать их и копировать меньший во временный массив\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j])\n            tmp[k++] = nums[i++];\n        else\n            tmp[k++] = nums[j++];\n    }\n    // Скопировать оставшиеся элементы левого и правого подмассивов во временный массив\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // Скопировать элементы временного массива tmp обратно в соответствующий диапазон исходного массива nums\n    for (k = 0; k < tmp.Length; ++k) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* Сортировка слиянием */\nvoid MergeSort(int[] nums, int left, int right) {\n    // Условие завершения\n    if (left >= right) return;       // Завершить рекурсию, когда длина подмассива равна 1\n    // Этап разбиения\n    int mid = left + (right - left) / 2;    // Вычислить середину\n    MergeSort(nums, left, mid);      // Рекурсивно обработать левый подмассив\n    MergeSort(nums, mid + 1, right); // Рекурсивно обработать правый подмассив\n    // Этап слияния\n    Merge(nums, left, mid, right);\n}\n
        merge_sort.go
        /* Объединить левый и правый подмассивы */\nfunc merge(nums []int, left, mid, right int) {\n    // Диапазон левого подмассива: [left, mid], диапазон правого подмассива: [mid+1, right]\n    // Создать временный массив tmp для хранения результата слияния\n    tmp := make([]int, right-left+1)\n    // Инициализировать начальные индексы левого и правого подмассивов\n    i, j, k := left, mid+1, 0\n    // Пока в левом и правом подмассивах еще есть элементы, сравнивать их и копировать меньший во временный массив\n    for i <= mid && j <= right {\n        if nums[i] <= nums[j] {\n            tmp[k] = nums[i]\n            i++\n        } else {\n            tmp[k] = nums[j]\n            j++\n        }\n        k++\n    }\n    // Скопировать оставшиеся элементы левого и правого подмассивов во временный массив\n    for i <= mid {\n        tmp[k] = nums[i]\n        i++\n        k++\n    }\n    for j <= right {\n        tmp[k] = nums[j]\n        j++\n        k++\n    }\n    // Скопировать элементы временного массива tmp обратно в соответствующий диапазон исходного массива nums\n    for k := 0; k < len(tmp); k++ {\n        nums[left+k] = tmp[k]\n    }\n}\n\n/* Сортировка слиянием */\nfunc mergeSort(nums []int, left, right int) {\n    // Условие завершения\n    if left >= right {\n        return\n    }\n    // Этап разбиения\n    mid := left + (right - left) / 2\n    mergeSort(nums, left, mid)\n    mergeSort(nums, mid+1, right)\n    // Этап слияния\n    merge(nums, left, mid, right)\n}\n
        merge_sort.swift
        /* Объединить левый и правый подмассивы */\nfunc merge(nums: inout [Int], left: Int, mid: Int, right: Int) {\n    // Диапазон левого подмассива: [left, mid], диапазон правого подмассива: [mid+1, right]\n    // Создать временный массив tmp для хранения результата слияния\n    var tmp = Array(repeating: 0, count: right - left + 1)\n    // Инициализировать начальные индексы левого и правого подмассивов\n    var i = left, j = mid + 1, k = 0\n    // Пока в левом и правом подмассивах еще есть элементы, сравнивать их и копировать меньший во временный массив\n    while i <= mid, j <= right {\n        if nums[i] <= nums[j] {\n            tmp[k] = nums[i]\n            i += 1\n        } else {\n            tmp[k] = nums[j]\n            j += 1\n        }\n        k += 1\n    }\n    // Скопировать оставшиеся элементы левого и правого подмассивов во временный массив\n    while i <= mid {\n        tmp[k] = nums[i]\n        i += 1\n        k += 1\n    }\n    while j <= right {\n        tmp[k] = nums[j]\n        j += 1\n        k += 1\n    }\n    // Скопировать элементы временного массива tmp обратно в соответствующий диапазон исходного массива nums\n    for k in tmp.indices {\n        nums[left + k] = tmp[k]\n    }\n}\n\n/* Сортировка слиянием */\nfunc mergeSort(nums: inout [Int], left: Int, right: Int) {\n    // Условие завершения\n    if left >= right { // Завершить рекурсию, когда длина подмассива равна 1\n        return\n    }\n    // Этап разбиения\n    let mid = left + (right - left) / 2 // Вычислить середину\n    mergeSort(nums: &nums, left: left, right: mid) // Рекурсивно обработать левый подмассив\n    mergeSort(nums: &nums, left: mid + 1, right: right) // Рекурсивно обработать правый подмассив\n    // Этап слияния\n    merge(nums: &nums, left: left, mid: mid, right: right)\n}\n
        merge_sort.js
        /* Объединить левый и правый подмассивы */\nfunction merge(nums, left, mid, right) {\n    // Диапазон левого подмассива: [left, mid], диапазон правого подмассива: [mid+1, right]\n    // Создать временный массив tmp для хранения результата слияния\n    const tmp = new Array(right - left + 1);\n    // Инициализировать начальные индексы левого и правого подмассивов\n    let i = left,\n        j = mid + 1,\n        k = 0;\n    // Пока в левом и правом подмассивах еще есть элементы, сравнивать их и копировать меньший во временный массив\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j]) {\n            tmp[k++] = nums[i++];\n        } else {\n            tmp[k++] = nums[j++];\n        }\n    }\n    // Скопировать оставшиеся элементы левого и правого подмассивов во временный массив\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // Скопировать элементы временного массива tmp обратно в соответствующий диапазон исходного массива nums\n    for (k = 0; k < tmp.length; k++) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* Сортировка слиянием */\nfunction mergeSort(nums, left, right) {\n    // Условие завершения\n    if (left >= right) return; // Завершить рекурсию, когда длина подмассива равна 1\n    // Этап разбиения\n    let mid = Math.floor(left + (right - left) / 2); // Вычислить середину\n    mergeSort(nums, left, mid); // Рекурсивно обработать левый подмассив\n    mergeSort(nums, mid + 1, right); // Рекурсивно обработать правый подмассив\n    // Этап слияния\n    merge(nums, left, mid, right);\n}\n
        merge_sort.ts
        /* Объединить левый и правый подмассивы */\nfunction merge(nums: number[], left: number, mid: number, right: number): void {\n    // Диапазон левого подмассива: [left, mid], диапазон правого подмассива: [mid+1, right]\n    // Создать временный массив tmp для хранения результата слияния\n    const tmp = new Array(right - left + 1);\n    // Инициализировать начальные индексы левого и правого подмассивов\n    let i = left,\n        j = mid + 1,\n        k = 0;\n    // Пока в левом и правом подмассивах еще есть элементы, сравнивать их и копировать меньший во временный массив\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j]) {\n            tmp[k++] = nums[i++];\n        } else {\n            tmp[k++] = nums[j++];\n        }\n    }\n    // Скопировать оставшиеся элементы левого и правого подмассивов во временный массив\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // Скопировать элементы временного массива tmp обратно в соответствующий диапазон исходного массива nums\n    for (k = 0; k < tmp.length; k++) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* Сортировка слиянием */\nfunction mergeSort(nums: number[], left: number, right: number): void {\n    // Условие завершения\n    if (left >= right) return; // Завершить рекурсию, когда длина подмассива равна 1\n    // Этап разбиения\n    let mid = Math.floor(left + (right - left) / 2); // Вычислить середину\n    mergeSort(nums, left, mid); // Рекурсивно обработать левый подмассив\n    mergeSort(nums, mid + 1, right); // Рекурсивно обработать правый подмассив\n    // Этап слияния\n    merge(nums, left, mid, right);\n}\n
        merge_sort.dart
        /* Объединить левый и правый подмассивы */\nvoid merge(List<int> nums, int left, int mid, int right) {\n  // Диапазон левого подмассива: [left, mid], диапазон правого подмассива: [mid+1, right]\n  // Создать временный массив tmp для хранения результата слияния\n  List<int> tmp = List.filled(right - left + 1, 0);\n  // Инициализировать начальные индексы левого и правого подмассивов\n  int i = left, j = mid + 1, k = 0;\n  // Пока в левом и правом подмассивах еще есть элементы, сравнивать их и копировать меньший во временный массив\n  while (i <= mid && j <= right) {\n    if (nums[i] <= nums[j])\n      tmp[k++] = nums[i++];\n    else\n      tmp[k++] = nums[j++];\n  }\n  // Скопировать оставшиеся элементы левого и правого подмассивов во временный массив\n  while (i <= mid) {\n    tmp[k++] = nums[i++];\n  }\n  while (j <= right) {\n    tmp[k++] = nums[j++];\n  }\n  // Скопировать элементы временного массива tmp обратно в соответствующий диапазон исходного массива nums\n  for (k = 0; k < tmp.length; k++) {\n    nums[left + k] = tmp[k];\n  }\n}\n\n/* Сортировка слиянием */\nvoid mergeSort(List<int> nums, int left, int right) {\n  // Условие завершения\n  if (left >= right) return; // Завершить рекурсию, когда длина подмассива равна 1\n  // Этап разбиения\n  int mid = left + (right - left) ~/ 2; // Вычислить середину\n  mergeSort(nums, left, mid); // Рекурсивно обработать левый подмассив\n  mergeSort(nums, mid + 1, right); // Рекурсивно обработать правый подмассив\n  // Этап слияния\n  merge(nums, left, mid, right);\n}\n
        merge_sort.rs
        /* Объединить левый и правый подмассивы */\nfn merge(nums: &mut [i32], left: usize, mid: usize, right: usize) {\n    // Диапазон левого подмассива: [left, mid], диапазон правого подмассива: [mid+1, right]\n    // Создать временный массив tmp для хранения результата слияния\n    let tmp_size = right - left + 1;\n    let mut tmp = vec![0; tmp_size];\n    // Инициализировать начальные индексы левого и правого подмассивов\n    let (mut i, mut j, mut k) = (left, mid + 1, 0);\n    // Пока в левом и правом подмассивах еще есть элементы, сравнивать их и копировать меньший во временный массив\n    while i <= mid && j <= right {\n        if nums[i] <= nums[j] {\n            tmp[k] = nums[i];\n            i += 1;\n        } else {\n            tmp[k] = nums[j];\n            j += 1;\n        }\n        k += 1;\n    }\n    // Скопировать оставшиеся элементы левого и правого подмассивов во временный массив\n    while i <= mid {\n        tmp[k] = nums[i];\n        k += 1;\n        i += 1;\n    }\n    while j <= right {\n        tmp[k] = nums[j];\n        k += 1;\n        j += 1;\n    }\n    // Скопировать элементы временного массива tmp обратно в соответствующий диапазон исходного массива nums\n    for k in 0..tmp_size {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* Сортировка слиянием */\nfn merge_sort(nums: &mut [i32], left: usize, right: usize) {\n    // Условие завершения\n    if left >= right {\n        return; // Завершить рекурсию, когда длина подмассива равна 1\n    }\n\n    // Этап разбиения\n    let mid = left + (right - left) / 2; // Вычислить середину\n    merge_sort(nums, left, mid); // Рекурсивно обработать левый подмассив\n    merge_sort(nums, mid + 1, right); // Рекурсивно обработать правый подмассив\n\n    // Этап слияния\n    merge(nums, left, mid, right);\n}\n
        merge_sort.c
        /* Объединить левый и правый подмассивы */\nvoid merge(int *nums, int left, int mid, int right) {\n    // Диапазон левого подмассива: [left, mid], диапазон правого подмассива: [mid+1, right]\n    // Создать временный массив tmp для хранения результата слияния\n    int tmpSize = right - left + 1;\n    int *tmp = (int *)malloc(tmpSize * sizeof(int));\n    // Инициализировать начальные индексы левого и правого подмассивов\n    int i = left, j = mid + 1, k = 0;\n    // Пока в левом и правом подмассивах еще есть элементы, сравнивать их и копировать меньший во временный массив\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j]) {\n            tmp[k++] = nums[i++];\n        } else {\n            tmp[k++] = nums[j++];\n        }\n    }\n    // Скопировать оставшиеся элементы левого и правого подмассивов во временный массив\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // Скопировать элементы временного массива tmp обратно в соответствующий диапазон исходного массива nums\n    for (k = 0; k < tmpSize; ++k) {\n        nums[left + k] = tmp[k];\n    }\n    // Освободить память\n    free(tmp);\n}\n\n/* Сортировка слиянием */\nvoid mergeSort(int *nums, int left, int right) {\n    // Условие завершения\n    if (left >= right)\n        return; // Завершить рекурсию, когда длина подмассива равна 1\n    // Этап разбиения\n    int mid = left + (right - left) / 2;    // Вычислить середину\n    mergeSort(nums, left, mid);      // Рекурсивно обработать левый подмассив\n    mergeSort(nums, mid + 1, right); // Рекурсивно обработать правый подмассив\n    // Этап слияния\n    merge(nums, left, mid, right);\n}\n
        merge_sort.kt
        /* Объединить левый и правый подмассивы */\nfun merge(nums: IntArray, left: Int, mid: Int, right: Int) {\n    // Диапазон левого подмассива: [left, mid], диапазон правого подмассива: [mid+1, right]\n    // Создать временный массив tmp для хранения результата слияния\n    val tmp = IntArray(right - left + 1)\n    // Инициализировать начальные индексы левого и правого подмассивов\n    var i = left\n    var j = mid + 1\n    var k = 0\n    // Пока в левом и правом подмассивах еще есть элементы, сравнивать их и копировать меньший во временный массив\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j])\n            tmp[k++] = nums[i++]\n        else\n            tmp[k++] = nums[j++]\n    }\n    // Скопировать оставшиеся элементы левого и правого подмассивов во временный массив\n    while (i <= mid) {\n        tmp[k++] = nums[i++]\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++]\n    }\n    // Скопировать элементы временного массива tmp обратно в соответствующий диапазон исходного массива nums\n    for (l in tmp.indices) {\n        nums[left + l] = tmp[l]\n    }\n}\n\n/* Сортировка слиянием */\nfun mergeSort(nums: IntArray, left: Int, right: Int) {\n    // Условие завершения\n    if (left >= right) return  // Завершить рекурсию, когда длина подмассива равна 1\n    // Этап разбиения\n    val mid = left + (right - left) / 2 // Вычислить середину\n    mergeSort(nums, left, mid) // Рекурсивно обработать левый подмассив\n    mergeSort(nums, mid + 1, right) // Рекурсивно обработать правый подмассив\n    // Этап слияния\n    merge(nums, left, mid, right)\n}\n
        merge_sort.rb
        ### Слияние левого и правого подмассивов ###\ndef merge(nums, left, mid, right)\n  # Интервал левого подмассива: [left, mid], правого подмассива: [mid+1, right]\n  # Создать временный массив tmp для хранения результата слияния\n  tmp = Array.new(right - left + 1, 0)\n  # Инициализировать начальные индексы левого и правого подмассивов\n  i, j, k = left, mid + 1, 0\n  # Пока в левом и правом подмассивах еще есть элементы, сравнивать их и копировать меньший во временный массив\n  while i <= mid && j <= right\n    if nums[i] <= nums[j]\n      tmp[k] = nums[i]\n      i += 1\n    else\n      tmp[k] = nums[j]\n      j += 1\n    end\n    k += 1\n  end\n  # Скопировать оставшиеся элементы левого и правого подмассивов во временный массив\n  while i <= mid\n    tmp[k] = nums[i]\n    i += 1\n    k += 1\n  end\n  while j <= right\n    tmp[k] = nums[j]\n    j += 1\n    k += 1\n  end\n  # Скопировать элементы временного массива tmp обратно в соответствующий диапазон исходного массива nums\n  (0...tmp.length).each do |k|\n    nums[left + k] = tmp[k]\n  end\nend\n\n### Сортировка слиянием ###\ndef merge_sort(nums, left, right)\n  # Условие завершения\n  # Когда длина подмассива равна 1, рекурсия завершается\n  return if left >= right\n  # Этап разбиения\n  mid = left + (right - left) / 2 # Вычислить середину\n  merge_sort(nums, left, mid) # Рекурсивно обработать левый подмассив\n  merge_sort(nums, mid + 1, right) # Рекурсивно обработать правый подмассив\n  # Этап слияния\n  merge(nums, left, mid, right)\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 11. Сортировка","11.6   Сортировка слиянием"],"tags":[]},{"location":"chapter_sorting/merge_sort/#1162","level":2,"title":"11.6.2   Характеристики алгоритма","text":"
        • Временная сложность равна \\(O(n \\log n)\\), алгоритм не является адаптивным: этап разделения создает дерево рекурсии высоты \\(\\log n\\) , а суммарное число операций слияния на каждом уровне равно \\(n\\) , поэтому общая временная сложность составляет \\(O(n \\log n)\\) .
        • Пространственная сложность равна \\(O(n)\\), сортировка не выполняется на месте: глубина рекурсии равна \\(\\log n\\) , из-за чего требуется \\(O(\\log n)\\) памяти под стек вызовов. Для этапа слияния нужен вспомогательный массив, поэтому дополнительно используется \\(O(n)\\) памяти.
        • Стабильная сортировка: в процессе слияния относительный порядок равных элементов не меняется.
        ","path":["Глава 11. Сортировка","11.6   Сортировка слиянием"],"tags":[]},{"location":"chapter_sorting/merge_sort/#1163","level":2,"title":"11.6.3   Сортировка связного списка","text":"

        Для связных списков сортировка слиянием имеет заметное преимущество перед другими алгоритмами сортировки: пространственную сложность задачи сортировки списка можно оптимизировать до \\(O(1)\\).

        • Этап разделения: работу по разбиению списка можно реализовать с помощью \"итерации\" вместо \"рекурсии\", тем самым устранив расход памяти на стек вызовов.
        • Этап слияния: в связном списке добавление и удаление узлов требует только изменения ссылок (указателей), поэтому при слиянии двух коротких упорядоченных списков в один длинный упорядоченный список не нужно создавать дополнительный список.

        Детали реализации достаточно сложны; заинтересованные читатели могут обратиться к соответствующим материалам самостоятельно.

        ","path":["Глава 11. Сортировка","11.6   Сортировка слиянием"],"tags":[]},{"location":"chapter_sorting/quick_sort/","level":1,"title":"11.5   Быстрая сортировка","text":"

        Быстрая сортировка (quick sort) - это алгоритм сортировки, основанный на стратегии \"разделяй и властвуй\"; он работает эффективно и применяется очень широко.

        Ключевая операция быстрой сортировки - это \"разделение с опорным элементом\". Ее цель такова: выбрать некоторый элемент массива в качестве \"опорного\" и переместить все элементы меньше опорного влево от него, а все элементы больше опорного - вправо. Конкретный процесс показан на рисунке 11-8.

        1. Выбрать самый левый элемент массива как опорный и инициализировать два указателя i и j , направленные на левую и правую границы массива.
        2. Запустить цикл, в котором i и j ищут соответственно первый элемент, больший опорного, и первый элемент, меньший опорного, после чего эти два элемента меняются местами.
        3. Повторять шаг 2. , пока указатели i и j не встретятся, а затем обменять опорный элемент с элементом на границе двух подмассивов.
        <1><2><3><4><5><6><7><8><9>

        Рисунок 11-8   Шаги разделения с опорным элементом

        После завершения разделения исходный массив разбивается на три части: левый подмассив, опорный элемент и правый подмассив; при этом выполняется условие \"любой элемент левого подмассива \\(\\leq\\) опорный элемент \\(\\leq\\) любой элемент правого подмассива\". Следовательно, далее нам нужно лишь отсортировать эти два подмассива.

        Стратегия разделяй и властвуй в быстрой сортировке

        Иными словами, разделение с опорным элементом сводит задачу сортировки длинного массива к двум задачам сортировки более коротких массивов.

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py
        def partition(self, nums: list[int], left: int, right: int) -> int:\n    \"\"\"Разбиение с опорными указателями\"\"\"\n    # Взять nums[left] в качестве опорного элемента\n    i, j = left, right\n    while i < j:\n        while i < j and nums[j] >= nums[left]:\n            j -= 1  # Идти справа налево в поисках первого элемента меньше опорного\n        while i < j and nums[i] <= nums[left]:\n            i += 1  # Идти слева направо в поисках первого элемента больше опорного\n        # Обмен элементов\n        nums[i], nums[j] = nums[j], nums[i]\n    # Переместить опорный элемент на границу двух подмассивов\n    nums[i], nums[left] = nums[left], nums[i]\n    return i  # Вернуть индекс опорного элемента\n
        quick_sort.cpp
        /* Разбиение с опорными указателями */\nint partition(vector<int> &nums, int left, int right) {\n    // Взять nums[left] в качестве опорного элемента\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;                // Идти справа налево в поисках первого элемента меньше опорного\n        while (i < j && nums[i] <= nums[left])\n            i++;                // Идти слева направо в поисках первого элемента больше опорного\n        swap(nums[i], nums[j]); // Поменять эти два элемента местами\n    }\n    swap(nums[i], nums[left]);  // Переместить опорный элемент на границу двух подмассивов\n    return i;                   // Вернуть индекс опорного элемента\n}\n
        quick_sort.java
        /* Обмен элементов */\nvoid swap(int[] nums, int i, int j) {\n    int tmp = nums[i];\n    nums[i] = nums[j];\n    nums[j] = tmp;\n}\n\n/* Разбиение с опорными указателями */\nint partition(int[] nums, int left, int right) {\n    // Взять nums[left] в качестве опорного элемента\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;          // Идти справа налево в поисках первого элемента меньше опорного\n        while (i < j && nums[i] <= nums[left])\n            i++;          // Идти слева направо в поисках первого элемента больше опорного\n        swap(nums, i, j); // Поменять эти два элемента местами\n    }\n    swap(nums, i, left);  // Переместить опорный элемент на границу двух подмассивов\n    return i;             // Вернуть индекс опорного элемента\n}\n
        quick_sort.cs
        /* Обмен элементов */\nvoid Swap(int[] nums, int i, int j) {\n    (nums[j], nums[i]) = (nums[i], nums[j]);\n}\n\n/* Разбиение с опорными указателями */\nint Partition(int[] nums, int left, int right) {\n    // Взять nums[left] в качестве опорного элемента\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;          // Идти справа налево в поисках первого элемента меньше опорного\n        while (i < j && nums[i] <= nums[left])\n            i++;          // Идти слева направо в поисках первого элемента больше опорного\n        Swap(nums, i, j); // Поменять эти два элемента местами\n    }\n    Swap(nums, i, left);  // Переместить опорный элемент на границу двух подмассивов\n    return i;             // Вернуть индекс опорного элемента\n}\n
        quick_sort.go
        /* Разбиение с опорными указателями */\nfunc (q *quickSort) partition(nums []int, left, right int) int {\n    // Взять nums[left] в качестве опорного элемента\n    i, j := left, right\n    for i < j {\n        for i < j && nums[j] >= nums[left] {\n            j-- // Идти справа налево в поисках первого элемента меньше опорного\n        }\n        for i < j && nums[i] <= nums[left] {\n            i++ // Идти слева направо в поисках первого элемента больше опорного\n        }\n        // Обмен элементов\n        nums[i], nums[j] = nums[j], nums[i]\n    }\n    // Переместить опорный элемент на границу двух подмассивов\n    nums[i], nums[left] = nums[left], nums[i]\n    return i // Вернуть индекс опорного элемента\n}\n
        quick_sort.swift
        /* Разбиение с опорными указателями */\nfunc partition(nums: inout [Int], left: Int, right: Int) -> Int {\n    // Взять nums[left] в качестве опорного элемента\n    var i = left\n    var j = right\n    while i < j {\n        while i < j, nums[j] >= nums[left] {\n            j -= 1 // Идти справа налево в поисках первого элемента меньше опорного\n        }\n        while i < j, nums[i] <= nums[left] {\n            i += 1 // Идти слева направо в поисках первого элемента больше опорного\n        }\n        nums.swapAt(i, j) // Поменять эти два элемента местами\n    }\n    nums.swapAt(i, left) // Переместить опорный элемент на границу двух подмассивов\n    return i // Вернуть индекс опорного элемента\n}\n
        quick_sort.js
        /* Обмен элементов */\nswap(nums, i, j) {\n    let tmp = nums[i];\n    nums[i] = nums[j];\n    nums[j] = tmp;\n}\n\n/* Разбиение с опорными указателями */\npartition(nums, left, right) {\n    // Взять nums[left] в качестве опорного элемента\n    let i = left,\n        j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) {\n            j -= 1; // Идти справа налево в поисках первого элемента меньше опорного\n        }\n        while (i < j && nums[i] <= nums[left]) {\n            i += 1; // Идти слева направо в поисках первого элемента больше опорного\n        }\n        // Обмен элементов\n        this.swap(nums, i, j); // Поменять эти два элемента местами\n    }\n    this.swap(nums, i, left); // Переместить опорный элемент на границу двух подмассивов\n    return i; // Вернуть индекс опорного элемента\n}\n
        quick_sort.ts
        /* Обмен элементов */\nswap(nums: number[], i: number, j: number): void {\n    let tmp = nums[i];\n    nums[i] = nums[j];\n    nums[j] = tmp;\n}\n\n/* Разбиение с опорными указателями */\npartition(nums: number[], left: number, right: number): number {\n    // Взять nums[left] в качестве опорного элемента\n    let i = left,\n        j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) {\n            j -= 1; // Идти справа налево в поисках первого элемента меньше опорного\n        }\n        while (i < j && nums[i] <= nums[left]) {\n            i += 1; // Идти слева направо в поисках первого элемента больше опорного\n        }\n        // Обмен элементов\n        this.swap(nums, i, j); // Поменять эти два элемента местами\n    }\n    this.swap(nums, i, left); // Переместить опорный элемент на границу двух подмассивов\n    return i; // Вернуть индекс опорного элемента\n}\n
        quick_sort.dart
        /* Обмен элементов */\nvoid _swap(List<int> nums, int i, int j) {\n  int tmp = nums[i];\n  nums[i] = nums[j];\n  nums[j] = tmp;\n}\n\n/* Разбиение с опорными указателями */\nint _partition(List<int> nums, int left, int right) {\n  // Взять nums[left] в качестве опорного элемента\n  int i = left, j = right;\n  while (i < j) {\n    while (i < j && nums[j] >= nums[left]) j--; // Идти справа налево в поисках первого элемента меньше опорного\n    while (i < j && nums[i] <= nums[left]) i++; // Идти слева направо в поисках первого элемента больше опорного\n    _swap(nums, i, j); // Поменять эти два элемента местами\n  }\n  _swap(nums, i, left); // Переместить опорный элемент на границу двух подмассивов\n  return i; // Вернуть индекс опорного элемента\n}\n
        quick_sort.rs
        /* Разбиение с опорными указателями */\nfn partition(nums: &mut [i32], left: usize, right: usize) -> usize {\n    // Взять nums[left] в качестве опорного элемента\n    let (mut i, mut j) = (left, right);\n    while i < j {\n        while i < j && nums[j] >= nums[left] {\n            j -= 1; // Идти справа налево в поисках первого элемента меньше опорного\n        }\n        while i < j && nums[i] <= nums[left] {\n            i += 1; // Идти слева направо в поисках первого элемента больше опорного\n        }\n        nums.swap(i, j); // Поменять эти два элемента местами\n    }\n    nums.swap(i, left); // Переместить опорный элемент на границу двух подмассивов\n    i // Вернуть индекс опорного элемента\n}\n
        quick_sort.c
        /* Обмен элементов */\nvoid swap(int nums[], int i, int j) {\n    int tmp = nums[i];\n    nums[i] = nums[j];\n    nums[j] = tmp;\n}\n\n/* Разбиение с опорными указателями */\nint partition(int nums[], int left, int right) {\n    // Взять nums[left] в качестве опорного элемента\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) {\n            j--; // Идти справа налево в поисках первого элемента меньше опорного\n        }\n        while (i < j && nums[i] <= nums[left]) {\n            i++; // Идти слева направо в поисках первого элемента больше опорного\n        }\n        // Поменять эти два элемента местами\n        swap(nums, i, j);\n    }\n    // Переместить опорный элемент на границу двух подмассивов\n    swap(nums, i, left);\n    // Вернуть индекс опорного элемента\n    return i;\n}\n
        quick_sort.kt
        /* Обмен элементов */\nfun swap(nums: IntArray, i: Int, j: Int) {\n    val temp = nums[i]\n    nums[i] = nums[j]\n    nums[j] = temp\n}\n\n/* Разбиение с опорными указателями */\nfun partition(nums: IntArray, left: Int, right: Int): Int {\n    // Взять nums[left] в качестве опорного элемента\n    var i = left\n    var j = right\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--           // Идти справа налево в поисках первого элемента меньше опорного\n        while (i < j && nums[i] <= nums[left])\n            i++           // Идти слева направо в поисках первого элемента больше опорного\n        swap(nums, i, j)  // Поменять эти два элемента местами\n    }\n    swap(nums, i, left)   // Переместить опорный элемент на границу двух подмассивов\n    return i              // Вернуть индекс опорного элемента\n}\n
        quick_sort.rb
        ### Разбиение с опорными указателями ###\ndef partition(nums, left, right)\n  # Взять nums[left] в качестве опорного элемента\n  i, j = left, right\n  while i < j\n    while i < j && nums[j] >= nums[left]\n      j -= 1 # Идти справа налево в поисках первого элемента меньше опорного\n    end\n    while i < j && nums[i] <= nums[left]\n      i += 1 # Идти слева направо в поисках первого элемента больше опорного\n    end\n    # Обмен элементов\n    nums[i], nums[j] = nums[j], nums[i]\n  end\n  # Переместить опорный элемент на границу двух подмассивов\n  nums[i], nums[left] = nums[left], nums[i]\n  i # Вернуть индекс опорного элемента\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 11. Сортировка","11.5   Быстрая сортировка"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1151","level":2,"title":"11.5.1   Алгоритм","text":"

        Общий процесс быстрой сортировки показан на рисунке 11-9.

        1. Сначала выполнить \"разделение с опорным элементом\" для исходного массива и получить неотсортированные левый и правый подмассивы.
        2. Затем рекурсивно выполнить \"разделение с опорным элементом\" для левого и правого подмассивов.
        3. Продолжать рекурсию до тех пор, пока длина подмассива не станет равной 1; после этого сортировка всего массива будет завершена.

        Рисунок 11-9   Процесс быстрой сортировки

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py
        def quick_sort(self, nums: list[int], left: int, right: int):\n    \"\"\"Быстрая сортировка\"\"\"\n    # Завершить рекурсию, когда длина подмассива равна 1\n    if left >= right:\n        return\n    # Разбиение с опорными указателями\n    pivot = self.partition(nums, left, right)\n    # Рекурсивно обработать левый и правый подмассивы\n    self.quick_sort(nums, left, pivot - 1)\n    self.quick_sort(nums, pivot + 1, right)\n
        quick_sort.cpp
        /* Быстрая сортировка */\nvoid quickSort(vector<int> &nums, int left, int right) {\n    // Завершить рекурсию, когда длина подмассива равна 1\n    if (left >= right)\n        return;\n    // Разбиение с опорными указателями\n    int pivot = partition(nums, left, right);\n    // Рекурсивно обработать левый и правый подмассивы\n    quickSort(nums, left, pivot - 1);\n    quickSort(nums, pivot + 1, right);\n}\n
        quick_sort.java
        /* Быстрая сортировка */\nvoid quickSort(int[] nums, int left, int right) {\n    // Завершить рекурсию, когда длина подмассива равна 1\n    if (left >= right)\n        return;\n    // Разбиение с опорными указателями\n    int pivot = partition(nums, left, right);\n    // Рекурсивно обработать левый и правый подмассивы\n    quickSort(nums, left, pivot - 1);\n    quickSort(nums, pivot + 1, right);\n}\n
        quick_sort.cs
        /* Быстрая сортировка */\nvoid QuickSort(int[] nums, int left, int right) {\n    // Завершить рекурсию, когда длина подмассива равна 1\n    if (left >= right)\n        return;\n    // Разбиение с опорными указателями\n    int pivot = Partition(nums, left, right);\n    // Рекурсивно обработать левый и правый подмассивы\n    QuickSort(nums, left, pivot - 1);\n    QuickSort(nums, pivot + 1, right);\n}\n
        quick_sort.go
        /* Быстрая сортировка */\nfunc (q *quickSort) quickSort(nums []int, left, right int) {\n    // Завершить рекурсию, когда длина подмассива равна 1\n    if left >= right {\n        return\n    }\n    // Разбиение с опорными указателями\n    pivot := q.partition(nums, left, right)\n    // Рекурсивно обработать левый и правый подмассивы\n    q.quickSort(nums, left, pivot-1)\n    q.quickSort(nums, pivot+1, right)\n}\n
        quick_sort.swift
        /* Быстрая сортировка */\nfunc quickSort(nums: inout [Int], left: Int, right: Int) {\n    // Завершить рекурсию, когда длина подмассива равна 1\n    if left >= right {\n        return\n    }\n    // Разбиение с опорными указателями\n    let pivot = partition(nums: &nums, left: left, right: right)\n    // Рекурсивно обработать левый и правый подмассивы\n    quickSort(nums: &nums, left: left, right: pivot - 1)\n    quickSort(nums: &nums, left: pivot + 1, right: right)\n}\n
        quick_sort.js
        /* Быстрая сортировка */\nquickSort(nums, left, right) {\n    // Завершить рекурсию, когда длина подмассива равна 1\n    if (left >= right) return;\n    // Разбиение с опорными указателями\n    const pivot = this.partition(nums, left, right);\n    // Рекурсивно обработать левый и правый подмассивы\n    this.quickSort(nums, left, pivot - 1);\n    this.quickSort(nums, pivot + 1, right);\n}\n
        quick_sort.ts
        /* Быстрая сортировка */\nquickSort(nums: number[], left: number, right: number): void {\n    // Завершить рекурсию, когда длина подмассива равна 1\n    if (left >= right) {\n        return;\n    }\n    // Разбиение с опорными указателями\n    const pivot = this.partition(nums, left, right);\n    // Рекурсивно обработать левый и правый подмассивы\n    this.quickSort(nums, left, pivot - 1);\n    this.quickSort(nums, pivot + 1, right);\n}\n
        quick_sort.dart
        /* Быстрая сортировка */\nvoid quickSort(List<int> nums, int left, int right) {\n  // Завершить рекурсию, когда длина подмассива равна 1\n  if (left >= right) return;\n  // Разбиение с опорными указателями\n  int pivot = _partition(nums, left, right);\n  // Рекурсивно обработать левый и правый подмассивы\n  quickSort(nums, left, pivot - 1);\n  quickSort(nums, pivot + 1, right);\n}\n
        quick_sort.rs
        /* Быстрая сортировка */\npub fn quick_sort(left: i32, right: i32, nums: &mut [i32]) {\n    // Завершить рекурсию, когда длина подмассива равна 1\n    if left >= right {\n        return;\n    }\n    // Разбиение с опорными указателями\n    let pivot = Self::partition(nums, left as usize, right as usize) as i32;\n    // Рекурсивно обработать левый и правый подмассивы\n    Self::quick_sort(left, pivot - 1, nums);\n    Self::quick_sort(pivot + 1, right, nums);\n}\n
        quick_sort.c
        /* Быстрая сортировка */\nvoid quickSort(int nums[], int left, int right) {\n    // Завершить рекурсию, когда длина подмассива равна 1\n    if (left >= right) {\n        return;\n    }\n    // Разбиение с опорными указателями\n    int pivot = partition(nums, left, right);\n    // Рекурсивно обработать левый и правый подмассивы\n    quickSort(nums, left, pivot - 1);\n    quickSort(nums, pivot + 1, right);\n}\n
        quick_sort.kt
        /* Быстрая сортировка */\nfun quickSort(nums: IntArray, left: Int, right: Int) {\n    // Завершить рекурсию, когда длина подмассива равна 1\n    if (left >= right) return\n    // Разбиение с опорными указателями\n    val pivot = partition(nums, left, right)\n    // Рекурсивно обработать левый и правый подмассивы\n    quickSort(nums, left, pivot - 1)\n    quickSort(nums, pivot + 1, right)\n}\n
        quick_sort.rb
        ### Класс быстрой сортировки ###\ndef quick_sort(nums, left, right)\n  # Рекурсивно обрабатывать, пока длина подмассива не станет равной 1\n  if left < right\n    # Разбиение с опорными указателями\n    pivot = partition(nums, left, right)\n    # Рекурсивно обработать левый и правый подмассивы\n    quick_sort(nums, left, pivot - 1)\n    quick_sort(nums, pivot + 1, right)\n  end\n  nums\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 11. Сортировка","11.5   Быстрая сортировка"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1152","level":2,"title":"11.5.2   Характеристики алгоритма","text":"
        • Временная сложность равна \\(O(n \\log n)\\), алгоритм не является адаптивным: в среднем глубина рекурсии при разделении равна \\(\\log n\\) , а суммарное число циклов на каждом уровне равно \\(n\\) , поэтому общая сложность составляет \\(O(n \\log n)\\) . В худшем случае каждое разделение делит массив длины \\(n\\) на подмассивы длины \\(0\\) и \\(n - 1\\) ; тогда глубина рекурсии достигает \\(n\\) , на каждом уровне выполняется \\(n\\) операций, и общая временная сложность вырождается в \\(O(n^2)\\) .
        • Пространственная сложность равна \\(O(n)\\), сортировка выполняется на месте: если входной массив полностью отсортирован в обратном порядке, глубина рекурсии достигает худшего случая \\(n\\) , что требует \\(O(n)\\) памяти под стек вызовов. При этом сама сортировка выполняется в исходном массиве без дополнительного массива.
        • Нестабильная сортировка: на последнем шаге разделения опорный элемент может быть обменян вправо от равного ему элемента.
        ","path":["Глава 11. Сортировка","11.5   Быстрая сортировка"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1153","level":2,"title":"11.5.3   Почему быстрая сортировка быстрая","text":"

        Уже по названию понятно, что быстрая сортировка должна иметь преимущества по эффективности. Хотя ее средняя временная сложность совпадает со сложностью \"сортировки слиянием\" и \"пирамидальной сортировки\", на практике быстрая сортировка обычно работает быстрее. Основные причины таковы.

        • Вероятность худшего случая очень мала: хотя худшая временная сложность быстрой сортировки равна \\(O(n^2)\\) и она не так стабильна, как сортировка слиянием, в подавляющем большинстве случаев она работает за \\(O(n \\log n)\\) .
        • Высокая эффективность использования кэша: при выполнении разделения система может загрузить весь подмассив в кэш, поэтому доступ к элементам оказывается быстрым. Алгоритмы вроде \"пирамидальной сортировки\" требуют скачкообразного доступа к элементам и таким свойством не обладают.
        • Небольшой константный множитель в сложности: среди трех перечисленных алгоритмов у быстрой сортировки обычно меньше всего сравнений, присваиваний и обменов. Это похоже на причину, по которой \"сортировка вставками\" часто быстрее \"сортировки пузырьком\".
        ","path":["Глава 11. Сортировка","11.5   Быстрая сортировка"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1154","level":2,"title":"11.5.4   Оптимизация выбора опорного элемента","text":"

        На некоторых входных данных временная эффективность быстрой сортировки может ухудшаться. Рассмотрим крайний случай: входной массив полностью отсортирован в обратном порядке. Поскольку в качестве опорного мы выбираем самый левый элемент, после разделения он будет обменян в самый правый конец массива, из-за чего длина левого подмассива станет \\(n - 1\\) , а длина правого - \\(0\\) . Если рекурсия будет продолжаться таким образом, то после каждого разделения один из подмассивов будет иметь длину \\(0\\) , стратегия \"разделяй и властвуй\" потеряет смысл, а быстрая сортировка выродится в нечто близкое к \"сортировке пузырьком\".

        Чтобы по возможности избежать такого сценария, можно улучшить стратегию выбора опорного элемента в процедуре разделения. Например, можно выбирать случайный элемент массива как опорный. Однако если не повезет и каждый раз будет выбираться неудачный опорный элемент, производительность все равно останется неудовлетворительной.

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

        Чтобы улучшить ситуацию, можно взять три кандидата (обычно первый, последний и средний элементы массива) и использовать медиану этих трех значений как опорный элемент. Благодаря этому вероятность того, что опорный элемент окажется \"не слишком маленьким и не слишком большим\", заметно возрастает. Конечно, можно брать и большее число кандидатов, чтобы еще сильнее повысить устойчивость алгоритма. После этого вероятность деградации временной сложности до \\(O(n^2)\\) существенно уменьшается.

        Пример кода:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py
        def median_three(self, nums: list[int], left: int, mid: int, right: int) -> int:\n    \"\"\"Выбрать медиану из трех кандидатов\"\"\"\n    l, m, r = nums[left], nums[mid], nums[right]\n    if (l <= m <= r) or (r <= m <= l):\n        return mid  # m находится между l и r\n    if (m <= l <= r) or (r <= l <= m):\n        return left  # l находится между m и r\n    return right\n\ndef partition(self, nums: list[int], left: int, right: int) -> int:\n    \"\"\"Разбиение с опорными указателями (медиана трех)\"\"\"\n    # Взять nums[left] в качестве опорного элемента\n    med = self.median_three(nums, left, (left + right) // 2, right)\n    # Переместить медиану в крайний левый элемент массива\n    nums[left], nums[med] = nums[med], nums[left]\n    # Взять nums[left] в качестве опорного элемента\n    i, j = left, right\n    while i < j:\n        while i < j and nums[j] >= nums[left]:\n            j -= 1  # Идти справа налево в поисках первого элемента меньше опорного\n        while i < j and nums[i] <= nums[left]:\n            i += 1  # Идти слева направо в поисках первого элемента больше опорного\n        # Обмен элементов\n        nums[i], nums[j] = nums[j], nums[i]\n    # Переместить опорный элемент на границу двух подмассивов\n    nums[i], nums[left] = nums[left], nums[i]\n    return i  # Вернуть индекс опорного элемента\n
        quick_sort.cpp
        /* Выбрать медиану из трех кандидатов */\nint medianThree(vector<int> &nums, int left, int mid, int right) {\n    int l = nums[left], m = nums[mid], r = nums[right];\n    if ((l <= m && m <= r) || (r <= m && m <= l))\n        return mid; // m находится между l и r\n    if ((m <= l && l <= r) || (r <= l && l <= m))\n        return left; // l находится между m и r\n    return right;\n}\n\n/* Разбиение с опорными указателями (медиана трех) */\nint partition(vector<int> &nums, int left, int right) {\n    // Выбрать медиану из трех кандидатов\n    int med = medianThree(nums, left, (left + right) / 2, right);\n    // Переместить медиану в крайний левый элемент массива\n    swap(nums[left], nums[med]);\n    // Взять nums[left] в качестве опорного элемента\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;                // Идти справа налево в поисках первого элемента меньше опорного\n        while (i < j && nums[i] <= nums[left])\n            i++;                // Идти слева направо в поисках первого элемента больше опорного\n        swap(nums[i], nums[j]); // Поменять эти два элемента местами\n    }\n    swap(nums[i], nums[left]);  // Переместить опорный элемент на границу двух подмассивов\n    return i;                   // Вернуть индекс опорного элемента\n}\n
        quick_sort.java
        /* Выбрать медиану из трех кандидатов */\nint medianThree(int[] nums, int left, int mid, int right) {\n    int l = nums[left], m = nums[mid], r = nums[right];\n    if ((l <= m && m <= r) || (r <= m && m <= l))\n        return mid; // m находится между l и r\n    if ((m <= l && l <= r) || (r <= l && l <= m))\n        return left; // l находится между m и r\n    return right;\n}\n\n/* Разбиение с опорными указателями (медиана трех) */\nint partition(int[] nums, int left, int right) {\n    // Выбрать медиану из трех кандидатов\n    int med = medianThree(nums, left, (left + right) / 2, right);\n    // Переместить медиану в крайний левый элемент массива\n    swap(nums, left, med);\n    // Взять nums[left] в качестве опорного элемента\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;          // Идти справа налево в поисках первого элемента меньше опорного\n        while (i < j && nums[i] <= nums[left])\n            i++;          // Идти слева направо в поисках первого элемента больше опорного\n        swap(nums, i, j); // Поменять эти два элемента местами\n    }\n    swap(nums, i, left);  // Переместить опорный элемент на границу двух подмассивов\n    return i;             // Вернуть индекс опорного элемента\n}\n
        quick_sort.cs
        /* Выбрать медиану из трех кандидатов */\nint MedianThree(int[] nums, int left, int mid, int right) {\n    int l = nums[left], m = nums[mid], r = nums[right];\n    if ((l <= m && m <= r) || (r <= m && m <= l))\n        return mid; // m находится между l и r\n    if ((m <= l && l <= r) || (r <= l && l <= m))\n        return left; // l находится между m и r\n    return right;\n}\n\n/* Разбиение с опорными указателями (медиана трех) */\nint Partition(int[] nums, int left, int right) {\n    // Выбрать медиану из трех кандидатов\n    int med = MedianThree(nums, left, (left + right) / 2, right);\n    // Переместить медиану в крайний левый элемент массива\n    Swap(nums, left, med);\n    // Взять nums[left] в качестве опорного элемента\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;          // Идти справа налево в поисках первого элемента меньше опорного\n        while (i < j && nums[i] <= nums[left])\n            i++;          // Идти слева направо в поисках первого элемента больше опорного\n        Swap(nums, i, j); // Поменять эти два элемента местами\n    }\n    Swap(nums, i, left);  // Переместить опорный элемент на границу двух подмассивов\n    return i;             // Вернуть индекс опорного элемента\n}\n
        quick_sort.go
        /* Выбрать медиану из трех кандидатов */\nfunc (q *quickSortMedian) medianThree(nums []int, left, mid, right int) int {\n    l, m, r := nums[left], nums[mid], nums[right]\n    if (l <= m && m <= r) || (r <= m && m <= l) {\n        return mid // m находится между l и r\n    }\n    if (m <= l && l <= r) || (r <= l && l <= m) {\n        return left // l находится между m и r\n    }\n    return right\n}\n\n/* Разбиение с опорными указателями (медиана трех) */\nfunc (q *quickSortMedian) partition(nums []int, left, right int) int {\n    // Взять nums[left] в качестве опорного элемента\n    med := q.medianThree(nums, left, (left+right)/2, right)\n    // Переместить медиану в крайний левый элемент массива\n    nums[left], nums[med] = nums[med], nums[left]\n    // Взять nums[left] в качестве опорного элемента\n    i, j := left, right\n    for i < j {\n        for i < j && nums[j] >= nums[left] {\n            j-- // Идти справа налево в поисках первого элемента меньше опорного\n        }\n        for i < j && nums[i] <= nums[left] {\n            i++ // Идти слева направо в поисках первого элемента больше опорного\n        }\n        // Обмен элементов\n        nums[i], nums[j] = nums[j], nums[i]\n    }\n    // Переместить опорный элемент на границу двух подмассивов\n    nums[i], nums[left] = nums[left], nums[i]\n    return i // Вернуть индекс опорного элемента\n}\n
        quick_sort.swift
        /* Выбрать медиану из трех кандидатов */\nfunc medianThree(nums: [Int], left: Int, mid: Int, right: Int) -> Int {\n    let l = nums[left]\n    let m = nums[mid]\n    let r = nums[right]\n    if (l <= m && m <= r) || (r <= m && m <= l) {\n        return mid // m находится между l и r\n    }\n    if (m <= l && l <= r) || (r <= l && l <= m) {\n        return left // l находится между m и r\n    }\n    return right\n}\n\n/* Разбиение с опорными указателями (медиана трех) */\nfunc partitionMedian(nums: inout [Int], left: Int, right: Int) -> Int {\n    // Выбрать медиану из трех кандидатов\n    let med = medianThree(nums: nums, left: left, mid: left + (right - left) / 2, right: right)\n    // Переместить медиану в крайний левый элемент массива\n    nums.swapAt(left, med)\n    return partition(nums: &nums, left: left, right: right)\n}\n
        quick_sort.js
        /* Выбрать медиану из трех кандидатов */\nmedianThree(nums, left, mid, right) {\n    let l = nums[left],\n        m = nums[mid],\n        r = nums[right];\n    // m находится между l и r\n    if ((l <= m && m <= r) || (r <= m && m <= l)) return mid;\n    // l находится между m и r\n    if ((m <= l && l <= r) || (r <= l && l <= m)) return left;\n    return right;\n}\n\n/* Разбиение с опорными указателями (медиана трех) */\npartition(nums, left, right) {\n    // Выбрать медиану из трех кандидатов\n    let med = this.medianThree(\n        nums,\n        left,\n        Math.floor((left + right) / 2),\n        right\n    );\n    // Переместить медиану в крайний левый элемент массива\n    this.swap(nums, left, med);\n    // Взять nums[left] в качестве опорного элемента\n    let i = left,\n        j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) j--; // Идти справа налево в поисках первого элемента меньше опорного\n        while (i < j && nums[i] <= nums[left]) i++; // Идти слева направо в поисках первого элемента больше опорного\n        this.swap(nums, i, j); // Поменять эти два элемента местами\n    }\n    this.swap(nums, i, left); // Переместить опорный элемент на границу двух подмассивов\n    return i; // Вернуть индекс опорного элемента\n}\n
        quick_sort.ts
        /* Выбрать медиану из трех кандидатов */\nmedianThree(\n    nums: number[],\n    left: number,\n    mid: number,\n    right: number\n): number {\n    let l = nums[left],\n        m = nums[mid],\n        r = nums[right];\n    // m находится между l и r\n    if ((l <= m && m <= r) || (r <= m && m <= l)) return mid;\n    // l находится между m и r\n    if ((m <= l && l <= r) || (r <= l && l <= m)) return left;\n    return right;\n}\n\n/* Разбиение с опорными указателями (медиана трех) */\npartition(nums: number[], left: number, right: number): number {\n    // Выбрать медиану из трех кандидатов\n    let med = this.medianThree(\n        nums,\n        left,\n        Math.floor((left + right) / 2),\n        right\n    );\n    // Переместить медиану в крайний левый элемент массива\n    this.swap(nums, left, med);\n    // Взять nums[left] в качестве опорного элемента\n    let i = left,\n        j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) {\n            j--; // Идти справа налево в поисках первого элемента меньше опорного\n        }\n        while (i < j && nums[i] <= nums[left]) {\n            i++; // Идти слева направо в поисках первого элемента больше опорного\n        }\n        this.swap(nums, i, j); // Поменять эти два элемента местами\n    }\n    this.swap(nums, i, left); // Переместить опорный элемент на границу двух подмассивов\n    return i; // Вернуть индекс опорного элемента\n}\n
        quick_sort.dart
        /* Выбрать медиану из трех кандидатов */\nint _medianThree(List<int> nums, int left, int mid, int right) {\n  int l = nums[left], m = nums[mid], r = nums[right];\n  if ((l <= m && m <= r) || (r <= m && m <= l))\n    return mid; // m находится между l и r\n  if ((m <= l && l <= r) || (r <= l && l <= m))\n    return left; // l находится между m и r\n  return right;\n}\n\n/* Разбиение с опорными указателями (медиана трех) */\nint _partition(List<int> nums, int left, int right) {\n  // Выбрать медиану из трех кандидатов\n  int med = _medianThree(nums, left, (left + right) ~/ 2, right);\n  // Переместить медиану в крайний левый элемент массива\n  _swap(nums, left, med);\n  // Взять nums[left] в качестве опорного элемента\n  int i = left, j = right;\n  while (i < j) {\n    while (i < j && nums[j] >= nums[left]) j--; // Идти справа налево в поисках первого элемента меньше опорного\n    while (i < j && nums[i] <= nums[left]) i++; // Идти слева направо в поисках первого элемента больше опорного\n    _swap(nums, i, j); // Поменять эти два элемента местами\n  }\n  _swap(nums, i, left); // Переместить опорный элемент на границу двух подмассивов\n  return i; // Вернуть индекс опорного элемента\n}\n
        quick_sort.rs
        /* Выбрать медиану из трех кандидатов */\nfn median_three(nums: &mut [i32], left: usize, mid: usize, right: usize) -> usize {\n    let (l, m, r) = (nums[left], nums[mid], nums[right]);\n    if (l <= m && m <= r) || (r <= m && m <= l) {\n        return mid; // m находится между l и r\n    }\n    if (m <= l && l <= r) || (r <= l && l <= m) {\n        return left; // l находится между m и r\n    }\n    right\n}\n\n/* Разбиение с опорными указателями (медиана трех) */\nfn partition(nums: &mut [i32], left: usize, right: usize) -> usize {\n    // Выбрать медиану из трех кандидатов\n    let med = Self::median_three(nums, left, (left + right) / 2, right);\n    // Переместить медиану в крайний левый элемент массива\n    nums.swap(left, med);\n    // Взять nums[left] в качестве опорного элемента\n    let (mut i, mut j) = (left, right);\n    while i < j {\n        while i < j && nums[j] >= nums[left] {\n            j -= 1; // Идти справа налево в поисках первого элемента меньше опорного\n        }\n        while i < j && nums[i] <= nums[left] {\n            i += 1; // Идти слева направо в поисках первого элемента больше опорного\n        }\n        nums.swap(i, j); // Поменять эти два элемента местами\n    }\n    nums.swap(i, left); // Переместить опорный элемент на границу двух подмассивов\n    i // Вернуть индекс опорного элемента\n}\n
        quick_sort.c
        /* Выбрать медиану из трех кандидатов */\nint medianThree(int nums[], int left, int mid, int right) {\n    int l = nums[left], m = nums[mid], r = nums[right];\n    if ((l <= m && m <= r) || (r <= m && m <= l))\n        return mid; // m находится между l и r\n    if ((m <= l && l <= r) || (r <= l && l <= m))\n        return left; // l находится между m и r\n    return right;\n}\n\n/* Разбиение с опорными указателями (медиана трех) */\nint partitionMedian(int nums[], int left, int right) {\n    // Выбрать медиану из трех кандидатов\n    int med = medianThree(nums, left, (left + right) / 2, right);\n    // Переместить медиану в крайний левый элемент массива\n    swap(nums, left, med);\n    // Взять nums[left] в качестве опорного элемента\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--; // Идти справа налево в поисках первого элемента меньше опорного\n        while (i < j && nums[i] <= nums[left])\n            i++;          // Идти слева направо в поисках первого элемента больше опорного\n        swap(nums, i, j); // Поменять эти два элемента местами\n    }\n    swap(nums, i, left); // Переместить опорный элемент на границу двух подмассивов\n    return i;            // Вернуть индекс опорного элемента\n}\n
        quick_sort.kt
        /* Выбрать медиану из трех кандидатов */\nfun medianThree(nums: IntArray, left: Int, mid: Int, right: Int): Int {\n    val l = nums[left]\n    val m = nums[mid]\n    val r = nums[right]\n    if ((m in l..r) || (m in r..l))\n        return mid  // m находится между l и r\n    if ((l in m..r) || (l in r..m))\n        return left // l находится между m и r\n    return right\n}\n\n/* Разбиение с опорными указателями (медиана трех) */\nfun partitionMedian(nums: IntArray, left: Int, right: Int): Int {\n    // Выбрать медиану из трех кандидатов\n    val med = medianThree(nums, left, (left + right) / 2, right)\n    // Переместить медиану в крайний левый элемент массива\n    swap(nums, left, med)\n    // Взять nums[left] в качестве опорного элемента\n    var i = left\n    var j = right\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--                      // Идти справа налево в поисках первого элемента меньше опорного\n        while (i < j && nums[i] <= nums[left])\n            i++                      // Идти слева направо в поисках первого элемента больше опорного\n        swap(nums, i, j)             // Поменять эти два элемента местами\n    }\n    swap(nums, i, left)              // Переместить опорный элемент на границу двух подмассивов\n    return i                         // Вернуть индекс опорного элемента\n}\n
        quick_sort.rb
        ### Выбрать медиану из трех кандидатов ###\ndef median_three(nums, left, mid, right)\n  # Выбрать медиану из трех кандидатов\n  _l, _m, _r = nums[left], nums[mid], nums[right]\n  # m находится между l и r\n  return mid if (_l <= _m && _m <= _r) || (_r <= _m && _m <= _l)\n  # l находится между m и r\n  return left if (_m <= _l && _l <= _r) || (_r <= _l && _l <= _m)\n  return right\nend\n\n### Выбрать медиану из трех кандидатов ###\ndef median_three(nums, left, mid, right)\n  # Выбрать медиану из трех кандидатов\n  _l, _m, _r = nums[left], nums[mid], nums[right]\n  # m находится между l и r\n  return mid if (_l <= _m && _m <= _r) || (_r <= _m && _m <= _l)\n  # l находится между m и r\n  return left if (_m <= _l && _l <= _r) || (_r <= _l && _l <= _m)\n  return right\nend\n\n# ## Разбиение с опорными указателями (медиана трех) ###\ndef partition(nums, left, right)\n  # ## Использовать nums[left] как опорный элемент\n  med = median_three(nums, left, (left + right) / 2, right)\n  # Переместить медиану в крайний левый элемент массива\n  nums[left], nums[med] = nums[med], nums[left]\n  i, j = left, right\n  while i < j\n    while i < j && nums[j] >= nums[left]\n      j -= 1 # Идти справа налево в поисках первого элемента меньше опорного\n    end\n    while i < j && nums[i] <= nums[left]\n      i += 1 # Идти слева направо в поисках первого элемента больше опорного\n    end\n    # Обмен элементов\n    nums[i], nums[j] = nums[j], nums[i]\n  end\n  # Переместить опорный элемент на границу двух подмассивов\n  nums[i], nums[left] = nums[left], nums[i]\n  i # Вернуть индекс опорного элемента\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 11. Сортировка","11.5   Быстрая сортировка"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1155","level":2,"title":"11.5.5   Оптимизация глубины рекурсии","text":"

        На некоторых входных данных быстрая сортировка может занимать слишком много памяти. Рассмотрим полностью отсортированный входной массив. Пусть длина текущего подмассива в рекурсии равна \\(m\\) ; тогда после каждого разделения будут получаться левый подмассив длины \\(0\\) и правый подмассив длины \\(m - 1\\) . Это означает, что на каждом уровне размер задачи уменьшается совсем немного (лишь на один элемент), а высота дерева рекурсии достигает \\(n - 1\\) , поэтому требуется \\(O(n)\\) памяти под стек вызовов.

        Чтобы избежать накопления стековых кадров, после каждого разделения можно сравнивать длины двух подмассивов и рекурсивно обрабатывать только более короткий из них. Поскольку длина короткого подмассива не превысит \\(n / 2\\) , такой подход гарантирует, что глубина рекурсии не превысит \\(\\log n\\) , а худшая пространственная сложность будет оптимизирована до \\(O(\\log n)\\) . Код приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py
        def quick_sort(self, nums: list[int], left: int, right: int):\n    \"\"\"Быстрая сортировка (оптимизация глубины рекурсии)\"\"\"\n    # Завершить, когда длина подмассива равна 1\n    while left < right:\n        # Операция разбиения с опорными указателями\n        pivot = self.partition(nums, left, right)\n        # Выполнить быструю сортировку для более короткого из двух подмассивов\n        if pivot - left < right - pivot:\n            self.quick_sort(nums, left, pivot - 1)  # Рекурсивно отсортировать левый подмассив\n            left = pivot + 1  # Оставшийся неотсортированный диапазон: [pivot + 1, right]\n        else:\n            self.quick_sort(nums, pivot + 1, right)  # Рекурсивно отсортировать правый подмассив\n            right = pivot - 1  # Оставшийся неотсортированный диапазон: [left, pivot - 1]\n
        quick_sort.cpp
        /* Быстрая сортировка (оптимизация глубины рекурсии) */\nvoid quickSort(vector<int> &nums, int left, int right) {\n    // Завершить, когда длина подмассива равна 1\n    while (left < right) {\n        // Операция разбиения с опорными указателями\n        int pivot = partition(nums, left, right);\n        // Выполнить быструю сортировку для более короткого из двух подмассивов\n        if (pivot - left < right - pivot) {\n            quickSort(nums, left, pivot - 1); // Рекурсивно отсортировать левый подмассив\n            left = pivot + 1;                 // Оставшийся неотсортированный диапазон: [pivot + 1, right]\n        } else {\n            quickSort(nums, pivot + 1, right); // Рекурсивно отсортировать правый подмассив\n            right = pivot - 1;                 // Оставшийся неотсортированный диапазон: [left, pivot - 1]\n        }\n    }\n}\n
        quick_sort.java
        /* Быстрая сортировка (оптимизация глубины рекурсии) */\nvoid quickSort(int[] nums, int left, int right) {\n    // Завершить, когда длина подмассива равна 1\n    while (left < right) {\n        // Операция разбиения с опорными указателями\n        int pivot = partition(nums, left, right);\n        // Выполнить быструю сортировку для более короткого из двух подмассивов\n        if (pivot - left < right - pivot) {\n            quickSort(nums, left, pivot - 1); // Рекурсивно отсортировать левый подмассив\n            left = pivot + 1; // Оставшийся неотсортированный диапазон: [pivot + 1, right]\n        } else {\n            quickSort(nums, pivot + 1, right); // Рекурсивно отсортировать правый подмассив\n            right = pivot - 1; // Оставшийся неотсортированный диапазон: [left, pivot - 1]\n        }\n    }\n}\n
        quick_sort.cs
        /* Быстрая сортировка (оптимизация глубины рекурсии) */\nvoid QuickSort(int[] nums, int left, int right) {\n    // Завершить, когда длина подмассива равна 1\n    while (left < right) {\n        // Операция разбиения с опорными указателями\n        int pivot = Partition(nums, left, right);\n        // Выполнить быструю сортировку для более короткого из двух подмассивов\n        if (pivot - left < right - pivot) {\n            QuickSort(nums, left, pivot - 1);  // Рекурсивно отсортировать левый подмассив\n            left = pivot + 1;  // Оставшийся неотсортированный диапазон: [pivot + 1, right]\n        } else {\n            QuickSort(nums, pivot + 1, right); // Рекурсивно отсортировать правый подмассив\n            right = pivot - 1; // Оставшийся неотсортированный диапазон: [left, pivot - 1]\n        }\n    }\n}\n
        quick_sort.go
        /* Быстрая сортировка (оптимизация глубины рекурсии) */\nfunc (q *quickSortTailCall) quickSort(nums []int, left, right int) {\n    // Завершить, когда длина подмассива равна 1\n    for left < right {\n        // Операция разбиения с опорными указателями\n        pivot := q.partition(nums, left, right)\n        // Выполнить быструю сортировку для более короткого из двух подмассивов\n        if pivot-left < right-pivot {\n            q.quickSort(nums, left, pivot-1) // Рекурсивно отсортировать левый подмассив\n            left = pivot + 1                 // Оставшийся неотсортированный диапазон: [pivot + 1, right]\n        } else {\n            q.quickSort(nums, pivot+1, right) // Рекурсивно отсортировать правый подмассив\n            right = pivot - 1                 // Оставшийся неотсортированный диапазон: [left, pivot - 1]\n        }\n    }\n}\n
        quick_sort.swift
        /* Быстрая сортировка (оптимизация глубины рекурсии) */\nfunc quickSortTailCall(nums: inout [Int], left: Int, right: Int) {\n    var left = left\n    var right = right\n    // Завершить, когда длина подмассива равна 1\n    while left < right {\n        // Операция разбиения с опорными указателями\n        let pivot = partition(nums: &nums, left: left, right: right)\n        // Выполнить быструю сортировку для более короткого из двух подмассивов\n        if (pivot - left) < (right - pivot) {\n            quickSortTailCall(nums: &nums, left: left, right: pivot - 1) // Рекурсивно отсортировать левый подмассив\n            left = pivot + 1 // Оставшийся неотсортированный диапазон: [pivot + 1, right]\n        } else {\n            quickSortTailCall(nums: &nums, left: pivot + 1, right: right) // Рекурсивно отсортировать правый подмассив\n            right = pivot - 1 // Оставшийся неотсортированный диапазон: [left, pivot - 1]\n        }\n    }\n}\n
        quick_sort.js
        /* Быстрая сортировка (оптимизация глубины рекурсии) */\nquickSort(nums, left, right) {\n    // Завершить, когда длина подмассива равна 1\n    while (left < right) {\n        // Операция разбиения с опорными указателями\n        let pivot = this.partition(nums, left, right);\n        // Выполнить быструю сортировку для более короткого из двух подмассивов\n        if (pivot - left < right - pivot) {\n            this.quickSort(nums, left, pivot - 1); // Рекурсивно отсортировать левый подмассив\n            left = pivot + 1; // Оставшийся неотсортированный диапазон: [pivot + 1, right]\n        } else {\n            this.quickSort(nums, pivot + 1, right); // Рекурсивно отсортировать правый подмассив\n            right = pivot - 1; // Оставшийся неотсортированный диапазон: [left, pivot - 1]\n        }\n    }\n}\n
        quick_sort.ts
        /* Быстрая сортировка (оптимизация глубины рекурсии) */\nquickSort(nums: number[], left: number, right: number): void {\n    // Завершить, когда длина подмассива равна 1\n    while (left < right) {\n        // Операция разбиения с опорными указателями\n        let pivot = this.partition(nums, left, right);\n        // Выполнить быструю сортировку для более короткого из двух подмассивов\n        if (pivot - left < right - pivot) {\n            this.quickSort(nums, left, pivot - 1); // Рекурсивно отсортировать левый подмассив\n            left = pivot + 1; // Оставшийся неотсортированный диапазон: [pivot + 1, right]\n        } else {\n            this.quickSort(nums, pivot + 1, right); // Рекурсивно отсортировать правый подмассив\n            right = pivot - 1; // Оставшийся неотсортированный диапазон: [left, pivot - 1]\n        }\n    }\n}\n
        quick_sort.dart
        /* Быстрая сортировка (оптимизация глубины рекурсии) */\nvoid quickSort(List<int> nums, int left, int right) {\n  // Завершить, когда длина подмассива равна 1\n  while (left < right) {\n    // Операция разбиения с опорными указателями\n    int pivot = _partition(nums, left, right);\n    // Выполнить быструю сортировку для более короткого из двух подмассивов\n    if (pivot - left < right - pivot) {\n      quickSort(nums, left, pivot - 1); // Рекурсивно отсортировать левый подмассив\n      left = pivot + 1; // Оставшийся неотсортированный диапазон: [pivot + 1, right]\n    } else {\n      quickSort(nums, pivot + 1, right); // Рекурсивно отсортировать правый подмассив\n      right = pivot - 1; // Оставшийся неотсортированный диапазон: [left, pivot - 1]\n    }\n  }\n}\n
        quick_sort.rs
        /* Быстрая сортировка (оптимизация глубины рекурсии) */\npub fn quick_sort(mut left: i32, mut right: i32, nums: &mut [i32]) {\n    // Завершить, когда длина подмассива равна 1\n    while left < right {\n        // Операция разбиения с опорными указателями\n        let pivot = Self::partition(nums, left as usize, right as usize) as i32;\n        // Выполнить быструю сортировку для более короткого из двух подмассивов\n        if pivot - left < right - pivot {\n            Self::quick_sort(left, pivot - 1, nums); // Рекурсивно отсортировать левый подмассив\n            left = pivot + 1; // Оставшийся неотсортированный диапазон: [pivot + 1, right]\n        } else {\n            Self::quick_sort(pivot + 1, right, nums); // Рекурсивно отсортировать правый подмассив\n            right = pivot - 1; // Оставшийся неотсортированный диапазон: [left, pivot - 1]\n        }\n    }\n}\n
        quick_sort.c
        /* Быстрая сортировка (оптимизация глубины рекурсии) */\nvoid quickSortTailCall(int nums[], int left, int right) {\n    // Завершить, когда длина подмассива равна 1\n    while (left < right) {\n        // Операция разбиения с опорными указателями\n        int pivot = partition(nums, left, right);\n        // Выполнить быструю сортировку для более короткого из двух подмассивов\n        if (pivot - left < right - pivot) {\n            // Рекурсивно отсортировать левый подмассив\n            quickSortTailCall(nums, left, pivot - 1);\n            // Оставшийся неотсортированный диапазон: [pivot + 1, right]\n            left = pivot + 1;\n        } else {\n            // Рекурсивно отсортировать правый подмассив\n            quickSortTailCall(nums, pivot + 1, right);\n            // Оставшийся неотсортированный диапазон: [left, pivot - 1]\n            right = pivot - 1;\n        }\n    }\n}\n
        quick_sort.kt
        /* Быстрая сортировка (оптимизация глубины рекурсии) */\nfun quickSortTailCall(nums: IntArray, left: Int, right: Int) {\n    // Завершить, когда длина подмассива равна 1\n    var l = left\n    var r = right\n    while (l < r) {\n        // Операция разбиения с опорными указателями\n        val pivot = partition(nums, l, r)\n        // Выполнить быструю сортировку для более короткого из двух подмассивов\n        if (pivot - l < r - pivot) {\n            quickSort(nums, l, pivot - 1) // Рекурсивно отсортировать левый подмассив\n            l = pivot + 1 // Оставшийся неотсортированный диапазон: [pivot + 1, right]\n        } else {\n            quickSort(nums, pivot + 1, r) // Рекурсивно отсортировать правый подмассив\n            r = pivot - 1 // Оставшийся неотсортированный диапазон: [left, pivot - 1]\n        }\n    }\n}\n
        quick_sort.rb
        ### Разбиение с опорными указателями ###\ndef partition(nums, left, right)\n  # Использовать nums[left] как опорный элемент\n  i = left\n  j = right\n  while i < j\n    while i < j && nums[j] >= nums[left]\n      j -= 1 # Идти справа налево в поисках первого элемента меньше опорного\n    end\n    while i < j && nums[i] <= nums[left]\n      i += 1 # Идти слева направо в поисках первого элемента больше опорного\n    end\n    # Обмен элементов\n    nums[i], nums[j] = nums[j], nums[i]\n  end\n  # Переместить опорный элемент на границу двух подмассивов\n  nums[i], nums[left] = nums[left], nums[i]\n  i # Вернуть индекс опорного элемента\nend\n\n# ## Быстрая сортировка (оптимизация глубины рекурсии) ###\ndef quick_sort(nums, left, right)\n  # Рекурсивно обрабатывать, пока длина подмассива не станет равной 1\n  while left < right\n    # Разбиение с опорными указателями\n    pivot = partition(nums, left, right)\n    # Выполнить быструю сортировку для более короткого из двух подмассивов\n    if pivot - left < right - pivot\n      quick_sort(nums, left, pivot - 1)\n      left = pivot + 1 # Оставшийся неотсортированный диапазон: [pivot + 1, right]\n    else\n      quick_sort(nums, pivot + 1, right)\n      right = pivot - 1 # Оставшийся неотсортированный диапазон: [left, pivot - 1]\n    end\n  end\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 11. Сортировка","11.5   Быстрая сортировка"],"tags":[]},{"location":"chapter_sorting/radix_sort/","level":1,"title":"11.10   Поразрядная сортировка","text":"

        В предыдущем разделе была рассмотрена сортировка подсчетом: она хорошо подходит для случаев, когда объем данных \\(n\\) велик, а диапазон значений \\(m\\) сравнительно мал. Предположим теперь, что нужно отсортировать \\(n = 10^6\\) номеров студентов, причем каждый номер представляет собой \\(8\\)-значное число. Тогда диапазон данных \\(m = 10^8\\) оказывается очень большим; сортировка подсчетом потребует огромного объема памяти, а поразрядная сортировка позволяет этого избежать.

        Поразрядная сортировка (radix sort) по своей основной идее совпадает с сортировкой подсчетом и тоже реализует сортировку через подсчет количества. При этом поразрядная сортировка использует соотношение между разрядами числа и последовательно сортирует данные по каждому разряду, получая итоговый упорядоченный результат.

        ","path":["Глава 11. Сортировка","11.10   Поразрядная сортировка"],"tags":[]},{"location":"chapter_sorting/radix_sort/#11101","level":2,"title":"11.10.1   Алгоритм","text":"

        Рассмотрим пример со студенческими номерами: будем считать, что младший разряд имеет номер \\(1\\) , а старший - номер \\(8\\) . Тогда процесс поразрядной сортировки показан на рисунке 11-18.

        1. Инициализировать номер разряда \\(k = 1\\) .
        2. Выполнить \"сортировку подсчетом\" по \\(k\\)-му разряду студенческого номера. После этого данные будут упорядочены по \\(k\\)-му разряду по возрастанию.
        3. Увеличить \\(k\\) на \\(1\\) и вернуться к шагу 2. , продолжая процесс, пока сортировка не будет выполнена для всех разрядов.

        Рисунок 11-18   Процесс поразрядной сортировки

        Ниже разберем реализацию кода. Для числа \\(x\\) в системе счисления с основанием \\(d\\) получить его \\(k\\)-й разряд \\(x_k\\) можно по формуле:

        \\[ x_k = \\lfloor\\frac{x}{d^{k-1}}\\rfloor \\bmod d \\]

        где \\(\\lfloor a \\rfloor\\) обозначает округление числа \\(a\\) вниз, а \\(\\bmod \\: d\\) означает взятие остатка по модулю \\(d\\) . Для студенческих номеров выполняется \\(d = 10\\) и \\(k \\in [1, 8]\\) .

        Кроме того, нам нужно слегка изменить код сортировки подсчетом, чтобы он мог сортировать числа по их \\(k\\)-му разряду:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby radix_sort.py
        def digit(num: int, exp: int) -> int:\n    \"\"\"Получить k-й разряд элемента num, где exp = 10^(k-1)\"\"\"\n    # Передача exp вместо k позволяет избежать повторного дорогостоящего вычисления степени\n    return (num // exp) % 10\n\ndef counting_sort_digit(nums: list[int], exp: int):\n    \"\"\"Сортировка подсчетом (сортировка по k-му разряду nums)\"\"\"\n    # Разряды десятичной системы лежат в диапазоне 0~9, поэтому нужен массив корзин длины 10\n    counter = [0] * 10\n    n = len(nums)\n    # Подсчитать число появлений каждой цифры от 0 до 9\n    for i in range(n):\n        d = digit(nums[i], exp)  # Получить k-й разряд nums[i], обозначив его как d\n        counter[d] += 1  # Подсчитать число появлений цифры d\n    # Вычислить префиксные суммы и преобразовать «число появлений» в «индекс массива»\n    for i in range(1, 10):\n        counter[i] += counter[i - 1]\n    # Выполняя обратный проход, заполнить res элементами по статистике в корзинах\n    res = [0] * n\n    for i in range(n - 1, -1, -1):\n        d = digit(nums[i], exp)\n        j = counter[d] - 1  # Получить индекс j цифры d в массиве\n        res[j] = nums[i]  # Поместить текущий элемент по индексу j\n        counter[d] -= 1  # Уменьшить количество d на 1\n    # Перезаписать исходный массив nums результатом\n    for i in range(n):\n        nums[i] = res[i]\n\ndef radix_sort(nums: list[int]):\n    \"\"\"Поразрядная сортировка\"\"\"\n    # Получить максимальный элемент массива, чтобы определить максимальное число разрядов\n    m = max(nums)\n    # Проходить разряды от младшего к старшему\n    exp = 1\n    while exp <= m:\n        # Выполнить сортировку подсчетом по k-му разряду элементов массива\n        # k = 1 -> exp = 1\n        # k = 2 -> exp = 10\n        # то есть exp = 10^(k-1)\n        counting_sort_digit(nums, exp)\n        exp *= 10\n
        radix_sort.cpp
        /* Получить k-й разряд элемента num, где exp = 10^(k-1) */\nint digit(int num, int exp) {\n    // Передача exp вместо k позволяет избежать повторного дорогостоящего вычисления степени\n    return (num / exp) % 10;\n}\n\n/* Сортировка подсчетом (сортировка по k-му разряду nums) */\nvoid countingSortDigit(vector<int> &nums, int exp) {\n    // Разряды десятичной системы лежат в диапазоне 0~9, поэтому нужен массив корзин длины 10\n    vector<int> counter(10, 0);\n    int n = nums.size();\n    // Подсчитать число появлений каждой цифры от 0 до 9\n    for (int i = 0; i < n; i++) {\n        int d = digit(nums[i], exp); // Получить k-й разряд nums[i], обозначив его как d\n        counter[d]++;                // Подсчитать число появлений цифры d\n    }\n    // Вычислить префиксные суммы и преобразовать «число появлений» в «индекс массива»\n    for (int i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // Выполняя обратный проход, заполнить res элементами по статистике в корзинах\n    vector<int> res(n, 0);\n    for (int i = n - 1; i >= 0; i--) {\n        int d = digit(nums[i], exp);\n        int j = counter[d] - 1; // Получить индекс j цифры d в массиве\n        res[j] = nums[i];       // Поместить текущий элемент по индексу j\n        counter[d]--;           // Уменьшить количество d на 1\n    }\n    // Перезаписать исходный массив nums результатом\n    for (int i = 0; i < n; i++)\n        nums[i] = res[i];\n}\n\n/* Поразрядная сортировка */\nvoid radixSort(vector<int> &nums) {\n    // Получить максимальный элемент массива, чтобы определить максимальное число разрядов\n    int m = *max_element(nums.begin(), nums.end());\n    // Проходить разряды от младшего к старшему\n    for (int exp = 1; exp <= m; exp *= 10)\n        // Выполнить сортировку подсчетом по k-му разряду элементов массива\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // то есть exp = 10^(k-1)\n        countingSortDigit(nums, exp);\n}\n
        radix_sort.java
        /* Получить k-й разряд элемента num, где exp = 10^(k-1) */\nint digit(int num, int exp) {\n    // Передача exp вместо k позволяет избежать повторного дорогостоящего вычисления степени\n    return (num / exp) % 10;\n}\n\n/* Сортировка подсчетом (сортировка по k-му разряду nums) */\nvoid countingSortDigit(int[] nums, int exp) {\n    // Разряды десятичной системы лежат в диапазоне 0~9, поэтому нужен массив корзин длины 10\n    int[] counter = new int[10];\n    int n = nums.length;\n    // Подсчитать число появлений каждой цифры от 0 до 9\n    for (int i = 0; i < n; i++) {\n        int d = digit(nums[i], exp); // Получить k-й разряд nums[i], обозначив его как d\n        counter[d]++;                // Подсчитать число появлений цифры d\n    }\n    // Вычислить префиксные суммы и преобразовать «число появлений» в «индекс массива»\n    for (int i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // Выполняя обратный проход, заполнить res элементами по статистике в корзинах\n    int[] res = new int[n];\n    for (int i = n - 1; i >= 0; i--) {\n        int d = digit(nums[i], exp);\n        int j = counter[d] - 1; // Получить индекс j цифры d в массиве\n        res[j] = nums[i];       // Поместить текущий элемент по индексу j\n        counter[d]--;           // Уменьшить количество d на 1\n    }\n    // Перезаписать исходный массив nums результатом\n    for (int i = 0; i < n; i++)\n        nums[i] = res[i];\n}\n\n/* Поразрядная сортировка */\nvoid radixSort(int[] nums) {\n    // Получить максимальный элемент массива, чтобы определить максимальное число разрядов\n    int m = Integer.MIN_VALUE;\n    for (int num : nums)\n        if (num > m)\n            m = num;\n    // Проходить разряды от младшего к старшему\n    for (int exp = 1; exp <= m; exp *= 10) {\n        // Выполнить сортировку подсчетом по k-му разряду элементов массива\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // то есть exp = 10^(k-1)\n        countingSortDigit(nums, exp);\n    }\n}\n
        radix_sort.cs
        /* Получить k-й разряд элемента num, где exp = 10^(k-1) */\nint Digit(int num, int exp) {\n    // Передача exp вместо k позволяет избежать повторного дорогостоящего вычисления степени\n    return (num / exp) % 10;\n}\n\n/* Сортировка подсчетом (сортировка по k-му разряду nums) */\nvoid CountingSortDigit(int[] nums, int exp) {\n    // Разряды десятичной системы лежат в диапазоне 0~9, поэтому нужен массив корзин длины 10\n    int[] counter = new int[10];\n    int n = nums.Length;\n    // Подсчитать число появлений каждой цифры от 0 до 9\n    for (int i = 0; i < n; i++) {\n        int d = Digit(nums[i], exp); // Получить k-й разряд nums[i], обозначив его как d\n        counter[d]++;                // Подсчитать число появлений цифры d\n    }\n    // Вычислить префиксные суммы и преобразовать «число появлений» в «индекс массива»\n    for (int i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // Выполняя обратный проход, заполнить res элементами по статистике в корзинах\n    int[] res = new int[n];\n    for (int i = n - 1; i >= 0; i--) {\n        int d = Digit(nums[i], exp);\n        int j = counter[d] - 1; // Получить индекс j цифры d в массиве\n        res[j] = nums[i];       // Поместить текущий элемент по индексу j\n        counter[d]--;           // Уменьшить количество d на 1\n    }\n    // Перезаписать исходный массив nums результатом\n    for (int i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n\n/* Поразрядная сортировка */\nvoid RadixSort(int[] nums) {\n    // Получить максимальный элемент массива, чтобы определить максимальное число разрядов\n    int m = int.MinValue;\n    foreach (int num in nums) {\n        if (num > m) m = num;\n    }\n    // Проходить разряды от младшего к старшему\n    for (int exp = 1; exp <= m; exp *= 10) {\n        // Выполнить сортировку подсчетом по k-му разряду элементов массива\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // то есть exp = 10^(k-1)\n        CountingSortDigit(nums, exp);\n    }\n}\n
        radix_sort.go
        /* Получить k-й разряд элемента num, где exp = 10^(k-1) */\nfunc digit(num, exp int) int {\n    // Передача exp вместо k позволяет избежать повторного дорогостоящего вычисления степени\n    return (num / exp) % 10\n}\n\n/* Сортировка подсчетом (сортировка по k-му разряду nums) */\nfunc countingSortDigit(nums []int, exp int) {\n    // Разряды десятичной системы лежат в диапазоне 0~9, поэтому нужен массив корзин длины 10\n    counter := make([]int, 10)\n    n := len(nums)\n    // Подсчитать число появлений каждой цифры от 0 до 9\n    for i := 0; i < n; i++ {\n        d := digit(nums[i], exp) // Получить k-й разряд nums[i], обозначив его как d\n        counter[d]++             // Подсчитать число появлений цифры d\n    }\n    // Вычислить префиксные суммы и преобразовать «число появлений» в «индекс массива»\n    for i := 1; i < 10; i++ {\n        counter[i] += counter[i-1]\n    }\n    // Выполняя обратный проход, заполнить res элементами по статистике в корзинах\n    res := make([]int, n)\n    for i := n - 1; i >= 0; i-- {\n        d := digit(nums[i], exp)\n        j := counter[d] - 1 // Получить индекс j цифры d в массиве\n        res[j] = nums[i]    // Поместить текущий элемент по индексу j\n        counter[d]--        // Уменьшить количество d на 1\n    }\n    // Перезаписать исходный массив nums результатом\n    for i := 0; i < n; i++ {\n        nums[i] = res[i]\n    }\n}\n\n/* Поразрядная сортировка */\nfunc radixSort(nums []int) {\n    // Получить максимальный элемент массива, чтобы определить максимальное число разрядов\n    max := math.MinInt\n    for _, num := range nums {\n        if num > max {\n            max = num\n        }\n    }\n    // Проходить разряды от младшего к старшему\n    for exp := 1; max >= exp; exp *= 10 {\n        // Выполнить сортировку подсчетом по k-му разряду элементов массива\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // то есть exp = 10^(k-1)\n        countingSortDigit(nums, exp)\n    }\n}\n
        radix_sort.swift
        /* Получить k-й разряд элемента num, где exp = 10^(k-1) */\nfunc digit(num: Int, exp: Int) -> Int {\n    // Передача exp вместо k позволяет избежать повторного дорогостоящего вычисления степени\n    (num / exp) % 10\n}\n\n/* Сортировка подсчетом (сортировка по k-му разряду nums) */\nfunc countingSortDigit(nums: inout [Int], exp: Int) {\n    // Разряды десятичной системы лежат в диапазоне 0~9, поэтому нужен массив корзин длины 10\n    var counter = Array(repeating: 0, count: 10)\n    // Подсчитать число появлений каждой цифры от 0 до 9\n    for i in nums.indices {\n        let d = digit(num: nums[i], exp: exp) // Получить k-й разряд nums[i], обозначив его как d\n        counter[d] += 1 // Подсчитать число появлений цифры d\n    }\n    // Вычислить префиксные суммы и преобразовать «число появлений» в «индекс массива»\n    for i in 1 ..< 10 {\n        counter[i] += counter[i - 1]\n    }\n    // Выполняя обратный проход, заполнить res элементами по статистике в корзинах\n    var res = Array(repeating: 0, count: nums.count)\n    for i in nums.indices.reversed() {\n        let d = digit(num: nums[i], exp: exp)\n        let j = counter[d] - 1 // Получить индекс j цифры d в массиве\n        res[j] = nums[i] // Поместить текущий элемент по индексу j\n        counter[d] -= 1 // Уменьшить количество d на 1\n    }\n    // Перезаписать исходный массив nums результатом\n    for i in nums.indices {\n        nums[i] = res[i]\n    }\n}\n\n/* Поразрядная сортировка */\nfunc radixSort(nums: inout [Int]) {\n    // Получить максимальный элемент массива, чтобы определить максимальное число разрядов\n    var m = Int.min\n    for num in nums {\n        if num > m {\n            m = num\n        }\n    }\n    // Проходить разряды от младшего к старшему\n    for exp in sequence(first: 1, next: { m >= ($0 * 10) ? $0 * 10 : nil }) {\n        // Выполнить сортировку подсчетом по k-му разряду элементов массива\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // то есть exp = 10^(k-1)\n        countingSortDigit(nums: &nums, exp: exp)\n    }\n}\n
        radix_sort.js
        /* Получить k-й разряд элемента num, где exp = 10^(k-1) */\nfunction digit(num, exp) {\n    // Передача exp вместо k позволяет избежать повторного дорогостоящего вычисления степени\n    return Math.floor(num / exp) % 10;\n}\n\n/* Сортировка подсчетом (сортировка по k-му разряду nums) */\nfunction countingSortDigit(nums, exp) {\n    // Разряды десятичной системы лежат в диапазоне 0~9, поэтому нужен массив корзин длины 10\n    const counter = new Array(10).fill(0);\n    const n = nums.length;\n    // Подсчитать число появлений каждой цифры от 0 до 9\n    for (let i = 0; i < n; i++) {\n        const d = digit(nums[i], exp); // Получить k-й разряд nums[i], обозначив его как d\n        counter[d]++; // Подсчитать число появлений цифры d\n    }\n    // Вычислить префиксные суммы и преобразовать «число появлений» в «индекс массива»\n    for (let i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // Выполняя обратный проход, заполнить res элементами по статистике в корзинах\n    const res = new Array(n).fill(0);\n    for (let i = n - 1; i >= 0; i--) {\n        const d = digit(nums[i], exp);\n        const j = counter[d] - 1; // Получить индекс j цифры d в массиве\n        res[j] = nums[i]; // Поместить текущий элемент по индексу j\n        counter[d]--; // Уменьшить количество d на 1\n    }\n    // Перезаписать исходный массив nums результатом\n    for (let i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n\n/* Поразрядная сортировка */\nfunction radixSort(nums) {\n    // Получить максимальный элемент массива, чтобы определить максимальное число разрядов\n    let m = Math.max(... nums);\n    // Проходить разряды от младшего к старшему\n    for (let exp = 1; exp <= m; exp *= 10) {\n        // Выполнить сортировку подсчетом по k-му разряду элементов массива\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // то есть exp = 10^(k-1)\n        countingSortDigit(nums, exp);\n    }\n}\n
        radix_sort.ts
        /* Получить k-й разряд элемента num, где exp = 10^(k-1) */\nfunction digit(num: number, exp: number): number {\n    // Передача exp вместо k позволяет избежать повторного дорогостоящего вычисления степени\n    return Math.floor(num / exp) % 10;\n}\n\n/* Сортировка подсчетом (сортировка по k-му разряду nums) */\nfunction countingSortDigit(nums: number[], exp: number): void {\n    // Разряды десятичной системы лежат в диапазоне 0~9, поэтому нужен массив корзин длины 10\n    const counter = new Array(10).fill(0);\n    const n = nums.length;\n    // Подсчитать число появлений каждой цифры от 0 до 9\n    for (let i = 0; i < n; i++) {\n        const d = digit(nums[i], exp); // Получить k-й разряд nums[i], обозначив его как d\n        counter[d]++; // Подсчитать число появлений цифры d\n    }\n    // Вычислить префиксные суммы и преобразовать «число появлений» в «индекс массива»\n    for (let i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // Выполняя обратный проход, заполнить res элементами по статистике в корзинах\n    const res = new Array(n).fill(0);\n    for (let i = n - 1; i >= 0; i--) {\n        const d = digit(nums[i], exp);\n        const j = counter[d] - 1; // Получить индекс j цифры d в массиве\n        res[j] = nums[i]; // Поместить текущий элемент по индексу j\n        counter[d]--; // Уменьшить количество d на 1\n    }\n    // Перезаписать исходный массив nums результатом\n    for (let i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n\n/* Поразрядная сортировка */\nfunction radixSort(nums: number[]): void {\n    // Получить максимальный элемент массива, чтобы определить максимальное число разрядов\n    let m: number = Math.max(... nums);\n    // Проходить разряды от младшего к старшему\n    for (let exp = 1; exp <= m; exp *= 10) {\n        // Выполнить сортировку подсчетом по k-му разряду элементов массива\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // то есть exp = 10^(k-1)\n        countingSortDigit(nums, exp);\n    }\n}\n
        radix_sort.dart
        /* Получить k-й разряд элемента _num, где exp = 10^(k-1) */\nint digit(int _num, int exp) {\n  // Передача exp вместо k позволяет избежать повторного дорогостоящего вычисления степени\n  return (_num ~/ exp) % 10;\n}\n\n/* Сортировка подсчетом (сортировка по k-му разряду nums) */\nvoid countingSortDigit(List<int> nums, int exp) {\n  // Разряды десятичной системы лежат в диапазоне 0~9, поэтому нужен массив корзин длины 10\n  List<int> counter = List<int>.filled(10, 0);\n  int n = nums.length;\n  // Подсчитать число появлений каждой цифры от 0 до 9\n  for (int i = 0; i < n; i++) {\n    int d = digit(nums[i], exp); // Получить k-й разряд nums[i], обозначив его как d\n    counter[d]++; // Подсчитать число появлений цифры d\n  }\n  // Вычислить префиксные суммы и преобразовать «число появлений» в «индекс массива»\n  for (int i = 1; i < 10; i++) {\n    counter[i] += counter[i - 1];\n  }\n  // Выполняя обратный проход, заполнить res элементами по статистике в корзинах\n  List<int> res = List<int>.filled(n, 0);\n  for (int i = n - 1; i >= 0; i--) {\n    int d = digit(nums[i], exp);\n    int j = counter[d] - 1; // Получить индекс j цифры d в массиве\n    res[j] = nums[i]; // Поместить текущий элемент по индексу j\n    counter[d]--; // Уменьшить количество d на 1\n  }\n  // Перезаписать исходный массив nums результатом\n  for (int i = 0; i < n; i++) nums[i] = res[i];\n}\n\n/* Поразрядная сортировка */\nvoid radixSort(List<int> nums) {\n  // Получить максимальный элемент массива, чтобы определить максимальное число разрядов\n  // В dart длина int составляет 64 бита\n  int m = -1 << 63;\n  for (int _num in nums) if (_num > m) m = _num;\n  // Проходить разряды от младшего к старшему\n  for (int exp = 1; exp <= m; exp *= 10)\n    // Выполнить сортировку подсчетом по k-му разряду элементов массива\n    // k = 1 -> exp = 1\n    // k = 2 -> exp = 10\n    // то есть exp = 10^(k-1)\n    countingSortDigit(nums, exp);\n}\n
        radix_sort.rs
        /* Получить k-й разряд элемента num, где exp = 10^(k-1) */\nfn digit(num: i32, exp: i32) -> usize {\n    // Передача exp вместо k позволяет избежать повторного дорогостоящего вычисления степени\n    return ((num / exp) % 10) as usize;\n}\n\n/* Сортировка подсчетом (сортировка по k-му разряду nums) */\nfn counting_sort_digit(nums: &mut [i32], exp: i32) {\n    // Разряды десятичной системы лежат в диапазоне 0~9, поэтому нужен массив корзин длины 10\n    let mut counter = [0; 10];\n    let n = nums.len();\n    // Подсчитать число появлений каждой цифры от 0 до 9\n    for i in 0..n {\n        let d = digit(nums[i], exp); // Получить k-й разряд nums[i], обозначив его как d\n        counter[d] += 1; // Подсчитать число появлений цифры d\n    }\n    // Вычислить префиксные суммы и преобразовать «число появлений» в «индекс массива»\n    for i in 1..10 {\n        counter[i] += counter[i - 1];\n    }\n    // Выполняя обратный проход, заполнить res элементами по статистике в корзинах\n    let mut res = vec![0; n];\n    for i in (0..n).rev() {\n        let d = digit(nums[i], exp);\n        let j = counter[d] - 1; // Получить индекс j цифры d в массиве\n        res[j] = nums[i]; // Поместить текущий элемент по индексу j\n        counter[d] -= 1; // Уменьшить количество d на 1\n    }\n    // Перезаписать исходный массив nums результатом\n    nums.copy_from_slice(&res);\n}\n\n/* Поразрядная сортировка */\nfn radix_sort(nums: &mut [i32]) {\n    // Получить максимальный элемент массива, чтобы определить максимальное число разрядов\n    let m = *nums.into_iter().max().unwrap();\n    // Проходить разряды от младшего к старшему\n    let mut exp = 1;\n    while exp <= m {\n        counting_sort_digit(nums, exp);\n        exp *= 10;\n    }\n}\n
        radix_sort.c
        /* Получить k-й разряд элемента num, где exp = 10^(k-1) */\nint digit(int num, int exp) {\n    // Передача exp вместо k позволяет избежать повторного дорогостоящего вычисления степени\n    return (num / exp) % 10;\n}\n\n/* Сортировка подсчетом (сортировка по k-му разряду nums) */\nvoid countingSortDigit(int nums[], int size, int exp) {\n    // Разряды десятичной системы лежат в диапазоне 0~9, поэтому нужен массив корзин длины 10\n    int *counter = (int *)malloc((sizeof(int) * 10));\n    memset(counter, 0, sizeof(int) * 10); // Инициализировать нулем для последующего освобождения памяти\n    // Подсчитать число появлений каждой цифры от 0 до 9\n    for (int i = 0; i < size; i++) {\n        // Получить k-й разряд nums[i], обозначив его как d\n        int d = digit(nums[i], exp);\n        // Подсчитать число появлений цифры d\n        counter[d]++;\n    }\n    // Вычислить префиксные суммы и преобразовать «число появлений» в «индекс массива»\n    for (int i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // Выполняя обратный проход, заполнить res элементами по статистике в корзинах\n    int *res = (int *)malloc(sizeof(int) * size);\n    for (int i = size - 1; i >= 0; i--) {\n        int d = digit(nums[i], exp);\n        int j = counter[d] - 1; // Получить индекс j цифры d в массиве\n        res[j] = nums[i];       // Поместить текущий элемент по индексу j\n        counter[d]--;           // Уменьшить количество d на 1\n    }\n    // Перезаписать исходный массив nums результатом\n    for (int i = 0; i < size; i++) {\n        nums[i] = res[i];\n    }\n    // Освободить память\n    free(res);\n    free(counter);\n}\n\n/* Поразрядная сортировка */\nvoid radixSort(int nums[], int size) {\n    // Получить максимальный элемент массива, чтобы определить максимальное число разрядов\n    int max = INT32_MIN;\n    for (int i = 0; i < size; i++) {\n        if (nums[i] > max) {\n            max = nums[i];\n        }\n    }\n    // Проходить разряды от младшего к старшему\n    for (int exp = 1; max >= exp; exp *= 10)\n        // Выполнить сортировку подсчетом по k-му разряду элементов массива\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // то есть exp = 10^(k-1)\n        countingSortDigit(nums, size, exp);\n}\n
        radix_sort.kt
        /* Получить k-й разряд элемента num, где exp = 10^(k-1) */\nfun digit(num: Int, exp: Int): Int {\n    // Передача exp вместо k позволяет избежать повторного дорогостоящего вычисления степени\n    return (num / exp) % 10\n}\n\n/* Сортировка подсчетом (сортировка по k-му разряду nums) */\nfun countingSortDigit(nums: IntArray, exp: Int) {\n    // Разряды десятичной системы лежат в диапазоне 0~9, поэтому нужен массив корзин длины 10\n    val counter = IntArray(10)\n    val n = nums.size\n    // Подсчитать число появлений каждой цифры от 0 до 9\n    for (i in 0..<n) {\n        val d = digit(nums[i], exp) // Получить k-й разряд nums[i], обозначив его как d\n        counter[d]++                // Подсчитать число появлений цифры d\n    }\n    // Вычислить префиксные суммы и преобразовать «число появлений» в «индекс массива»\n    for (i in 1..9) {\n        counter[i] += counter[i - 1]\n    }\n    // Выполняя обратный проход, заполнить res элементами по статистике в корзинах\n    val res = IntArray(n)\n    for (i in n - 1 downTo 0) {\n        val d = digit(nums[i], exp)\n        val j = counter[d] - 1 // Получить индекс j цифры d в массиве\n        res[j] = nums[i]       // Поместить текущий элемент по индексу j\n        counter[d]--           // Уменьшить количество d на 1\n    }\n    // Перезаписать исходный массив nums результатом\n    for (i in 0..<n)\n        nums[i] = res[i]\n}\n\n/* Поразрядная сортировка */\nfun radixSort(nums: IntArray) {\n    // Получить максимальный элемент массива, чтобы определить максимальное число разрядов\n    var m = Int.MIN_VALUE\n    for (num in nums) if (num > m) m = num\n    var exp = 1\n    // Проходить разряды от младшего к старшему\n    while (exp <= m) {\n        // Выполнить сортировку подсчетом по k-му разряду элементов массива\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // то есть exp = 10^(k-1)\n        countingSortDigit(nums, exp)\n        exp *= 10\n    }\n}\n
        radix_sort.rb
        ### Получить k-й разряд элемента num, где exp = 10^(k-1) ###\ndef digit(num, exp)\n  # Передача exp вместо k позволяет избежать повторного выполнения дорогостоящих вычислений степени\n  (num / exp) % 10\nend\n\n### Получить k-й разряд элемента num, где exp = 10^(k-1) ###\ndef digit(num, exp)\n  # Передача exp вместо k позволяет избежать повторного выполнения дорогостоящих вычислений степени\n  (num / exp) % 10\nend\n\n# ## Сортировка подсчетом (сортировка по k-му разряду nums) ###\ndef counting_sort_digit(nums, exp)\n  # Разряды десятичной системы лежат в диапазоне 0~9, поэтому нужен массив корзин длины 10\n  counter = Array.new(10, 0)\n  n = nums.length\n  # Подсчитать число появлений каждой цифры от 0 до 9\n  for i in 0...n\n    d = digit(nums[i], exp) # Получить k-й разряд nums[i], обозначив его как d\n    counter[d] += 1 # Подсчитать число появлений цифры d\n  end\n  # Вычислить префиксные суммы и преобразовать «число появлений» в «индекс массива»\n  (1...10).each { |i| counter[i] += counter[i - 1] }\n  # Выполняя обратный проход, заполнить res элементами по статистике в корзинах\n  res = Array.new(n, 0)\n  for i in (n - 1).downto(0)\n    d = digit(nums[i], exp)\n    j = counter[d] - 1 # Получить индекс j цифры d в массиве\n    res[j] = nums[i] # Поместить текущий элемент по индексу j\n    counter[d] -= 1 # Уменьшить количество d на 1\n  end\n  # Перезаписать исходный массив nums результатом\n  (0...n).each { |i| nums[i] = res[i] }\nend\n\n### Поразрядная сортировка ###\ndef radix_sort(nums)\n  # Получить максимальный элемент массива, чтобы определить максимальное число разрядов\n  m = nums.max\n  # Проходить разряды от младшего к старшему\n  exp = 1\n  while exp <= m\n    # Выполнить сортировку подсчетом по k-му разряду элементов массива\n    # k = 1 -> exp = 1\n    # k = 2 -> exp = 10\n    # то есть exp = 10^(k-1)\n    counting_sort_digit(nums, exp)\n    exp *= 10\n  end\nend\n
        Визуализация кода

        Во весь экран >

        Почему сортировка выполняется от младшего разряда к старшему?

        В последовательных раундах сортировки результаты более позднего раунда перекрывают результаты предыдущего. Например, если после первого раунда получилось \\(a < b\\) , а после второго - \\(a > b\\) , то именно результат второго раунда станет окончательным. Поскольку старшие разряды имеют более высокий приоритет, сначала нужно сортировать по младшим разрядам, а затем по старшим.

        ","path":["Глава 11. Сортировка","11.10   Поразрядная сортировка"],"tags":[]},{"location":"chapter_sorting/radix_sort/#11102","level":2,"title":"11.10.2   Характеристики алгоритма","text":"

        По сравнению с сортировкой подсчетом поразрядная сортировка подходит для случаев с большим диапазоном чисел, но только при условии, что данные можно представить в виде чисел фиксированной длины и число разрядов не слишком велико. Например, числа с плавающей запятой плохо подходят для поразрядной сортировки, потому что число разрядов \\(k\\) слишком велико и может привести к ситуации \\(O(nk) \\gg O(n^2)\\) .

        • Временная сложность равна \\(O(nk)\\), алгоритм не является адаптивным: пусть объем данных равен \\(n\\) , числа записаны в системе счисления с основанием \\(d\\) , а максимальное число разрядов равно \\(k\\) . Тогда выполнение сортировки подсчетом для одного разряда требует \\(O(n + d)\\) времени, а сортировка по всем \\(k\\) разрядам требует \\(O((n + d)k)\\) времени. Обычно \\(d\\) и \\(k\\) сравнительно малы, поэтому временная сложность стремится к \\(O(n)\\) .
        • Пространственная сложность равна \\(O(n + d)\\), сортировка не выполняется на месте: как и в сортировке подсчетом, здесь требуются массивы res и counter длины \\(n\\) и \\(d\\) .
        • Стабильная сортировка: если сортировка подсчетом стабильна, то и поразрядная сортировка стабильна; если же сортировка подсчетом нестабильна, поразрядная сортировка не может гарантировать корректный результат.
        ","path":["Глава 11. Сортировка","11.10   Поразрядная сортировка"],"tags":[]},{"location":"chapter_sorting/selection_sort/","level":1,"title":"11.2   Сортировка выбором","text":"

        Сортировка выбором (selection sort) работает очень просто: запускается цикл, и на каждом шаге из неотсортированного диапазона выбирается минимальный элемент, после чего он переносится в конец уже отсортированного диапазона.

        Пусть длина массива равна \\(n\\) ; тогда процесс сортировки выбором выглядит так, как показано на рисунке 11-2.

        1. В начальном состоянии все элементы не отсортированы, то есть неотсортированный диапазон индексов равен \\([0, n-1]\\) .
        2. Выбрать минимальный элемент из диапазона \\([0, n-1]\\) и поменять его местами с элементом в позиции \\(0\\) . После этого первый элемент массива отсортирован.
        3. Выбрать минимальный элемент из диапазона \\([1, n-1]\\) и поменять его местами с элементом в позиции \\(1\\) . После этого первые два элемента массива отсортированы.
        4. Продолжать по аналогии. После \\(n - 1\\) раундов выбора и обмена первые \\(n - 1\\) элементов массива будут отсортированы.
        5. Оставшийся элемент обязательно является максимальным, сортировать его не нужно, поэтому массив считается отсортированным.
        <1><2><3><4><5><6><7><8><9><10><11>

        Рисунок 11-2   Шаги сортировки выбором

        В коде мы используем \\(k\\) для записи минимального элемента в пределах неотсортированного диапазона:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby selection_sort.py
        def selection_sort(nums: list[int]):\n    \"\"\"Сортировка выбором\"\"\"\n    n = len(nums)\n    # Внешний цикл: неотсортированный диапазон [i, n-1]\n    for i in range(n - 1):\n        # Внутренний цикл: найти минимальный элемент в неотсортированном диапазоне\n        k = i\n        for j in range(i + 1, n):\n            if nums[j] < nums[k]:\n                k = j  # Записать индекс минимального элемента\n        # Поменять этот минимальный элемент местами с первым элементом неотсортированного диапазона\n        nums[i], nums[k] = nums[k], nums[i]\n
        selection_sort.cpp
        /* Сортировка выбором */\nvoid selectionSort(vector<int> &nums) {\n    int n = nums.size();\n    // Внешний цикл: неотсортированный диапазон [i, n-1]\n    for (int i = 0; i < n - 1; i++) {\n        // Внутренний цикл: найти минимальный элемент в неотсортированном диапазоне\n        int k = i;\n        for (int j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k])\n                k = j; // Записать индекс минимального элемента\n        }\n        // Поменять этот минимальный элемент местами с первым элементом неотсортированного диапазона\n        swap(nums[i], nums[k]);\n    }\n}\n
        selection_sort.java
        /* Сортировка выбором */\nvoid selectionSort(int[] nums) {\n    int n = nums.length;\n    // Внешний цикл: неотсортированный диапазон [i, n-1]\n    for (int i = 0; i < n - 1; i++) {\n        // Внутренний цикл: найти минимальный элемент в неотсортированном диапазоне\n        int k = i;\n        for (int j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k])\n                k = j; // Записать индекс минимального элемента\n        }\n        // Поменять этот минимальный элемент местами с первым элементом неотсортированного диапазона\n        int temp = nums[i];\n        nums[i] = nums[k];\n        nums[k] = temp;\n    }\n}\n
        selection_sort.cs
        /* Сортировка выбором */\nvoid SelectionSort(int[] nums) {\n    int n = nums.Length;\n    // Внешний цикл: неотсортированный диапазон [i, n-1]\n    for (int i = 0; i < n - 1; i++) {\n        // Внутренний цикл: найти минимальный элемент в неотсортированном диапазоне\n        int k = i;\n        for (int j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k])\n                k = j; // Записать индекс минимального элемента\n        }\n        // Поменять этот минимальный элемент местами с первым элементом неотсортированного диапазона\n        (nums[k], nums[i]) = (nums[i], nums[k]);\n    }\n}\n
        selection_sort.go
        /* Сортировка выбором */\nfunc selectionSort(nums []int) {\n    n := len(nums)\n    // Внешний цикл: неотсортированный диапазон [i, n-1]\n    for i := 0; i < n-1; i++ {\n        // Внутренний цикл: найти минимальный элемент в неотсортированном диапазоне\n        k := i\n        for j := i + 1; j < n; j++ {\n            if nums[j] < nums[k] {\n                // Записать индекс минимального элемента\n                k = j\n            }\n        }\n        // Поменять этот минимальный элемент местами с первым элементом неотсортированного диапазона\n        nums[i], nums[k] = nums[k], nums[i]\n\n    }\n}\n
        selection_sort.swift
        /* Сортировка выбором */\nfunc selectionSort(nums: inout [Int]) {\n    // Внешний цикл: неотсортированный диапазон [i, n-1]\n    for i in nums.indices.dropLast() {\n        // Внутренний цикл: найти минимальный элемент в неотсортированном диапазоне\n        var k = i\n        for j in nums.indices.dropFirst(i + 1) {\n            if nums[j] < nums[k] {\n                k = j // Записать индекс минимального элемента\n            }\n        }\n        // Поменять этот минимальный элемент местами с первым элементом неотсортированного диапазона\n        nums.swapAt(i, k)\n    }\n}\n
        selection_sort.js
        /* Сортировка выбором */\nfunction selectionSort(nums) {\n    let n = nums.length;\n    // Внешний цикл: неотсортированный диапазон [i, n-1]\n    for (let i = 0; i < n - 1; i++) {\n        // Внутренний цикл: найти минимальный элемент в неотсортированном диапазоне\n        let k = i;\n        for (let j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k]) {\n                k = j; // Записать индекс минимального элемента\n            }\n        }\n        // Поменять этот минимальный элемент местами с первым элементом неотсортированного диапазона\n        [nums[i], nums[k]] = [nums[k], nums[i]];\n    }\n}\n
        selection_sort.ts
        /* Сортировка выбором */\nfunction selectionSort(nums: number[]): void {\n    let n = nums.length;\n    // Внешний цикл: неотсортированный диапазон [i, n-1]\n    for (let i = 0; i < n - 1; i++) {\n        // Внутренний цикл: найти минимальный элемент в неотсортированном диапазоне\n        let k = i;\n        for (let j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k]) {\n                k = j; // Записать индекс минимального элемента\n            }\n        }\n        // Поменять этот минимальный элемент местами с первым элементом неотсортированного диапазона\n        [nums[i], nums[k]] = [nums[k], nums[i]];\n    }\n}\n
        selection_sort.dart
        /* Сортировка выбором */\nvoid selectionSort(List<int> nums) {\n  int n = nums.length;\n  // Внешний цикл: неотсортированный диапазон [i, n-1]\n  for (int i = 0; i < n - 1; i++) {\n    // Внутренний цикл: найти минимальный элемент в неотсортированном диапазоне\n    int k = i;\n    for (int j = i + 1; j < n; j++) {\n      if (nums[j] < nums[k]) k = j; // Записать индекс минимального элемента\n    }\n    // Поменять этот минимальный элемент местами с первым элементом неотсортированного диапазона\n    int temp = nums[i];\n    nums[i] = nums[k];\n    nums[k] = temp;\n  }\n}\n
        selection_sort.rs
        /* Сортировка выбором */\nfn selection_sort(nums: &mut [i32]) {\n    if nums.is_empty() {\n        return;\n    }\n    let n = nums.len();\n    // Внешний цикл: неотсортированный диапазон [i, n-1]\n    for i in 0..n - 1 {\n        // Внутренний цикл: найти минимальный элемент в неотсортированном диапазоне\n        let mut k = i;\n        for j in i + 1..n {\n            if nums[j] < nums[k] {\n                k = j; // Записать индекс минимального элемента\n            }\n        }\n        // Поменять этот минимальный элемент местами с первым элементом неотсортированного диапазона\n        nums.swap(i, k);\n    }\n}\n
        selection_sort.c
        /* Сортировка выбором */\nvoid selectionSort(int nums[], int n) {\n    // Внешний цикл: неотсортированный диапазон [i, n-1]\n    for (int i = 0; i < n - 1; i++) {\n        // Внутренний цикл: найти минимальный элемент в неотсортированном диапазоне\n        int k = i;\n        for (int j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k])\n                k = j; // Записать индекс минимального элемента\n        }\n        // Поменять этот минимальный элемент местами с первым элементом неотсортированного диапазона\n        int temp = nums[i];\n        nums[i] = nums[k];\n        nums[k] = temp;\n    }\n}\n
        selection_sort.kt
        /* Сортировка выбором */\nfun selectionSort(nums: IntArray) {\n    val n = nums.size\n    // Внешний цикл: неотсортированный диапазон [i, n-1]\n    for (i in 0..<n - 1) {\n        var k = i\n        // Внутренний цикл: найти минимальный элемент в неотсортированном диапазоне\n        for (j in i + 1..<n) {\n            if (nums[j] < nums[k])\n                k = j // Записать индекс минимального элемента\n        }\n        // Поменять этот минимальный элемент местами с первым элементом неотсортированного диапазона\n        val temp = nums[i]\n        nums[i] = nums[k]\n        nums[k] = temp\n    }\n}\n
        selection_sort.rb
        ### Сортировка выбором ###\ndef selection_sort(nums)\n  n = nums.length\n  # Внешний цикл: неотсортированный диапазон [i, n-1]\n  for i in 0...(n - 1)\n    # Внутренний цикл: найти минимальный элемент в неотсортированном диапазоне\n    k = i\n    for j in (i + 1)...n\n      if nums[j] < nums[k]\n        k = j # Записать индекс минимального элемента\n      end\n    end\n    # Поменять этот минимальный элемент местами с первым элементом неотсортированного диапазона\n    nums[i], nums[k] = nums[k], nums[i]\n  end\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 11. Сортировка","11.2   Сортировка выбором"],"tags":[]},{"location":"chapter_sorting/selection_sort/#1121","level":2,"title":"11.2.1   Характеристики алгоритма","text":"
        • Временная сложность равна \\(O(n^2)\\), сортировка не является адаптивной: внешний цикл выполняется \\(n - 1\\) раз; в первом раунде длина неотсортированного диапазона равна \\(n\\) , а в последнем - \\(2\\) , то есть отдельные раунды содержат \\(n\\), \\(n - 1\\), \\(\\dots\\), \\(3\\), \\(2\\) проходов внутреннего цикла, их сумма равна \\(\\frac{(n - 1)(n + 2)}{2}\\) .
        • Пространственная сложность равна \\(O(1)\\), сортировка выполняется на месте: указатели \\(i\\) и \\(j\\) используют константный объем дополнительной памяти.
        • Нестабильная сортировка: как показано на рисунке 11-3, элемент nums[i] может быть переставлен вправо от другого равного ему элемента, из-за чего их относительный порядок изменится.

        Рисунок 11-3   Пример нестабильности сортировки выбором

        ","path":["Глава 11. Сортировка","11.2   Сортировка выбором"],"tags":[]},{"location":"chapter_sorting/sorting_algorithm/","level":1,"title":"11.1   Алгоритмы сортировки","text":"

        Алгоритмы сортировки (sorting algorithm) используются для упорядочивания набора данных по определенному правилу. Они применяются очень широко, потому что упорядоченные данные обычно проще анализировать, обрабатывать и искать в них нужные элементы.

        Как показано на рисунке 11-1, данными в алгоритмах сортировки могут быть целые числа, числа с плавающей запятой, символы, строки и другие типы. Критерий сравнения тоже можно задать по-разному, например по величине чисел, по порядку ASCII-кодов символов или по пользовательскому правилу.

        Рисунок 11-1   Примеры типов данных и правил сравнения

        ","path":["Глава 11. Сортировка","11.1   Алгоритмы сортировки"],"tags":[]},{"location":"chapter_sorting/sorting_algorithm/#1111","level":2,"title":"11.1.1   Критерии оценки","text":"

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

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

        Стабильность: стабильная сортировка после завершения не меняет относительный порядок одинаковых элементов в массиве.

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

        # Входные данные уже отсортированы по имени\n# (name, age)\n  ('A', 19)\n  ('B', 18)\n  ('C', 21)\n  ('D', 19)\n  ('E', 23)\n\n# Если затем нестабильным алгоритмом отсортировать список по возрасту,\n# относительный порядок ('D', 19) и ('A', 19) изменится,\n# и свойство упорядоченности по имени будет потеряно\n  ('B', 18)\n  ('D', 19)\n  ('A', 19)\n  ('C', 21)\n  ('E', 23)\n

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

        Основанность на сравнении: сортировка на основе сравнений использует операторы сравнения (\\(<\\), \\(=\\), \\(>\\)), чтобы определить относительный порядок элементов и отсортировать массив; ее теоретически лучшая временная сложность равна \\(O(n \\log n)\\) . А вот сортировка без сравнений не опирается на операторы сравнения, поэтому может достигать \\(O(n)\\) , но универсальность у нее ниже.

        ","path":["Глава 11. Сортировка","11.1   Алгоритмы сортировки"],"tags":[]},{"location":"chapter_sorting/sorting_algorithm/#1112","level":2,"title":"11.1.2   Идеальный алгоритм сортировки","text":"

        Быстрый, выполняющийся на месте, стабильный, адаптивный и универсальный. Очевидно, что на сегодняшний день не существует алгоритма сортировки, который одновременно обладал бы всеми этими свойствами. Поэтому при выборе алгоритма сортировки нужно исходить из конкретных особенностей данных и требований задачи.

        Далее мы последовательно изучим разные алгоритмы сортировки и на основании приведенных выше критериев разберем их преимущества и недостатки.

        ","path":["Глава 11. Сортировка","11.1   Алгоритмы сортировки"],"tags":[]},{"location":"chapter_sorting/summary/","level":1,"title":"11.11   Резюме","text":"","path":["Глава 11. Сортировка","11.11   Резюме"],"tags":[]},{"location":"chapter_sorting/summary/#1","level":3,"title":"1.   Ключевые выводы","text":"
        • Сортировка пузырьком выполняет сортировку за счет обмена соседних элементов. Если добавить флаг для досрочного выхода, лучшую временную сложность пузырьковой сортировки можно оптимизировать до \\(O(n)\\) .
        • Сортировка вставками на каждом раунде вставляет элемент из неотсортированного диапазона в правильную позицию внутри отсортированного диапазона. Хотя ее временная сложность равна \\(O(n^2)\\) , она очень популярна для задач сортировки небольших массивов, поскольку число элементарных операций у нее сравнительно невелико.
        • Быстрая сортировка основана на операции разделения с опорным элементом. При неудачном выборе опорного элемента на каждом раунде ее временная сложность может деградировать до \\(O(n^2)\\) . Использование медианы трех элементов или случайного опорного элемента уменьшает вероятность этой деградации. Если всегда рекурсивно обрабатывать более короткий поддиапазон первым, можно эффективно уменьшить глубину рекурсии и оптимизировать пространственную сложность до \\(O(\\log n)\\) .
        • Сортировка слиянием включает этапы разделения и слияния и служит типичным проявлением стратегии \"разделяй и властвуй\". Для сортировки массива ей требуется вспомогательный массив, поэтому пространственная сложность равна \\(O(n)\\) ; однако при сортировке связного списка пространственную сложность можно оптимизировать до \\(O(1)\\) .
        • Блочная сортировка включает три этапа: распределение данных по блокам, сортировку внутри блоков и объединение результатов. Она тоже отражает стратегию \"разделяй и властвуй\" и подходит для очень больших объемов данных. Ключ к эффективности блочной сортировки - равномерное распределение данных.
        • Сортировка подсчетом является частным случаем блочной сортировки; она реализует сортировку через подсчет числа вхождений данных. Сортировка подсчетом подходит для случаев, когда объем данных велик, но диапазон значений ограничен, и при этом данные можно преобразовать в положительные целые числа.
        • Поразрядная сортировка выполняет сортировку данных путем последовательной сортировки по каждому разряду и требует, чтобы данные можно было представить в виде чисел фиксированной разрядности.
        • В общем случае нам хотелось бы найти алгоритм сортировки, который одновременно обладал бы высокой эффективностью, стабильностью, выполнением на месте и адаптивностью. Но, как и в других разделах алгоритмов и структур данных, не существует одного алгоритма сортировки, способного удовлетворить всем этим требованиям одновременно. На практике приходится выбирать подходящий алгоритм в зависимости от свойств данных.
        • На рисунке 11-19 сравниваются эффективность, стабильность, выполнение на месте и адаптивность основных алгоритмов сортировки.

        Рисунок 11-19   Сравнение алгоритмов сортировки

        ","path":["Глава 11. Сортировка","11.11   Резюме"],"tags":[]},{"location":"chapter_sorting/summary/#2","level":3,"title":"2.   Вопросы и ответы","text":"

        В: В каких случаях стабильность алгоритма сортировки является обязательной?

        В реальных задачах нам может понадобиться сортировать объекты по некоторому атрибуту. Например, у студентов есть два атрибута: имя и рост. Мы хотим выполнить многоуровневую сортировку: сначала отсортировать по имени и получить (A, 180) (B, 185) (C, 170) (D, 170) , а затем отсортировать по росту. Если используемый алгоритм сортировки нестабилен, то мы можем получить (D, 170) (C, 170) (A, 180) (B, 185) .

        Нетрудно увидеть, что в этом случае студенты D и C поменялись местами, порядок по имени разрушился, а именно этого мы и не хотим.

        В: Можно ли поменять местами порядок \"поиска справа налево\" и \"поиска слева направо\" в разделении с опорным элементом?

        Нет. Если в качестве опорного элемента выбирается самый левый элемент, необходимо сначала выполнять \"поиск справа налево\", а уже затем - \"поиск слева направо\". Этот вывод кажется немного неочевидным, поэтому разберем его подробнее.

        Последний шаг partition() - это обмен nums[left] и nums[i] . После обмена все элементы слева от опорного должны быть <= опорного, а значит, перед этим обменом должно выполняться условие nums[left] >= nums[i]. Если сначала выполнять \"поиск слева направо\", то в случае, когда не удается найти элемент больше опорного, цикл завершится в состоянии i == j , и при этом может оказаться, что nums[j] == nums[i] > nums[left]. Иными словами, на последнем шаге обмена элемент, больший опорного, будет помещен в начало массива, из-за чего разделение завершится неверно.

        Например, для массива [0, 0, 0, 0, 1] , если сначала выполнять \"поиск слева направо\", после разделения получится [1, 0, 0, 0, 0] , а это неправильный результат.

        Если же выбрать nums[right] в качестве опорного элемента, то ситуация станет противоположной, и тогда сначала нужно выполнять \"поиск слева направо\".

        В: Почему при оптимизации глубины рекурсии в быстрой сортировке выбор короткого массива гарантирует, что глубина рекурсии не превысит \\(\\log n\\) ?

        Глубина рекурсии - это число текущих рекурсивных вызовов, которые еще не завершились. На каждом раунде разделения исходный массив разбивается на два подмассива. После оптимизации глубины рекурсии длина подмассива, в который мы продолжаем рекурсивный спуск, не превышает половины длины исходного массива. Если рассматривать худший случай, когда длина каждый раз становится ровно вдвое меньше, итоговая глубина рекурсии и будет равна \\(\\log n\\) .

        В исходной версии быстрой сортировки может происходить последовательный рекурсивный вызов для более длинных массивов; в худшем случае это будут длины \\(n\\) , \\(n - 1\\) , \\(\\dots\\) , \\(2\\) , \\(1\\) , а глубина рекурсии окажется равной \\(n\\) . Оптимизация глубины рекурсии как раз и позволяет избежать такого сценария.

        В: Если все элементы массива равны, будет ли временная сложность быстрой сортировки равна \\(O(n^2)\\) ? Как справиться с таким вырождением?

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

        В: Почему худшая временная сложность блочной сортировки равна \\(O(n^2)\\) ?

        В худшем случае все элементы попадут в один и тот же блок. Если затем сортировать этот блок алгоритмом со сложностью \\(O(n^2)\\) , то общая временная сложность тоже станет \\(O(n^2)\\) .

        ","path":["Глава 11. Сортировка","11.11   Резюме"],"tags":[]},{"location":"chapter_stack_and_queue/","level":1,"title":"Глава 5.   Стек и очередь","text":"

        Abstract

        Стек и очередь - две базовые линейные структуры данных.

        Они соответственно воплощают принципы \"последним пришел - первым вышел\" и \"первым пришел - первым вышел\".

        ","path":["Глава 5. Стек и очередь","Глава 5.   Стек и очередь"],"tags":[]},{"location":"chapter_stack_and_queue/#_1","level":2,"title":"Содержание главы","text":"
        • 5.1   Стек
        • 5.2   Очередь
        • 5.3   Двусторонняя очередь
        • 5.4   Резюме
        ","path":["Глава 5. Стек и очередь","Глава 5.   Стек и очередь"],"tags":[]},{"location":"chapter_stack_and_queue/deque/","level":1,"title":"5.3   Двусторонняя очередь","text":"

        В обычной очереди мы можем удалять элементы только из головы и добавлять их только в хвост. Как показано на рисунке 5-7, двусторонняя очередь (double-ended queue) обеспечивает большую гибкость и позволяет выполнять добавление и удаление элементов как с головы, так и с хвоста.

        Рисунок 5-7   Операции двусторонней очереди

        ","path":["Глава 5. Стек и очередь","5.3   Двусторонняя очередь"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#531","level":2,"title":"5.3.1   Основные операции с двусторонней очередью","text":"

        Распространенные операции двусторонней очереди приведены в таблице 5-3. Конкретные названия методов зависят от используемого языка программирования.

        Таблица 5-3   Эффективность операций двусторонней очереди

        Имя метода Описание Временная сложность push_first() Добавить элемент в голову очереди \\(O(1)\\) push_last() Добавить элемент в хвост очереди \\(O(1)\\) pop_first() Удалить элемент из головы очереди \\(O(1)\\) pop_last() Удалить элемент из хвоста очереди \\(O(1)\\) peek_first() Просмотреть элемент в голове очереди \\(O(1)\\) peek_last() Просмотреть элемент в хвосте очереди \\(O(1)\\)

        Точно так же мы можем напрямую использовать уже реализованные в языках программирования классы двусторонней очереди:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby deque.py
        from collections import deque\n\n# Инициализация двусторонней очереди\ndeq: deque[int] = deque()\n\n# Поместить элементы в очередь\ndeq.append(2)      # Добавить в хвост\ndeq.append(5)\ndeq.append(4)\ndeq.appendleft(3)  # Добавить в голову\ndeq.appendleft(1)\n\n# Просмотреть элементы\nfront: int = deq[0]  # Элемент в голове\nrear: int = deq[-1]  # Элемент в хвосте\n\n# Извлечь элементы из очереди\npop_front: int = deq.popleft()  # Извлечь элемент из головы\npop_rear: int = deq.pop()       # Извлечь элемент из хвоста\n\n# Получить длину двусторонней очереди\nsize: int = len(deq)\n\n# Проверить, пуста ли двусторонняя очередь\nis_empty: bool = len(deq) == 0\n
        deque.cpp
        /* Инициализация двусторонней очереди */\ndeque<int> deque;\n\n/* Поместить элементы в очередь */\ndeque.push_back(2);   // Добавить в хвост\ndeque.push_back(5);\ndeque.push_back(4);\ndeque.push_front(3);  // Добавить в голову\ndeque.push_front(1);\n\n/* Просмотреть элементы */\nint front = deque.front(); // Элемент в голове\nint back = deque.back();   // Элемент в хвосте\n\n/* Извлечь элементы из очереди */\ndeque.pop_front();  // Извлечь элемент из головы\ndeque.pop_back();   // Извлечь элемент из хвоста\n\n/* Получить длину двусторонней очереди */\nint size = deque.size();\n\n/* Проверить, пуста ли двусторонняя очередь */\nbool empty = deque.empty();\n
        deque.java
        /* Инициализация двусторонней очереди */\nDeque<Integer> deque = new LinkedList<>();\n\n/* Поместить элементы в очередь */\ndeque.offerLast(2);   // Добавить в хвост\ndeque.offerLast(5);\ndeque.offerLast(4);\ndeque.offerFirst(3);  // Добавить в голову\ndeque.offerFirst(1);\n\n/* Просмотреть элементы */\nint peekFirst = deque.peekFirst();  // Элемент в голове\nint peekLast = deque.peekLast();    // Элемент в хвосте\n\n/* Извлечь элементы из очереди */\nint popFirst = deque.pollFirst();  // Извлечь элемент из головы\nint popLast = deque.pollLast();    // Извлечь элемент из хвоста\n\n/* Получить длину двусторонней очереди */\nint size = deque.size();\n\n/* Проверить, пуста ли двусторонняя очередь */\nboolean isEmpty = deque.isEmpty();\n
        deque.cs
        /* Инициализация двусторонней очереди */\n// В C# двустороннюю очередь обычно моделируют через связный список LinkedList\nLinkedList<int> deque = new();\n\n/* Поместить элементы в очередь */\ndeque.AddLast(2);   // Добавить в хвост\ndeque.AddLast(5);\ndeque.AddLast(4);\ndeque.AddFirst(3);  // Добавить в голову\ndeque.AddFirst(1);\n\n/* Просмотреть элементы */\nint peekFirst = deque.First.Value;  // Элемент в голове\nint peekLast = deque.Last.Value;    // Элемент в хвосте\n\n/* Извлечь элементы из очереди */\ndeque.RemoveFirst();  // Извлечь элемент из головы\ndeque.RemoveLast();   // Извлечь элемент из хвоста\n\n/* Получить длину двусторонней очереди */\nint size = deque.Count;\n\n/* Проверить, пуста ли двусторонняя очередь */\nbool isEmpty = deque.Count == 0;\n
        deque_test.go
        /* Инициализация двусторонней очереди */\n// В Go list обычно используется как двусторонняя очередь\ndeque := list.New()\n\n/* Поместить элементы в очередь */\ndeque.PushBack(2)      // Добавить в хвост\ndeque.PushBack(5)\ndeque.PushBack(4)\ndeque.PushFront(3)     // Добавить в голову\ndeque.PushFront(1)\n\n/* Просмотреть элементы */\nfront := deque.Front() // Элемент в голове\nrear := deque.Back()   // Элемент в хвосте\n\n/* Извлечь элементы из очереди */\ndeque.Remove(front)    // Извлечь элемент из головы\ndeque.Remove(rear)     // Извлечь элемент из хвоста\n\n/* Получить длину двусторонней очереди */\nsize := deque.Len()\n\n/* Проверить, пуста ли двусторонняя очередь */\nisEmpty := deque.Len() == 0\n
        deque.swift
        /* Инициализация двусторонней очереди */\n// В Swift нет встроенного класса двусторонней очереди, поэтому можно использовать Array как двустороннюю очередь\nvar deque: [Int] = []\n\n/* Поместить элементы в очередь */\ndeque.append(2) // Добавить в хвост\ndeque.append(5)\ndeque.append(4)\ndeque.insert(3, at: 0) // Добавить в голову\ndeque.insert(1, at: 0)\n\n/* Просмотреть элементы */\nlet peekFirst = deque.first! // Элемент в голове\nlet peekLast = deque.last! // Элемент в хвосте\n\n/* Извлечь элементы из очереди */\n// При моделировании через Array сложность popFirst равна O(n)\nlet popFirst = deque.removeFirst() // Извлечь элемент из головы\nlet popLast = deque.removeLast() // Извлечь элемент из хвоста\n\n/* Получить длину двусторонней очереди */\nlet size = deque.count\n\n/* Проверить, пуста ли двусторонняя очередь */\nlet isEmpty = deque.isEmpty\n
        deque.js
        /* Инициализация двусторонней очереди */\n// В JavaScript нет встроенной двусторонней очереди, поэтому можно использовать Array как двустороннюю очередь\nconst deque = [];\n\n/* Поместить элементы в очередь */\ndeque.push(2);\ndeque.push(5);\ndeque.push(4);\n// Обрати внимание: поскольку это массив, метод unshift() имеет сложность O(n)\ndeque.unshift(3);\ndeque.unshift(1);\n\n/* Просмотреть элементы */\nconst peekFirst = deque[0];\nconst peekLast = deque[deque.length - 1];\n\n/* Извлечь элементы из очереди */\n// Обрати внимание: поскольку это массив, метод shift() имеет сложность O(n)\nconst popFront = deque.shift();\nconst popBack = deque.pop();\n\n/* Получить длину двусторонней очереди */\nconst size = deque.length;\n\n/* Проверить, пуста ли двусторонняя очередь */\nconst isEmpty = size === 0;\n
        deque.ts
        /* Инициализация двусторонней очереди */\n// В TypeScript нет встроенной двусторонней очереди, поэтому можно использовать Array как двустороннюю очередь\nconst deque: number[] = [];\n\n/* Поместить элементы в очередь */\ndeque.push(2);\ndeque.push(5);\ndeque.push(4);\n// Обрати внимание: поскольку это массив, метод unshift() имеет сложность O(n)\ndeque.unshift(3);\ndeque.unshift(1);\n\n/* Просмотреть элементы */\nconst peekFirst: number = deque[0];\nconst peekLast: number = deque[deque.length - 1];\n\n/* Извлечь элементы из очереди */\n// Обрати внимание: поскольку это массив, метод shift() имеет сложность O(n)\nconst popFront: number = deque.shift() as number;\nconst popBack: number = deque.pop() as number;\n\n/* Получить длину двусторонней очереди */\nconst size: number = deque.length;\n\n/* Проверить, пуста ли двусторонняя очередь */\nconst isEmpty: boolean = size === 0;\n
        deque.dart
        /* Инициализация двусторонней очереди */\n// В Dart Queue определена как двусторонняя очередь\nQueue<int> deque = Queue<int>();\n\n/* Поместить элементы в очередь */\ndeque.addLast(2);  // Добавить в хвост\ndeque.addLast(5);\ndeque.addLast(4);\ndeque.addFirst(3); // Добавить в голову\ndeque.addFirst(1);\n\n/* Просмотреть элементы */\nint peekFirst = deque.first; // Элемент в голове\nint peekLast = deque.last;   // Элемент в хвосте\n\n/* Извлечь элементы из очереди */\nint popFirst = deque.removeFirst(); // Извлечь элемент из головы\nint popLast = deque.removeLast();   // Извлечь элемент из хвоста\n\n/* Получить длину двусторонней очереди */\nint size = deque.length;\n\n/* Проверить, пуста ли двусторонняя очередь */\nbool isEmpty = deque.isEmpty;\n
        deque.rs
        /* Инициализация двусторонней очереди */\nlet mut deque: VecDeque<u32> = VecDeque::new();\n\n/* Поместить элементы в очередь */\ndeque.push_back(2);  // Добавить в хвост\ndeque.push_back(5);\ndeque.push_back(4);\ndeque.push_front(3); // Добавить в голову\ndeque.push_front(1);\n\n/* Просмотреть элементы */\nif let Some(front) = deque.front() { // Элемент в голове\n}\nif let Some(rear) = deque.back() {   // Элемент в хвосте\n}\n\n/* Извлечь элементы из очереди */\nif let Some(pop_front) = deque.pop_front() { // Извлечь элемент из головы\n}\nif let Some(pop_rear) = deque.pop_back() {   // Извлечь элемент из хвоста\n}\n\n/* Получить длину двусторонней очереди */\nlet size = deque.len();\n\n/* Проверить, пуста ли двусторонняя очередь */\nlet is_empty = deque.is_empty();\n
        deque.c
        // В C нет встроенной двусторонней очереди\n
        deque.kt
        /* Инициализация двусторонней очереди */\nval deque = LinkedList<Int>()\n\n/* Поместить элементы в очередь */\ndeque.offerLast(2)  // Добавить в хвост\ndeque.offerLast(5)\ndeque.offerLast(4)\ndeque.offerFirst(3) // Добавить в голову\ndeque.offerFirst(1)\n\n/* Просмотреть элементы */\nval peekFirst = deque.peekFirst() // Элемент в голове\nval peekLast = deque.peekLast()   // Элемент в хвосте\n\n/* Извлечь элементы из очереди */\nval popFirst = deque.pollFirst() // Извлечь элемент из головы\nval popLast = deque.pollLast()   // Извлечь элемент из хвоста\n\n/* Получить длину двусторонней очереди */\nval size = deque.size\n\n/* Проверить, пуста ли двусторонняя очередь */\nval isEmpty = deque.isEmpty()\n
        deque.rb
        # Инициализация двусторонней очереди\n# В Ruby нет встроенной двусторонней очереди, поэтому можно использовать Array как двустороннюю очередь\ndeque = []\n\n# Поместить элементы в очередь\ndeque << 2\ndeque << 5\ndeque << 4\n# Обрати внимание: поскольку это массив, метод Array#unshift имеет сложность O(n)\ndeque.unshift(3)\ndeque.unshift(1)\n\n# Просмотреть элементы\npeek_first = deque.first\npeek_last = deque.last\n\n# Извлечь элементы из очереди\n# Обрати внимание: поскольку это массив, метод Array#shift имеет сложность O(n)\npop_front = deque.shift\npop_back = deque.pop\n\n# Получить длину двусторонней очереди\nsize = deque.length\n\n# Проверить, пуста ли двусторонняя очередь\nis_empty = size.zero?\n
        Визуализация выполнения

        https://pythontutor.com/render.html#code=from%20collections%20import%20deque%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D0%B4%D0%B2%D1%83%D1%81%D1%82%D0%BE%D1%80%D0%BE%D0%BD%D0%BD%D1%8E%D1%8E%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C%0A%20%20%20%20deq%20%3D%20deque%28%29%0A%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BC%D0%B5%D1%81%D1%82%D0%B8%D1%82%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B2%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C%0A%20%20%20%20deq.append%282%29%20%20%23%20%D0%94%D0%BE%D0%B1%D0%B0%D0%B2%D0%B8%D1%82%D1%8C%20%D0%B2%20%D1%85%D0%B2%D0%BE%D1%81%D1%82%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%0A%20%20%20%20deq.append%285%29%0A%20%20%20%20deq.append%284%29%0A%20%20%20%20deq.appendleft%283%29%20%20%23%20%D0%94%D0%BE%D0%B1%D0%B0%D0%B2%D0%B8%D1%82%D1%8C%20%D0%B2%20%D0%B3%D0%BE%D0%BB%D0%BE%D0%B2%D1%83%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%0A%20%20%20%20deq.appendleft%281%29%0A%20%20%20%20print%28%22%D0%B4%D0%B2%D1%83%D1%81%D1%82%D0%BE%D1%80%D0%BE%D0%BD%D0%BD%D1%8F%D1%8F%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C%20deque%20%3D%22%2C%20deq%29%0A%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B8%D1%82%D1%8C%20%D0%B4%D0%BE%D1%81%D1%82%D1%83%D0%BF%20%D0%BA%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D1%83%0A%20%20%20%20front%20%3D%20deq%5B0%5D%20%20%23%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B2%20%D0%B3%D0%BE%D0%BB%D0%BE%D0%B2%D0%B5%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%0A%20%20%20%20print%28%22%D0%AD%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B2%20%D0%BD%D0%B0%D1%87%D0%B0%D0%BB%D0%B5%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%20front%20%3D%22%2C%20front%29%0A%20%20%20%20rear%20%3D%20deq%5B-1%5D%20%20%23%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B2%20%D1%85%D0%B2%D0%BE%D1%81%D1%82%D0%B5%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%0A%20%20%20%20print%28%22%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B2%20%D1%85%D0%B2%D0%BE%D1%81%D1%82%D0%B5%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%20rear%20%3D%22%2C%20rear%29%0A%0A%20%20%20%20%23%20%D0%98%D0%B7%D0%B2%D0%BB%D0%B5%D1%87%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B8%D0%B7%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%0A%20%20%20%20pop_front%20%3D%20deq.popleft%28%29%20%20%23%20%D0%B3%D0%BE%D0%BB%D0%BE%D0%B2%D0%B0%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%D0%98%D0%B7%D0%B2%D0%BB%D0%B5%D1%87%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B8%D0%B7%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%0A%20%20%20%20print%28%22%D0%AD%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%2C%20%D0%B8%D0%B7%D0%B2%D0%BB%D0%B5%D1%87%D0%B5%D0%BD%D0%BD%D1%8B%D0%B9%20%D0%B8%D0%B7%20%D0%B3%D0%BE%D0%BB%D0%BE%D0%B2%D1%8B%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%2C%20pop_front%20%3D%22%2C%20pop_front%29%0A%20%20%20%20print%28%22deque%20%D0%BF%D0%BE%D1%81%D0%BB%D0%B5%20%D0%B8%D0%B7%D0%B2%D0%BB%D0%B5%D1%87%D0%B5%D0%BD%D0%B8%D1%8F%20%D0%B8%D0%B7%20%D0%B3%D0%BE%D0%BB%D0%BE%D0%B2%D1%8B%20%3D%22%2C%20deq%29%0A%20%20%20%20pop_rear%20%3D%20deq.pop%28%29%20%20%23%20%D1%85%D0%B2%D0%BE%D1%81%D1%82%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%D0%98%D0%B7%D0%B2%D0%BB%D0%B5%D1%87%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B8%D0%B7%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%0A%20%20%20%20print%28%22%D0%AD%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%2C%20%D0%B8%D0%B7%D0%B2%D0%BB%D0%B5%D1%87%D0%B5%D0%BD%D0%BD%D1%8B%D0%B9%20%D0%B8%D0%B7%20%D1%85%D0%B2%D0%BE%D1%81%D1%82%D0%B0%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%2C%20pop_rear%20%3D%22%2C%20pop_rear%29%0A%20%20%20%20print%28%22deque%20%D0%BF%D0%BE%D1%81%D0%BB%D0%B5%20%D0%B8%D0%B7%D0%B2%D0%BB%D0%B5%D1%87%D0%B5%D0%BD%D0%B8%D1%8F%20%D0%B8%D0%B7%20%D1%85%D0%B2%D0%BE%D1%81%D1%82%D0%B0%20%3D%22%2C%20deq%29%0A%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B8%D1%82%D1%8C%20%D0%B4%D0%BB%D0%B8%D0%BD%D1%83%20%D0%B4%D0%B2%D1%83%D1%81%D1%82%D0%BE%D1%80%D0%BE%D0%BD%D0%BD%D0%B5%D0%B9%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%0A%20%20%20%20size%20%3D%20len%28deq%29%0A%20%20%20%20print%28%22%D0%94%D0%BB%D0%B8%D0%BD%D0%B0%20%D0%B4%D0%B2%D1%83%D1%81%D1%82%D0%BE%D1%80%D0%BE%D0%BD%D0%BD%D0%B5%D0%B9%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%20size%20%3D%22%2C%20size%29%0A%0A%20%20%20%20%23%20%D0%9F%D1%80%D0%BE%D0%B2%D0%B5%D1%80%D0%B8%D1%82%D1%8C%2C%20%D0%BF%D1%83%D1%81%D1%82%D0%B0%20%D0%BB%D0%B8%20%D0%B4%D0%B2%D1%83%D1%81%D1%82%D0%BE%D1%80%D0%BE%D0%BD%D0%BD%D1%8F%D1%8F%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C%0A%20%20%20%20is_empty%20%3D%20len%28deq%29%20%3D%3D%200%0A%20%20%20%20print%28%22%D0%9F%D1%83%D1%81%D1%82%D0%B0%20%D0%BB%D0%B8%20%D0%B4%D0%B2%D1%83%D1%81%D1%82%D0%BE%D1%80%D0%BE%D0%BD%D0%BD%D1%8F%D1%8F%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C%20%3D%22%2C%20is_empty%29&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

        ","path":["Глава 5. Стек и очередь","5.3   Двусторонняя очередь"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#532","level":2,"title":"5.3.2   Реализация двусторонней очереди *","text":"

        Реализация двусторонней очереди похожа на реализацию обычной очереди: в качестве базовой структуры данных можно выбрать связный список или массив.

        ","path":["Глава 5. Стек и очередь","5.3   Двусторонняя очередь"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#1","level":3,"title":"1.   Реализация на основе двусвязного списка","text":"

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

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

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

        <1><2><3><4><5>

        Рисунок 5-8   Операции enqueue и dequeue для двусторонней очереди на связном списке

        Код реализации приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linkedlist_deque.py
        class ListNode:\n    \"\"\"Узел двусвязного списка\"\"\"\n\n    def __init__(self, val: int):\n        \"\"\"Конструктор\"\"\"\n        self.val: int = val\n        self.next: ListNode | None = None  # Ссылка на узел-преемник\n        self.prev: ListNode | None = None  # Ссылка на узел-предшественник\n\nclass LinkedListDeque:\n    \"\"\"Двусторонняя очередь на основе двусвязного списка\"\"\"\n\n    def __init__(self):\n        \"\"\"Конструктор\"\"\"\n        self._front: ListNode | None = None  # Головной узел front\n        self._rear: ListNode | None = None  # Хвостовой узел rear\n        self._size: int = 0  # Длина двусторонней очереди\n\n    def size(self) -> int:\n        \"\"\"Получение длины двусторонней очереди\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"Проверка, пуста ли двусторонняя очередь\"\"\"\n        return self._size == 0\n\n    def push(self, num: int, is_front: bool):\n        \"\"\"Операция добавления в очередь\"\"\"\n        node = ListNode(num)\n        # Если связный список пуст, сделать так, чтобы и front, и rear указывали на node\n        if self.is_empty():\n            self._front = self._rear = node\n        # Операция добавления в голову очереди\n        elif is_front:\n            # Добавить node в голову списка\n            self._front.prev = node\n            node.next = self._front\n            self._front = node  # Обновить головной узел\n        # Операция добавления в хвост очереди\n        else:\n            # Добавить node в хвост списка\n            self._rear.next = node\n            node.prev = self._rear\n            self._rear = node  # Обновить хвостовой узел\n        self._size += 1  # Обновить длину очереди\n\n    def push_first(self, num: int):\n        \"\"\"Добавление в голову очереди\"\"\"\n        self.push(num, True)\n\n    def push_last(self, num: int):\n        \"\"\"Добавление в хвост очереди\"\"\"\n        self.push(num, False)\n\n    def pop(self, is_front: bool) -> int:\n        \"\"\"Операция извлечения из очереди\"\"\"\n        if self.is_empty():\n            raise IndexError(\"двусторонняя очередь пуста\")\n        # Операция извлечения из головы очереди\n        if is_front:\n            val: int = self._front.val  # Временно сохранить значение головного узла\n            # Удалить головной узел\n            fnext: ListNode | None = self._front.next\n            if fnext is not None:\n                fnext.prev = None\n                self._front.next = None\n            self._front = fnext  # Обновить головной узел\n        # Операция извлечения из хвоста очереди\n        else:\n            val: int = self._rear.val  # Временно сохранить значение хвостового узла\n            # Удалить хвостовой узел\n            rprev: ListNode | None = self._rear.prev\n            if rprev is not None:\n                rprev.next = None\n                self._rear.prev = None\n            self._rear = rprev  # Обновить хвостовой узел\n        self._size -= 1  # Обновить длину очереди\n        return val\n\n    def pop_first(self) -> int:\n        \"\"\"Извлечение из головы очереди\"\"\"\n        return self.pop(True)\n\n    def pop_last(self) -> int:\n        \"\"\"Извлечение из хвоста очереди\"\"\"\n        return self.pop(False)\n\n    def peek_first(self) -> int:\n        \"\"\"Доступ к элементу в начале очереди\"\"\"\n        if self.is_empty():\n            raise IndexError(\"двусторонняя очередь пуста\")\n        return self._front.val\n\n    def peek_last(self) -> int:\n        \"\"\"Доступ к элементу в конце очереди\"\"\"\n        if self.is_empty():\n            raise IndexError(\"двусторонняя очередь пуста\")\n        return self._rear.val\n\n    def to_array(self) -> list[int]:\n        \"\"\"Вернуть массив для вывода\"\"\"\n        node = self._front\n        res = [0] * self.size()\n        for i in range(self.size()):\n            res[i] = node.val\n            node = node.next\n        return res\n
        linkedlist_deque.cpp
        /* Узел двусвязного списка */\nstruct DoublyListNode {\n    int val;              // Значение узла\n    DoublyListNode *next; // Указатель на узел-преемник\n    DoublyListNode *prev; // Указатель на узел-предшественник\n    DoublyListNode(int val) : val(val), prev(nullptr), next(nullptr) {\n    }\n};\n\n/* Двусторонняя очередь на основе двусвязного списка */\nclass LinkedListDeque {\n  private:\n    DoublyListNode *front, *rear; // Головной узел front, хвостовой узел rear\n    int queSize = 0;              // Длина двусторонней очереди\n\n  public:\n    /* Конструктор */\n    LinkedListDeque() : front(nullptr), rear(nullptr) {\n    }\n\n    /* Метод-деструктор */\n    ~LinkedListDeque() {\n        // Обходить связный список, удалять узлы и освобождать память\n        DoublyListNode *pre, *cur = front;\n        while (cur != nullptr) {\n            pre = cur;\n            cur = cur->next;\n            delete pre;\n        }\n    }\n\n    /* Получение длины двусторонней очереди */\n    int size() {\n        return queSize;\n    }\n\n    /* Проверка, пуста ли двусторонняя очередь */\n    bool isEmpty() {\n        return size() == 0;\n    }\n\n    /* Операция добавления в очередь */\n    void push(int num, bool isFront) {\n        DoublyListNode *node = new DoublyListNode(num);\n        // Если связный список пуст, сделать так, чтобы и front, и rear указывали на node\n        if (isEmpty())\n            front = rear = node;\n        // Операция добавления в голову очереди\n        else if (isFront) {\n            // Добавить node в голову списка\n            front->prev = node;\n            node->next = front;\n            front = node; // Обновить головной узел\n        // Операция добавления в хвост очереди\n        } else {\n            // Добавить node в хвост списка\n            rear->next = node;\n            node->prev = rear;\n            rear = node; // Обновить хвостовой узел\n        }\n        queSize++; // Обновить длину очереди\n    }\n\n    /* Добавление в голову очереди */\n    void pushFirst(int num) {\n        push(num, true);\n    }\n\n    /* Добавление в хвост очереди */\n    void pushLast(int num) {\n        push(num, false);\n    }\n\n    /* Операция извлечения из очереди */\n    int pop(bool isFront) {\n        if (isEmpty())\n            throw out_of_range(\"очередь пуста\");\n        int val;\n        // Операция извлечения из головы очереди\n        if (isFront) {\n            val = front->val; // Временно сохранить значение головного узла\n            // Удалить головной узел\n            DoublyListNode *fNext = front->next;\n            if (fNext != nullptr) {\n                fNext->prev = nullptr;\n                front->next = nullptr;\n            }\n            delete front;\n            front = fNext; // Обновить головной узел\n        // Операция извлечения из хвоста очереди\n        } else {\n            val = rear->val; // Временно сохранить значение хвостового узла\n            // Удалить хвостовой узел\n            DoublyListNode *rPrev = rear->prev;\n            if (rPrev != nullptr) {\n                rPrev->next = nullptr;\n                rear->prev = nullptr;\n            }\n            delete rear;\n            rear = rPrev; // Обновить хвостовой узел\n        }\n        queSize--; // Обновить длину очереди\n        return val;\n    }\n\n    /* Извлечение из головы очереди */\n    int popFirst() {\n        return pop(true);\n    }\n\n    /* Извлечение из хвоста очереди */\n    int popLast() {\n        return pop(false);\n    }\n\n    /* Доступ к элементу в начале очереди */\n    int peekFirst() {\n        if (isEmpty())\n            throw out_of_range(\"двусторонняя очередь пуста\");\n        return front->val;\n    }\n\n    /* Доступ к элементу в конце очереди */\n    int peekLast() {\n        if (isEmpty())\n            throw out_of_range(\"двусторонняя очередь пуста\");\n        return rear->val;\n    }\n\n    /* Вернуть массив для вывода */\n    vector<int> toVector() {\n        DoublyListNode *node = front;\n        vector<int> res(size());\n        for (int i = 0; i < res.size(); i++) {\n            res[i] = node->val;\n            node = node->next;\n        }\n        return res;\n    }\n};\n
        linkedlist_deque.java
        /* Узел двусвязного списка */\nclass ListNode {\n    int val; // Значение узла\n    ListNode next; // Ссылка на узел-преемник\n    ListNode prev; // Ссылка на узел-предшественник\n\n    ListNode(int val) {\n        this.val = val;\n        prev = next = null;\n    }\n}\n\n/* Двусторонняя очередь на основе двусвязного списка */\nclass LinkedListDeque {\n    private ListNode front, rear; // Головной узел front, хвостовой узел rear\n    private int queSize = 0; // Длина двусторонней очереди\n\n    public LinkedListDeque() {\n        front = rear = null;\n    }\n\n    /* Получение длины двусторонней очереди */\n    public int size() {\n        return queSize;\n    }\n\n    /* Проверка, пуста ли двусторонняя очередь */\n    public boolean isEmpty() {\n        return size() == 0;\n    }\n\n    /* Операция добавления в очередь */\n    private void push(int num, boolean isFront) {\n        ListNode node = new ListNode(num);\n        // Если связный список пуст, сделать так, чтобы и front, и rear указывали на node\n        if (isEmpty())\n            front = rear = node;\n        // Операция добавления в голову очереди\n        else if (isFront) {\n            // Добавить node в голову списка\n            front.prev = node;\n            node.next = front;\n            front = node; // Обновить головной узел\n        // Операция добавления в хвост очереди\n        } else {\n            // Добавить node в хвост списка\n            rear.next = node;\n            node.prev = rear;\n            rear = node; // Обновить хвостовой узел\n        }\n        queSize++; // Обновить длину очереди\n    }\n\n    /* Добавление в голову очереди */\n    public void pushFirst(int num) {\n        push(num, true);\n    }\n\n    /* Добавление в хвост очереди */\n    public void pushLast(int num) {\n        push(num, false);\n    }\n\n    /* Операция извлечения из очереди */\n    private int pop(boolean isFront) {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        int val;\n        // Операция извлечения из головы очереди\n        if (isFront) {\n            val = front.val; // Временно сохранить значение головного узла\n            // Удалить головной узел\n            ListNode fNext = front.next;\n            if (fNext != null) {\n                fNext.prev = null;\n                front.next = null;\n            }\n            front = fNext; // Обновить головной узел\n        // Операция извлечения из хвоста очереди\n        } else {\n            val = rear.val; // Временно сохранить значение хвостового узла\n            // Удалить хвостовой узел\n            ListNode rPrev = rear.prev;\n            if (rPrev != null) {\n                rPrev.next = null;\n                rear.prev = null;\n            }\n            rear = rPrev; // Обновить хвостовой узел\n        }\n        queSize--; // Обновить длину очереди\n        return val;\n    }\n\n    /* Извлечение из головы очереди */\n    public int popFirst() {\n        return pop(true);\n    }\n\n    /* Извлечение из хвоста очереди */\n    public int popLast() {\n        return pop(false);\n    }\n\n    /* Доступ к элементу в начале очереди */\n    public int peekFirst() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return front.val;\n    }\n\n    /* Доступ к элементу в конце очереди */\n    public int peekLast() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return rear.val;\n    }\n\n    /* Вернуть массив для вывода */\n    public int[] toArray() {\n        ListNode node = front;\n        int[] res = new int[size()];\n        for (int i = 0; i < res.length; i++) {\n            res[i] = node.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
        linkedlist_deque.cs
        /* Узел двусвязного списка */\nclass ListNode(int val) {\n    public int val = val;       // Значение узла\n    public ListNode? next = null; // Ссылка на узел-преемник\n    public ListNode? prev = null; // Ссылка на узел-предшественник\n}\n\n/* Двусторонняя очередь на основе двусвязного списка */\nclass LinkedListDeque {\n    ListNode? front, rear; // Головной узел front, хвостовой узел rear\n    int queSize = 0;      // Длина двусторонней очереди\n\n    public LinkedListDeque() {\n        front = null;\n        rear = null;\n    }\n\n    /* Получение длины двусторонней очереди */\n    public int Size() {\n        return queSize;\n    }\n\n    /* Проверка, пуста ли двусторонняя очередь */\n    public bool IsEmpty() {\n        return Size() == 0;\n    }\n\n    /* Операция добавления в очередь */\n    void Push(int num, bool isFront) {\n        ListNode node = new(num);\n        // Если связный список пуст, сделать так, чтобы и front, и rear указывали на node\n        if (IsEmpty()) {\n            front = node;\n            rear = node;\n        }\n        // Операция добавления в голову очереди\n        else if (isFront) {\n            // Добавить node в голову списка\n            front!.prev = node;\n            node.next = front;\n            front = node; // Обновить головной узел\n        }\n        // Операция добавления в хвост очереди\n        else {\n            // Добавить node в хвост списка\n            rear!.next = node;\n            node.prev = rear;\n            rear = node;  // Обновить хвостовой узел\n        }\n\n        queSize++; // Обновить длину очереди\n    }\n\n    /* Добавление в голову очереди */\n    public void PushFirst(int num) {\n        Push(num, true);\n    }\n\n    /* Добавление в хвост очереди */\n    public void PushLast(int num) {\n        Push(num, false);\n    }\n\n    /* Операция извлечения из очереди */\n    int? Pop(bool isFront) {\n        if (IsEmpty())\n            throw new Exception();\n        int? val;\n        // Операция извлечения из головы очереди\n        if (isFront) {\n            val = front?.val; // Временно сохранить значение головного узла\n            // Удалить головной узел\n            ListNode? fNext = front?.next;\n            if (fNext != null) {\n                fNext.prev = null;\n                front!.next = null;\n            }\n            front = fNext;   // Обновить головной узел\n        }\n        // Операция извлечения из хвоста очереди\n        else {\n            val = rear?.val;  // Временно сохранить значение хвостового узла\n            // Удалить хвостовой узел\n            ListNode? rPrev = rear?.prev;\n            if (rPrev != null) {\n                rPrev.next = null;\n                rear!.prev = null;\n            }\n            rear = rPrev;    // Обновить хвостовой узел\n        }\n\n        queSize--; // Обновить длину очереди\n        return val;\n    }\n\n    /* Извлечение из головы очереди */\n    public int? PopFirst() {\n        return Pop(true);\n    }\n\n    /* Извлечение из хвоста очереди */\n    public int? PopLast() {\n        return Pop(false);\n    }\n\n    /* Доступ к элементу в начале очереди */\n    public int? PeekFirst() {\n        if (IsEmpty())\n            throw new Exception();\n        return front?.val;\n    }\n\n    /* Доступ к элементу в конце очереди */\n    public int? PeekLast() {\n        if (IsEmpty())\n            throw new Exception();\n        return rear?.val;\n    }\n\n    /* Вернуть массив для вывода */\n    public int?[] ToArray() {\n        ListNode? node = front;\n        int?[] res = new int?[Size()];\n        for (int i = 0; i < res.Length; i++) {\n            res[i] = node?.val;\n            node = node?.next;\n        }\n\n        return res;\n    }\n}\n
        linkedlist_deque.go
        /* Двусторонняя очередь на основе двусвязного списка */\ntype linkedListDeque struct {\n    // Использовать встроенный пакет list\n    data *list.List\n}\n\n/* Инициализировать двустороннюю очередь */\nfunc newLinkedListDeque() *linkedListDeque {\n    return &linkedListDeque{\n        data: list.New(),\n    }\n}\n\n/* Поместить элемент в голову очереди */\nfunc (s *linkedListDeque) pushFirst(value any) {\n    s.data.PushFront(value)\n}\n\n/* Поместить элемент в хвост очереди */\nfunc (s *linkedListDeque) pushLast(value any) {\n    s.data.PushBack(value)\n}\n\n/* Извлечь элемент из головы очереди */\nfunc (s *linkedListDeque) popFirst() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Front()\n    s.data.Remove(e)\n    return e.Value\n}\n\n/* Извлечь элемент из хвоста очереди */\nfunc (s *linkedListDeque) popLast() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Back()\n    s.data.Remove(e)\n    return e.Value\n}\n\n/* Доступ к элементу в начале очереди */\nfunc (s *linkedListDeque) peekFirst() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Front()\n    return e.Value\n}\n\n/* Доступ к элементу в конце очереди */\nfunc (s *linkedListDeque) peekLast() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Back()\n    return e.Value\n}\n\n/* Получение длины очереди */\nfunc (s *linkedListDeque) size() int {\n    return s.data.Len()\n}\n\n/* Проверка, пуста ли очередь */\nfunc (s *linkedListDeque) isEmpty() bool {\n    return s.data.Len() == 0\n}\n\n/* Получить List для вывода */\nfunc (s *linkedListDeque) toList() *list.List {\n    return s.data\n}\n
        linkedlist_deque.swift
        /* Узел двусвязного списка */\nclass ListNode {\n    var val: Int // Значение узла\n    var next: ListNode? // Ссылка на узел-преемник\n    weak var prev: ListNode? // Ссылка на узел-предшественник\n\n    init(val: Int) {\n        self.val = val\n    }\n}\n\n/* Двусторонняя очередь на основе двусвязного списка */\nclass LinkedListDeque {\n    private var front: ListNode? // Головной узел front\n    private var rear: ListNode? // Хвостовой узел rear\n    private var _size: Int // Длина двусторонней очереди\n\n    init() {\n        _size = 0\n    }\n\n    /* Получение длины двусторонней очереди */\n    func size() -> Int {\n        _size\n    }\n\n    /* Проверка, пуста ли двусторонняя очередь */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* Операция добавления в очередь */\n    private func push(num: Int, isFront: Bool) {\n        let node = ListNode(val: num)\n        // Если связный список пуст, сделать так, чтобы и front, и rear указывали на node\n        if isEmpty() {\n            front = node\n            rear = node\n        }\n        // Операция добавления в голову очереди\n        else if isFront {\n            // Добавить node в голову списка\n            front?.prev = node\n            node.next = front\n            front = node // Обновить головной узел\n        }\n        // Операция добавления в хвост очереди\n        else {\n            // Добавить node в хвост списка\n            rear?.next = node\n            node.prev = rear\n            rear = node // Обновить хвостовой узел\n        }\n        _size += 1 // Обновить длину очереди\n    }\n\n    /* Добавление в голову очереди */\n    func pushFirst(num: Int) {\n        push(num: num, isFront: true)\n    }\n\n    /* Добавление в хвост очереди */\n    func pushLast(num: Int) {\n        push(num: num, isFront: false)\n    }\n\n    /* Операция извлечения из очереди */\n    private func pop(isFront: Bool) -> Int {\n        if isEmpty() {\n            fatalError(\"двусторонняя очередь пуста\")\n        }\n        let val: Int\n        // Операция извлечения из головы очереди\n        if isFront {\n            val = front!.val // Временно сохранить значение головного узла\n            // Удалить головной узел\n            let fNext = front?.next\n            if fNext != nil {\n                fNext?.prev = nil\n                front?.next = nil\n            }\n            front = fNext // Обновить головной узел\n        }\n        // Операция извлечения из хвоста очереди\n        else {\n            val = rear!.val // Временно сохранить значение хвостового узла\n            // Удалить хвостовой узел\n            let rPrev = rear?.prev\n            if rPrev != nil {\n                rPrev?.next = nil\n                rear?.prev = nil\n            }\n            rear = rPrev // Обновить хвостовой узел\n        }\n        _size -= 1 // Обновить длину очереди\n        return val\n    }\n\n    /* Извлечение из головы очереди */\n    func popFirst() -> Int {\n        pop(isFront: true)\n    }\n\n    /* Извлечение из хвоста очереди */\n    func popLast() -> Int {\n        pop(isFront: false)\n    }\n\n    /* Доступ к элементу в начале очереди */\n    func peekFirst() -> Int {\n        if isEmpty() {\n            fatalError(\"двусторонняя очередь пуста\")\n        }\n        return front!.val\n    }\n\n    /* Доступ к элементу в конце очереди */\n    func peekLast() -> Int {\n        if isEmpty() {\n            fatalError(\"двусторонняя очередь пуста\")\n        }\n        return rear!.val\n    }\n\n    /* Вернуть массив для вывода */\n    func toArray() -> [Int] {\n        var node = front\n        var res = Array(repeating: 0, count: size())\n        for i in res.indices {\n            res[i] = node!.val\n            node = node?.next\n        }\n        return res\n    }\n}\n
        linkedlist_deque.js
        /* Узел двусвязного списка */\nclass ListNode {\n    prev; // Ссылка на узел-предшественник (указатель)\n    next; // Ссылка на узел-преемник (указатель)\n    val; // Значение узла\n\n    constructor(val) {\n        this.val = val;\n        this.next = null;\n        this.prev = null;\n    }\n}\n\n/* Двусторонняя очередь на основе двусвязного списка */\nclass LinkedListDeque {\n    #front; // Головной узел front\n    #rear; // Хвостовой узел rear\n    #queSize; // Длина двусторонней очереди\n\n    constructor() {\n        this.#front = null;\n        this.#rear = null;\n        this.#queSize = 0;\n    }\n\n    /* Операция добавления в хвост очереди */\n    pushLast(val) {\n        const node = new ListNode(val);\n        // Если связный список пуст, сделать так, чтобы и front, и rear указывали на node\n        if (this.#queSize === 0) {\n            this.#front = node;\n            this.#rear = node;\n        } else {\n            // Добавить node в хвост списка\n            this.#rear.next = node;\n            node.prev = this.#rear;\n            this.#rear = node; // Обновить хвостовой узел\n        }\n        this.#queSize++;\n    }\n\n    /* Операция добавления в голову очереди */\n    pushFirst(val) {\n        const node = new ListNode(val);\n        // Если связный список пуст, сделать так, чтобы и front, и rear указывали на node\n        if (this.#queSize === 0) {\n            this.#front = node;\n            this.#rear = node;\n        } else {\n            // Добавить node в голову списка\n            this.#front.prev = node;\n            node.next = this.#front;\n            this.#front = node; // Обновить головной узел\n        }\n        this.#queSize++;\n    }\n\n    /* Операция извлечения из хвоста очереди */\n    popLast() {\n        if (this.#queSize === 0) {\n            return null;\n        }\n        const value = this.#rear.val; // Сохранить значение хвостового узла\n        // Удалить хвостовой узел\n        let temp = this.#rear.prev;\n        if (temp !== null) {\n            temp.next = null;\n            this.#rear.prev = null;\n        }\n        this.#rear = temp; // Обновить хвостовой узел\n        this.#queSize--;\n        return value;\n    }\n\n    /* Операция извлечения из головы очереди */\n    popFirst() {\n        if (this.#queSize === 0) {\n            return null;\n        }\n        const value = this.#front.val; // Сохранить значение хвостового узла\n        // Удалить головной узел\n        let temp = this.#front.next;\n        if (temp !== null) {\n            temp.prev = null;\n            this.#front.next = null;\n        }\n        this.#front = temp; // Обновить головной узел\n        this.#queSize--;\n        return value;\n    }\n\n    /* Доступ к элементу в конце очереди */\n    peekLast() {\n        return this.#queSize === 0 ? null : this.#rear.val;\n    }\n\n    /* Доступ к элементу в начале очереди */\n    peekFirst() {\n        return this.#queSize === 0 ? null : this.#front.val;\n    }\n\n    /* Получение длины двусторонней очереди */\n    size() {\n        return this.#queSize;\n    }\n\n    /* Проверка, пуста ли двусторонняя очередь */\n    isEmpty() {\n        return this.#queSize === 0;\n    }\n\n    /* Вывести двустороннюю очередь */\n    print() {\n        const arr = [];\n        let temp = this.#front;\n        while (temp !== null) {\n            arr.push(temp.val);\n            temp = temp.next;\n        }\n        console.log('[' + arr.join(', ') + ']');\n    }\n}\n
        linkedlist_deque.ts
        /* Узел двусвязного списка */\nclass ListNode {\n    prev: ListNode; // Ссылка на узел-предшественник (указатель)\n    next: ListNode; // Ссылка на узел-преемник (указатель)\n    val: number; // Значение узла\n\n    constructor(val: number) {\n        this.val = val;\n        this.next = null;\n        this.prev = null;\n    }\n}\n\n/* Двусторонняя очередь на основе двусвязного списка */\nclass LinkedListDeque {\n    private front: ListNode; // Головной узел front\n    private rear: ListNode; // Хвостовой узел rear\n    private queSize: number; // Длина двусторонней очереди\n\n    constructor() {\n        this.front = null;\n        this.rear = null;\n        this.queSize = 0;\n    }\n\n    /* Операция добавления в хвост очереди */\n    pushLast(val: number): void {\n        const node: ListNode = new ListNode(val);\n        // Если связный список пуст, сделать так, чтобы и front, и rear указывали на node\n        if (this.queSize === 0) {\n            this.front = node;\n            this.rear = node;\n        } else {\n            // Добавить node в хвост списка\n            this.rear.next = node;\n            node.prev = this.rear;\n            this.rear = node; // Обновить хвостовой узел\n        }\n        this.queSize++;\n    }\n\n    /* Операция добавления в голову очереди */\n    pushFirst(val: number): void {\n        const node: ListNode = new ListNode(val);\n        // Если связный список пуст, сделать так, чтобы и front, и rear указывали на node\n        if (this.queSize === 0) {\n            this.front = node;\n            this.rear = node;\n        } else {\n            // Добавить node в голову списка\n            this.front.prev = node;\n            node.next = this.front;\n            this.front = node; // Обновить головной узел\n        }\n        this.queSize++;\n    }\n\n    /* Операция извлечения из хвоста очереди */\n    popLast(): number {\n        if (this.queSize === 0) {\n            return null;\n        }\n        const value: number = this.rear.val; // Сохранить значение хвостового узла\n        // Удалить хвостовой узел\n        let temp: ListNode = this.rear.prev;\n        if (temp !== null) {\n            temp.next = null;\n            this.rear.prev = null;\n        }\n        this.rear = temp; // Обновить хвостовой узел\n        this.queSize--;\n        return value;\n    }\n\n    /* Операция извлечения из головы очереди */\n    popFirst(): number {\n        if (this.queSize === 0) {\n            return null;\n        }\n        const value: number = this.front.val; // Сохранить значение хвостового узла\n        // Удалить головной узел\n        let temp: ListNode = this.front.next;\n        if (temp !== null) {\n            temp.prev = null;\n            this.front.next = null;\n        }\n        this.front = temp; // Обновить головной узел\n        this.queSize--;\n        return value;\n    }\n\n    /* Доступ к элементу в конце очереди */\n    peekLast(): number {\n        return this.queSize === 0 ? null : this.rear.val;\n    }\n\n    /* Доступ к элементу в начале очереди */\n    peekFirst(): number {\n        return this.queSize === 0 ? null : this.front.val;\n    }\n\n    /* Получение длины двусторонней очереди */\n    size(): number {\n        return this.queSize;\n    }\n\n    /* Проверка, пуста ли двусторонняя очередь */\n    isEmpty(): boolean {\n        return this.queSize === 0;\n    }\n\n    /* Вывести двустороннюю очередь */\n    print(): void {\n        const arr: number[] = [];\n        let temp: ListNode = this.front;\n        while (temp !== null) {\n            arr.push(temp.val);\n            temp = temp.next;\n        }\n        console.log('[' + arr.join(', ') + ']');\n    }\n}\n
        linkedlist_deque.dart
        /* Узел двусвязного списка */\nclass ListNode {\n  int val; // Значение узла\n  ListNode? next; // Ссылка на узел-преемник\n  ListNode? prev; // Ссылка на узел-предшественник\n\n  ListNode(this.val, {this.next, this.prev});\n}\n\n/* Двусторонняя очередь на основе двусвязного списка */\nclass LinkedListDeque {\n  late ListNode? _front; // Головной узел _front\n  late ListNode? _rear; // Хвостовой узел _rear\n  int _queSize = 0; // Длина двусторонней очереди\n\n  LinkedListDeque() {\n    this._front = null;\n    this._rear = null;\n  }\n\n  /* Получить длину двусторонней очереди */\n  int size() {\n    return this._queSize;\n  }\n\n  /* Проверка, пуста ли двусторонняя очередь */\n  bool isEmpty() {\n    return size() == 0;\n  }\n\n  /* Операция добавления в очередь */\n  void push(int _num, bool isFront) {\n    final ListNode node = ListNode(_num);\n    if (isEmpty()) {\n      // Если связный список пуст, пусть _front и _rear оба указывают на node\n      _front = _rear = node;\n    } else if (isFront) {\n      // Операция добавления в голову очереди\n      // Добавить node в начало связного списка\n      _front!.prev = node;\n      node.next = _front;\n      _front = node; // Обновить головной узел\n    } else {\n      // Операция добавления в хвост очереди\n      // Добавить node в конец связного списка\n      _rear!.next = node;\n      node.prev = _rear;\n      _rear = node; // Обновить хвостовой узел\n    }\n    _queSize++; // Обновить длину очереди\n  }\n\n  /* Добавление в голову очереди */\n  void pushFirst(int _num) {\n    push(_num, true);\n  }\n\n  /* Добавление в хвост очереди */\n  void pushLast(int _num) {\n    push(_num, false);\n  }\n\n  /* Операция извлечения из очереди */\n  int? pop(bool isFront) {\n    // Если очередь пуста, сразу вернуть null\n    if (isEmpty()) {\n      return null;\n    }\n    final int val;\n    if (isFront) {\n      // Операция извлечения из головы очереди\n      val = _front!.val; // Временно сохранить значение головного узла\n      // Удалить головной узел\n      ListNode? fNext = _front!.next;\n      if (fNext != null) {\n        fNext.prev = null;\n        _front!.next = null;\n      }\n      _front = fNext; // Обновить головной узел\n    } else {\n      // Операция извлечения из хвоста очереди\n      val = _rear!.val; // Временно сохранить значение хвостового узла\n      // Удалить хвостовой узел\n      ListNode? rPrev = _rear!.prev;\n      if (rPrev != null) {\n        rPrev.next = null;\n        _rear!.prev = null;\n      }\n      _rear = rPrev; // Обновить хвостовой узел\n    }\n    _queSize--; // Обновить длину очереди\n    return val;\n  }\n\n  /* Извлечение из головы очереди */\n  int? popFirst() {\n    return pop(true);\n  }\n\n  /* Извлечение из хвоста очереди */\n  int? popLast() {\n    return pop(false);\n  }\n\n  /* Доступ к элементу в начале очереди */\n  int? peekFirst() {\n    return _front?.val;\n  }\n\n  /* Доступ к элементу в конце очереди */\n  int? peekLast() {\n    return _rear?.val;\n  }\n\n  /* Вернуть массив для вывода */\n  List<int> toArray() {\n    ListNode? node = _front;\n    final List<int> res = [];\n    for (int i = 0; i < _queSize; i++) {\n      res.add(node!.val);\n      node = node.next;\n    }\n    return res;\n  }\n}\n
        linkedlist_deque.rs
        /* Узел двусвязного списка */\npub struct ListNode<T> {\n    pub val: T,                                 // Значение узла\n    pub next: Option<Rc<RefCell<ListNode<T>>>>, // Указатель на узел-преемник\n    pub prev: Option<Rc<RefCell<ListNode<T>>>>, // Указатель на узел-предшественник\n}\n\nimpl<T> ListNode<T> {\n    pub fn new(val: T) -> Rc<RefCell<ListNode<T>>> {\n        Rc::new(RefCell::new(ListNode {\n            val,\n            next: None,\n            prev: None,\n        }))\n    }\n}\n\n/* Двусторонняя очередь на основе двусвязного списка */\n#[allow(dead_code)]\npub struct LinkedListDeque<T> {\n    front: Option<Rc<RefCell<ListNode<T>>>>, // Головной узел front\n    rear: Option<Rc<RefCell<ListNode<T>>>>,  // Хвостовой узел rear\n    que_size: usize,                         // Длина двусторонней очереди\n}\n\nimpl<T: Copy> LinkedListDeque<T> {\n    pub fn new() -> Self {\n        Self {\n            front: None,\n            rear: None,\n            que_size: 0,\n        }\n    }\n\n    /* Получение длины двусторонней очереди */\n    pub fn size(&self) -> usize {\n        return self.que_size;\n    }\n\n    /* Проверка, пуста ли двусторонняя очередь */\n    pub fn is_empty(&self) -> bool {\n        return self.que_size == 0;\n    }\n\n    /* Операция добавления в очередь */\n    fn push(&mut self, num: T, is_front: bool) {\n        let node = ListNode::new(num);\n        // Операция добавления в голову очереди\n        if is_front {\n            match self.front.take() {\n                // Если связный список пуст, сделать так, чтобы и front, и rear указывали на node\n                None => {\n                    self.rear = Some(node.clone());\n                    self.front = Some(node);\n                }\n                // Добавить node в голову списка\n                Some(old_front) => {\n                    old_front.borrow_mut().prev = Some(node.clone());\n                    node.borrow_mut().next = Some(old_front);\n                    self.front = Some(node); // Обновить головной узел\n                }\n            }\n        }\n        // Операция добавления в хвост очереди\n        else {\n            match self.rear.take() {\n                // Если связный список пуст, сделать так, чтобы и front, и rear указывали на node\n                None => {\n                    self.front = Some(node.clone());\n                    self.rear = Some(node);\n                }\n                // Добавить node в хвост списка\n                Some(old_rear) => {\n                    old_rear.borrow_mut().next = Some(node.clone());\n                    node.borrow_mut().prev = Some(old_rear);\n                    self.rear = Some(node); // Обновить хвостовой узел\n                }\n            }\n        }\n        self.que_size += 1; // Обновить длину очереди\n    }\n\n    /* Добавление в голову очереди */\n    pub fn push_first(&mut self, num: T) {\n        self.push(num, true);\n    }\n\n    /* Добавление в хвост очереди */\n    pub fn push_last(&mut self, num: T) {\n        self.push(num, false);\n    }\n\n    /* Операция извлечения из очереди */\n    fn pop(&mut self, is_front: bool) -> Option<T> {\n        // Если очередь пуста, сразу вернуть None\n        if self.is_empty() {\n            return None;\n        };\n        // Операция извлечения из головы очереди\n        if is_front {\n            self.front.take().map(|old_front| {\n                match old_front.borrow_mut().next.take() {\n                    Some(new_front) => {\n                        new_front.borrow_mut().prev.take();\n                        self.front = Some(new_front); // Обновить головной узел\n                    }\n                    None => {\n                        self.rear.take();\n                    }\n                }\n                self.que_size -= 1; // Обновить длину очереди\n                old_front.borrow().val\n            })\n        }\n        // Операция извлечения из хвоста очереди\n        else {\n            self.rear.take().map(|old_rear| {\n                match old_rear.borrow_mut().prev.take() {\n                    Some(new_rear) => {\n                        new_rear.borrow_mut().next.take();\n                        self.rear = Some(new_rear); // Обновить хвостовой узел\n                    }\n                    None => {\n                        self.front.take();\n                    }\n                }\n                self.que_size -= 1; // Обновить длину очереди\n                old_rear.borrow().val\n            })\n        }\n    }\n\n    /* Извлечение из головы очереди */\n    pub fn pop_first(&mut self) -> Option<T> {\n        return self.pop(true);\n    }\n\n    /* Извлечение из хвоста очереди */\n    pub fn pop_last(&mut self) -> Option<T> {\n        return self.pop(false);\n    }\n\n    /* Доступ к элементу в начале очереди */\n    pub fn peek_first(&self) -> Option<&Rc<RefCell<ListNode<T>>>> {\n        self.front.as_ref()\n    }\n\n    /* Доступ к элементу в конце очереди */\n    pub fn peek_last(&self) -> Option<&Rc<RefCell<ListNode<T>>>> {\n        self.rear.as_ref()\n    }\n\n    /* Вернуть массив для вывода */\n    pub fn to_array(&self, head: Option<&Rc<RefCell<ListNode<T>>>>) -> Vec<T> {\n        let mut res: Vec<T> = Vec::new();\n        fn recur<T: Copy>(cur: Option<&Rc<RefCell<ListNode<T>>>>, res: &mut Vec<T>) {\n            if let Some(cur) = cur {\n                res.push(cur.borrow().val);\n                recur(cur.borrow().next.as_ref(), res);\n            }\n        }\n\n        recur(head, &mut res);\n        res\n    }\n}\n
        linkedlist_deque.c
        /* Узел двусвязного списка */\ntypedef struct DoublyListNode {\n    int val;                     // Значение узла\n    struct DoublyListNode *next; // Узел-преемник\n    struct DoublyListNode *prev; // Узел-предшественник\n} DoublyListNode;\n\n/* Конструктор */\nDoublyListNode *newDoublyListNode(int num) {\n    DoublyListNode *new = (DoublyListNode *)malloc(sizeof(DoublyListNode));\n    new->val = num;\n    new->next = NULL;\n    new->prev = NULL;\n    return new;\n}\n\n/* Деструктор */\nvoid delDoublyListNode(DoublyListNode *node) {\n    free(node);\n}\n\n/* Двусторонняя очередь на основе двусвязного списка */\ntypedef struct {\n    DoublyListNode *front, *rear; // Головной узел front, хвостовой узел rear\n    int queSize;                  // Длина двусторонней очереди\n} LinkedListDeque;\n\n/* Конструктор */\nLinkedListDeque *newLinkedListDeque() {\n    LinkedListDeque *deque = (LinkedListDeque *)malloc(sizeof(LinkedListDeque));\n    deque->front = NULL;\n    deque->rear = NULL;\n    deque->queSize = 0;\n    return deque;\n}\n\n/* Деструктор */\nvoid delLinkedListdeque(LinkedListDeque *deque) {\n    // Освободить все узлы\n    for (int i = 0; i < deque->queSize && deque->front != NULL; i++) {\n        DoublyListNode *tmp = deque->front;\n        deque->front = deque->front->next;\n        free(tmp);\n    }\n    // Освободить структуру deque\n    free(deque);\n}\n\n/* Получение длины очереди */\nint size(LinkedListDeque *deque) {\n    return deque->queSize;\n}\n\n/* Проверка, пуста ли очередь */\nbool empty(LinkedListDeque *deque) {\n    return (size(deque) == 0);\n}\n\n/* Поместить в очередь */\nvoid push(LinkedListDeque *deque, int num, bool isFront) {\n    DoublyListNode *node = newDoublyListNode(num);\n    // Если связный список пуст, пусть front и rear оба указывают на node\n    if (empty(deque)) {\n        deque->front = deque->rear = node;\n    }\n    // Операция добавления в голову очереди\n    else if (isFront) {\n        // Добавить node в голову списка\n        deque->front->prev = node;\n        node->next = deque->front;\n        deque->front = node; // Обновить головной узел\n    }\n    // Операция добавления в хвост очереди\n    else {\n        // Добавить node в хвост списка\n        deque->rear->next = node;\n        node->prev = deque->rear;\n        deque->rear = node;\n    }\n    deque->queSize++; // Обновить длину очереди\n}\n\n/* Добавление в голову очереди */\nvoid pushFirst(LinkedListDeque *deque, int num) {\n    push(deque, num, true);\n}\n\n/* Добавление в хвост очереди */\nvoid pushLast(LinkedListDeque *deque, int num) {\n    push(deque, num, false);\n}\n\n/* Доступ к элементу в начале очереди */\nint peekFirst(LinkedListDeque *deque) {\n    assert(size(deque) && deque->front);\n    return deque->front->val;\n}\n\n/* Доступ к элементу в конце очереди */\nint peekLast(LinkedListDeque *deque) {\n    assert(size(deque) && deque->rear);\n    return deque->rear->val;\n}\n\n/* Извлечь из очереди */\nint pop(LinkedListDeque *deque, bool isFront) {\n    if (empty(deque))\n        return -1;\n    int val;\n    // Операция извлечения из головы очереди\n    if (isFront) {\n        val = peekFirst(deque); // Временно сохранить значение головного узла\n        DoublyListNode *fNext = deque->front->next;\n        if (fNext) {\n            fNext->prev = NULL;\n            deque->front->next = NULL;\n        }\n        delDoublyListNode(deque->front);\n        deque->front = fNext; // Обновить головной узел\n    }\n    // Операция извлечения из хвоста очереди\n    else {\n        val = peekLast(deque); // Временно сохранить значение хвостового узла\n        DoublyListNode *rPrev = deque->rear->prev;\n        if (rPrev) {\n            rPrev->next = NULL;\n            deque->rear->prev = NULL;\n        }\n        delDoublyListNode(deque->rear);\n        deque->rear = rPrev; // Обновить хвостовой узел\n    }\n    deque->queSize--; // Обновить длину очереди\n    return val;\n}\n\n/* Извлечение из головы очереди */\nint popFirst(LinkedListDeque *deque) {\n    return pop(deque, true);\n}\n\n/* Извлечение из хвоста очереди */\nint popLast(LinkedListDeque *deque) {\n    return pop(deque, false);\n}\n\n/* Вывести очередь */\nvoid printLinkedListDeque(LinkedListDeque *deque) {\n    int *arr = malloc(sizeof(int) * deque->queSize);\n    // Скопировать данные связного списка в массив\n    int i;\n    DoublyListNode *node;\n    for (i = 0, node = deque->front; i < deque->queSize; i++) {\n        arr[i] = node->val;\n        node = node->next;\n    }\n    printArray(arr, deque->queSize);\n    free(arr);\n}\n
        linkedlist_deque.kt
        /* Узел двусвязного списка */\nclass ListNode(var _val: Int) {\n    // Значение узла\n    var next: ListNode? = null // Ссылка на узел-преемник\n    var prev: ListNode? = null // Ссылка на узел-предшественник\n}\n\n/* Двусторонняя очередь на основе двусвязного списка */\nclass LinkedListDeque {\n    private var front: ListNode? = null // Головной узел front\n    private var rear: ListNode? = null // Хвостовой узел rear\n    private var queSize: Int = 0 // Длина двусторонней очереди\n\n    /* Получение длины двусторонней очереди */\n    fun size(): Int {\n        return queSize\n    }\n\n    /* Проверка, пуста ли двусторонняя очередь */\n    fun isEmpty(): Boolean {\n        return size() == 0\n    }\n\n    /* Операция добавления в очередь */\n    fun push(num: Int, isFront: Boolean) {\n        val node = ListNode(num)\n        // Если связный список пуст, сделать так, чтобы и front, и rear указывали на node\n        if (isEmpty()) {\n            rear = node\n            front = rear\n            // Операция добавления в голову очереди\n        } else if (isFront) {\n            // Добавить node в голову списка\n            front?.prev = node\n            node.next = front\n            front = node // Обновить головной узел\n            // Операция добавления в хвост очереди\n        } else {\n            // Добавить node в хвост списка\n            rear?.next = node\n            node.prev = rear\n            rear = node // Обновить хвостовой узел\n        }\n        queSize++ // Обновить длину очереди\n    }\n\n    /* Добавление в голову очереди */\n    fun pushFirst(num: Int) {\n        push(num, true)\n    }\n\n    /* Добавление в хвост очереди */\n    fun pushLast(num: Int) {\n        push(num, false)\n    }\n\n    /* Операция извлечения из очереди */\n    fun pop(isFront: Boolean): Int {\n        if (isEmpty()) \n            throw IndexOutOfBoundsException()\n        val _val: Int\n        // Операция извлечения из головы очереди\n        if (isFront) {\n            _val = front!!._val // Временно сохранить значение головного узла\n            // Удалить головной узел\n            val fNext = front!!.next\n            if (fNext != null) {\n                fNext.prev = null\n                front!!.next = null\n            }\n            front = fNext // Обновить головной узел\n            // Операция извлечения из хвоста очереди\n        } else {\n            _val = rear!!._val // Временно сохранить значение хвостового узла\n            // Удалить хвостовой узел\n            val rPrev = rear!!.prev\n            if (rPrev != null) {\n                rPrev.next = null\n                rear!!.prev = null\n            }\n            rear = rPrev // Обновить хвостовой узел\n        }\n        queSize-- // Обновить длину очереди\n        return _val\n    }\n\n    /* Извлечение из головы очереди */\n    fun popFirst(): Int {\n        return pop(true)\n    }\n\n    /* Извлечение из хвоста очереди */\n    fun popLast(): Int {\n        return pop(false)\n    }\n\n    /* Доступ к элементу в начале очереди */\n    fun peekFirst(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return front!!._val\n    }\n\n    /* Доступ к элементу в конце очереди */\n    fun peekLast(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return rear!!._val\n    }\n\n    /* Вернуть массив для вывода */\n    fun toArray(): IntArray {\n        var node = front\n        val res = IntArray(size())\n        for (i in res.indices) {\n            res[i] = node!!._val\n            node = node.next\n        }\n        return res\n    }\n}\n
        linkedlist_deque.rb
        =begin\nFile: linkedlist_deque.rb\nCreated Time: 2024-04-06\nAuthor: Xuan Khoa Tu Nguyen (ngxktuzkai2000@gmail.com)\n=end\n\n# ## Узел двусвязного списка\nclass ListNode\n  attr_accessor :val\n  attr_accessor :next # Ссылка на узел-преемник\n  attr_accessor :prev # Ссылка на узел-предшественник\n\n  ### Конструктор ###\n  def initialize(val)\n    @val = val\n  end\nend\n\n### Двусторонняя очередь на основе двусвязного списка ###\nclass LinkedListDeque\n  ### Получение длины двусторонней очереди ###\n  attr_reader :size\n\n  ### Конструктор ###\n  def initialize\n    @front = nil  # Головной узел front\n    @rear = nil   # Хвостовой узел rear\n    @size = 0     # Длина двусторонней очереди\n  end\n\n  ### Проверка, пуста ли двусторонняя очередь ###\n  def is_empty?\n    size.zero?\n  end\n\n  ### Операция добавления в очередь ###\n  def push(num, is_front)\n    node = ListNode.new(num)\n    # Если связный список пуст, пусть front и rear оба указывают на node\n    if is_empty?\n      @front = @rear = node\n    # Операция добавления в голову очереди\n    elsif is_front\n      # Добавить node в голову списка\n      @front.prev = node\n      node.next = @front\n      @front = node # Обновить головной узел\n    # Операция добавления в хвост очереди\n    else\n      # Добавить node в хвост списка\n      @rear.next = node\n      node.prev = @rear\n      @rear = node # Обновить хвостовой узел\n    end\n    @size += 1 # Обновить длину очереди\n  end\n\n  ### Добавление в голову очереди ###\n  def push_first(num)\n    push(num, true)\n  end\n\n  ### Добавление в хвост очереди ###\n  def push_last(num)\n    push(num, false)\n  end\n\n  ### Операция извлечения из очереди ###\n  def pop(is_front)\n    raise IndexError, 'двусторонняя очередь пуста' if is_empty?\n\n    # Операция извлечения из головы очереди\n    if is_front\n      val = @front.val # Временно сохранить значение головного узла\n      # Удалить головной узел\n      fnext = @front.next\n      unless fnext.nil?\n        fnext.prev = nil\n        @front.next = nil\n      end\n      @front = fnext # Обновить головной узел\n    # Операция извлечения из хвоста очереди\n    else\n      val = @rear.val # Временно сохранить значение хвостового узла\n      # Удалить хвостовой узел\n      rprev = @rear.prev\n      unless rprev.nil?\n        rprev.next = nil\n        @rear.prev = nil\n      end\n      @rear = rprev # Обновить хвостовой узел\n    end\n    @size -= 1 # Обновить длину очереди\n\n    val\n  end\n\n  ### Извлечение из головы очереди ###\n  def pop_first\n    pop(true)\n  end\n\n  ### Извлечение из головы очереди ###\n  def pop_last\n    pop(false)\n  end\n\n  ### Доступ к элементу в начале очереди ###\n  def peek_first\n    raise IndexError, 'двусторонняя очередь пуста' if is_empty?\n\n    @front.val\n  end\n\n  ### Доступ к элементу в хвосте очереди ###\n  def peek_last\n    raise IndexError, 'двусторонняя очередь пуста' if is_empty?\n\n    @rear.val\n  end\n\n  ### Вернуть массив для вывода ###\n  def to_array\n    node = @front\n    res = Array.new(size, 0)\n    for i in 0...size\n      res[i] = node.val\n      node = node.next\n    end\n    res\n  end\nend\n
        ","path":["Глава 5. Стек и очередь","5.3   Двусторонняя очередь"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#2","level":3,"title":"2.   Реализация на основе массива","text":"

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

        <1><2><3><4><5>

        Рисунок 5-9   Операции enqueue и dequeue для двусторонней очереди на массиве

        На основе реализации обычной очереди нужно лишь добавить методы добавления в голову очереди и удаления из хвоста:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_deque.py
        class ArrayDeque:\n    \"\"\"Двусторонняя очередь на основе кольцевого массива\"\"\"\n\n    def __init__(self, capacity: int):\n        \"\"\"Конструктор\"\"\"\n        self._nums: list[int] = [0] * capacity\n        self._front: int = 0\n        self._size: int = 0\n\n    def capacity(self) -> int:\n        \"\"\"Получить вместимость двусторонней очереди\"\"\"\n        return len(self._nums)\n\n    def size(self) -> int:\n        \"\"\"Получение длины двусторонней очереди\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"Проверка, пуста ли двусторонняя очередь\"\"\"\n        return self._size == 0\n\n    def index(self, i: int) -> int:\n        \"\"\"Вычислить индекс в кольцевом массиве\"\"\"\n        # С помощью операции взятия по модулю соединить начало и конец массива\n        # Когда i выходит за конец массива, он возвращается в начало\n        # Когда i выходит за начало массива, он возвращается в конец\n        return (i + self.capacity()) % self.capacity()\n\n    def push_first(self, num: int):\n        \"\"\"Добавление в голову очереди\"\"\"\n        if self._size == self.capacity():\n            print(\"Двусторонняя очередь заполнена\")\n            return\n        # Указатель головы сдвигается на одну позицию влево\n        # С помощью операции взятия по модулю front после выхода за начало массива возвращается в хвост\n        self._front = self.index(self._front - 1)\n        # Добавить num в голову очереди\n        self._nums[self._front] = num\n        self._size += 1\n\n    def push_last(self, num: int):\n        \"\"\"Добавление в хвост очереди\"\"\"\n        if self._size == self.capacity():\n            print(\"Двусторонняя очередь заполнена\")\n            return\n        # Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n        rear = self.index(self._front + self._size)\n        # Добавить num в хвост очереди\n        self._nums[rear] = num\n        self._size += 1\n\n    def pop_first(self) -> int:\n        \"\"\"Извлечение из головы очереди\"\"\"\n        num = self.peek_first()\n        # Указатель головы сдвигается на одну позицию назад\n        self._front = self.index(self._front + 1)\n        self._size -= 1\n        return num\n\n    def pop_last(self) -> int:\n        \"\"\"Извлечение из хвоста очереди\"\"\"\n        num = self.peek_last()\n        self._size -= 1\n        return num\n\n    def peek_first(self) -> int:\n        \"\"\"Доступ к элементу в начале очереди\"\"\"\n        if self.is_empty():\n            raise IndexError(\"двусторонняя очередь пуста\")\n        return self._nums[self._front]\n\n    def peek_last(self) -> int:\n        \"\"\"Доступ к элементу в конце очереди\"\"\"\n        if self.is_empty():\n            raise IndexError(\"двусторонняя очередь пуста\")\n        # Вычислить индекс хвостового элемента\n        last = self.index(self._front + self._size - 1)\n        return self._nums[last]\n\n    def to_array(self) -> list[int]:\n        \"\"\"Вернуть массив для вывода\"\"\"\n        # Преобразовывать только элементы списка в пределах фактической длины\n        res = []\n        for i in range(self._size):\n            res.append(self._nums[self.index(self._front + i)])\n        return res\n
        array_deque.cpp
        /* Двусторонняя очередь на основе кольцевого массива */\nclass ArrayDeque {\n  private:\n    vector<int> nums; // Массив для хранения элементов двусторонней очереди\n    int front;        // Указатель head, указывающий на первый элемент очереди\n    int queSize;      // Длина двусторонней очереди\n\n  public:\n    /* Конструктор */\n    ArrayDeque(int capacity) {\n        nums.resize(capacity);\n        front = queSize = 0;\n    }\n\n    /* Получить вместимость двусторонней очереди */\n    int capacity() {\n        return nums.size();\n    }\n\n    /* Получение длины двусторонней очереди */\n    int size() {\n        return queSize;\n    }\n\n    /* Проверка, пуста ли двусторонняя очередь */\n    bool isEmpty() {\n        return queSize == 0;\n    }\n\n    /* Вычислить индекс в кольцевом массиве */\n    int index(int i) {\n        // С помощью операции взятия по модулю соединить начало и конец массива\n        // Когда i выходит за конец массива, он возвращается в начало\n        // Когда i выходит за начало массива, он возвращается в конец\n        return (i + capacity()) % capacity();\n    }\n\n    /* Добавление в голову очереди */\n    void pushFirst(int num) {\n        if (queSize == capacity()) {\n            cout << \"Двусторонняя очередь заполнена\" << endl;\n            return;\n        }\n        // Указатель головы сдвигается на одну позицию влево\n        // С помощью операции взятия по модулю front после выхода за начало массива возвращается в хвост\n        front = index(front - 1);\n        // Добавить num в голову очереди\n        nums[front] = num;\n        queSize++;\n    }\n\n    /* Добавление в хвост очереди */\n    void pushLast(int num) {\n        if (queSize == capacity()) {\n            cout << \"Двусторонняя очередь заполнена\" << endl;\n            return;\n        }\n        // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n        int rear = index(front + queSize);\n        // Добавить num в хвост очереди\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* Извлечение из головы очереди */\n    int popFirst() {\n        int num = peekFirst();\n        // Указатель головы сдвигается на одну позицию назад\n        front = index(front + 1);\n        queSize--;\n        return num;\n    }\n\n    /* Извлечение из хвоста очереди */\n    int popLast() {\n        int num = peekLast();\n        queSize--;\n        return num;\n    }\n\n    /* Доступ к элементу в начале очереди */\n    int peekFirst() {\n        if (isEmpty())\n            throw out_of_range(\"двусторонняя очередь пуста\");\n        return nums[front];\n    }\n\n    /* Доступ к элементу в конце очереди */\n    int peekLast() {\n        if (isEmpty())\n            throw out_of_range(\"двусторонняя очередь пуста\");\n        // Вычислить индекс хвостового элемента\n        int last = index(front + queSize - 1);\n        return nums[last];\n    }\n\n    /* Вернуть массив для вывода */\n    vector<int> toVector() {\n        // Преобразовывать только элементы списка в пределах фактической длины\n        vector<int> res(queSize);\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            res[i] = nums[index(j)];\n        }\n        return res;\n    }\n};\n
        array_deque.java
        /* Двусторонняя очередь на основе кольцевого массива */\nclass ArrayDeque {\n    private int[] nums; // Массив для хранения элементов двусторонней очереди\n    private int front; // Указатель head, указывающий на первый элемент очереди\n    private int queSize; // Длина двусторонней очереди\n\n    /* Конструктор */\n    public ArrayDeque(int capacity) {\n        this.nums = new int[capacity];\n        front = queSize = 0;\n    }\n\n    /* Получить вместимость двусторонней очереди */\n    public int capacity() {\n        return nums.length;\n    }\n\n    /* Получение длины двусторонней очереди */\n    public int size() {\n        return queSize;\n    }\n\n    /* Проверка, пуста ли двусторонняя очередь */\n    public boolean isEmpty() {\n        return queSize == 0;\n    }\n\n    /* Вычислить индекс в кольцевом массиве */\n    private int index(int i) {\n        // С помощью операции взятия по модулю соединить начало и конец массива\n        // Когда i выходит за конец массива, он возвращается в начало\n        // Когда i выходит за начало массива, он возвращается в конец\n        return (i + capacity()) % capacity();\n    }\n\n    /* Добавление в голову очереди */\n    public void pushFirst(int num) {\n        if (queSize == capacity()) {\n            System.out.println(\"Двусторонняя очередь заполнена\");\n            return;\n        }\n        // Указатель головы сдвигается на одну позицию влево\n        // С помощью операции взятия по модулю front после выхода за начало массива возвращается в хвост\n        front = index(front - 1);\n        // Добавить num в голову очереди\n        nums[front] = num;\n        queSize++;\n    }\n\n    /* Добавление в хвост очереди */\n    public void pushLast(int num) {\n        if (queSize == capacity()) {\n            System.out.println(\"Двусторонняя очередь заполнена\");\n            return;\n        }\n        // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n        int rear = index(front + queSize);\n        // Добавить num в хвост очереди\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* Извлечение из головы очереди */\n    public int popFirst() {\n        int num = peekFirst();\n        // Указатель головы сдвигается на одну позицию назад\n        front = index(front + 1);\n        queSize--;\n        return num;\n    }\n\n    /* Извлечение из хвоста очереди */\n    public int popLast() {\n        int num = peekLast();\n        queSize--;\n        return num;\n    }\n\n    /* Доступ к элементу в начале очереди */\n    public int peekFirst() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return nums[front];\n    }\n\n    /* Доступ к элементу в конце очереди */\n    public int peekLast() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        // Вычислить индекс хвостового элемента\n        int last = index(front + queSize - 1);\n        return nums[last];\n    }\n\n    /* Вернуть массив для вывода */\n    public int[] toArray() {\n        // Преобразовывать только элементы списка в пределах фактической длины\n        int[] res = new int[queSize];\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            res[i] = nums[index(j)];\n        }\n        return res;\n    }\n}\n
        array_deque.cs
        /* Двусторонняя очередь на основе кольцевого массива */\nclass ArrayDeque {\n    int[] nums;  // Массив для хранения элементов двусторонней очереди\n    int front;   // Указатель head, указывающий на первый элемент очереди\n    int queSize; // Длина двусторонней очереди\n\n    /* Конструктор */\n    public ArrayDeque(int capacity) {\n        nums = new int[capacity];\n        front = queSize = 0;\n    }\n\n    /* Получить вместимость двусторонней очереди */\n    int Capacity() {\n        return nums.Length;\n    }\n\n    /* Получение длины двусторонней очереди */\n    public int Size() {\n        return queSize;\n    }\n\n    /* Проверка, пуста ли двусторонняя очередь */\n    public bool IsEmpty() {\n        return queSize == 0;\n    }\n\n    /* Вычислить индекс в кольцевом массиве */\n    int Index(int i) {\n        // С помощью операции взятия по модулю соединить начало и конец массива\n        // Когда i выходит за конец массива, он возвращается в начало\n        // Когда i выходит за начало массива, он возвращается в конец\n        return (i + Capacity()) % Capacity();\n    }\n\n    /* Добавление в голову очереди */\n    public void PushFirst(int num) {\n        if (queSize == Capacity()) {\n            Console.WriteLine(\"Двусторонняя очередь заполнена\");\n            return;\n        }\n        // Указатель головы сдвигается на одну позицию влево\n        // С помощью операции взятия по модулю front после выхода за начало массива возвращается в хвост\n        front = Index(front - 1);\n        // Добавить num в голову очереди\n        nums[front] = num;\n        queSize++;\n    }\n\n    /* Добавление в хвост очереди */\n    public void PushLast(int num) {\n        if (queSize == Capacity()) {\n            Console.WriteLine(\"Двусторонняя очередь заполнена\");\n            return;\n        }\n        // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n        int rear = Index(front + queSize);\n        // Добавить num в хвост очереди\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* Извлечение из головы очереди */\n    public int PopFirst() {\n        int num = PeekFirst();\n        // Указатель головы сдвигается на одну позицию назад\n        front = Index(front + 1);\n        queSize--;\n        return num;\n    }\n\n    /* Извлечение из хвоста очереди */\n    public int PopLast() {\n        int num = PeekLast();\n        queSize--;\n        return num;\n    }\n\n    /* Доступ к элементу в начале очереди */\n    public int PeekFirst() {\n        if (IsEmpty()) {\n            throw new InvalidOperationException();\n        }\n        return nums[front];\n    }\n\n    /* Доступ к элементу в конце очереди */\n    public int PeekLast() {\n        if (IsEmpty()) {\n            throw new InvalidOperationException();\n        }\n        // Вычислить индекс хвостового элемента\n        int last = Index(front + queSize - 1);\n        return nums[last];\n    }\n\n    /* Вернуть массив для вывода */\n    public int[] ToArray() {\n        // Преобразовывать только элементы списка в пределах фактической длины\n        int[] res = new int[queSize];\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            res[i] = nums[Index(j)];\n        }\n        return res;\n    }\n}\n
        array_deque.go
        /* Двусторонняя очередь на основе кольцевого массива */\ntype arrayDeque struct {\n    nums        []int // Массив для хранения элементов двусторонней очереди\n    front       int   // Указатель head, указывающий на первый элемент очереди\n    queSize     int   // Длина двусторонней очереди\n    queCapacity int   // Вместимость очереди (то есть максимальное число элементов)\n}\n\n/* Инициализация очереди */\nfunc newArrayDeque(queCapacity int) *arrayDeque {\n    return &arrayDeque{\n        nums:        make([]int, queCapacity),\n        queCapacity: queCapacity,\n        front:       0,\n        queSize:     0,\n    }\n}\n\n/* Получение длины двусторонней очереди */\nfunc (q *arrayDeque) size() int {\n    return q.queSize\n}\n\n/* Проверка, пуста ли двусторонняя очередь */\nfunc (q *arrayDeque) isEmpty() bool {\n    return q.queSize == 0\n}\n\n/* Вычислить индекс в кольцевом массиве */\nfunc (q *arrayDeque) index(i int) int {\n    // С помощью операции взятия по модулю соединить начало и конец массива\n    // Когда i выходит за конец массива, он возвращается в начало\n    // Когда i выходит за начало массива, он возвращается в конец\n    return (i + q.queCapacity) % q.queCapacity\n}\n\n/* Добавление в голову очереди */\nfunc (q *arrayDeque) pushFirst(num int) {\n    if q.queSize == q.queCapacity {\n        fmt.Println(\"Двусторонняя очередь заполнена\")\n        return\n    }\n    // Указатель головы сдвигается на одну позицию влево\n    // С помощью операции взятия по модулю front после выхода за начало массива возвращается в хвост\n    q.front = q.index(q.front - 1)\n    // Добавить num в голову очереди\n    q.nums[q.front] = num\n    q.queSize++\n}\n\n/* Добавление в хвост очереди */\nfunc (q *arrayDeque) pushLast(num int) {\n    if q.queSize == q.queCapacity {\n        fmt.Println(\"Двусторонняя очередь заполнена\")\n        return\n    }\n    // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n    rear := q.index(q.front + q.queSize)\n    // Добавить num в хвост очереди\n    q.nums[rear] = num\n    q.queSize++\n}\n\n/* Извлечение из головы очереди */\nfunc (q *arrayDeque) popFirst() any {\n    num := q.peekFirst()\n    if num == nil {\n        return nil\n    }\n    // Указатель головы сдвигается на одну позицию назад\n    q.front = q.index(q.front + 1)\n    q.queSize--\n    return num\n}\n\n/* Извлечение из хвоста очереди */\nfunc (q *arrayDeque) popLast() any {\n    num := q.peekLast()\n    if num == nil {\n        return nil\n    }\n    q.queSize--\n    return num\n}\n\n/* Доступ к элементу в начале очереди */\nfunc (q *arrayDeque) peekFirst() any {\n    if q.isEmpty() {\n        return nil\n    }\n    return q.nums[q.front]\n}\n\n/* Доступ к элементу в конце очереди */\nfunc (q *arrayDeque) peekLast() any {\n    if q.isEmpty() {\n        return nil\n    }\n    // Вычислить индекс хвостового элемента\n    last := q.index(q.front + q.queSize - 1)\n    return q.nums[last]\n}\n\n/* Получить Slice для вывода */\nfunc (q *arrayDeque) toSlice() []int {\n    // Преобразовывать только элементы списка в пределах фактической длины\n    res := make([]int, q.queSize)\n    for i, j := 0, q.front; i < q.queSize; i++ {\n        res[i] = q.nums[q.index(j)]\n        j++\n    }\n    return res\n}\n
        array_deque.swift
        /* Двусторонняя очередь на основе кольцевого массива */\nclass ArrayDeque {\n    private var nums: [Int] // Массив для хранения элементов двусторонней очереди\n    private var front: Int // Указатель head, указывающий на первый элемент очереди\n    private var _size: Int // Длина двусторонней очереди\n\n    /* Конструктор */\n    init(capacity: Int) {\n        nums = Array(repeating: 0, count: capacity)\n        front = 0\n        _size = 0\n    }\n\n    /* Получить вместимость двусторонней очереди */\n    func capacity() -> Int {\n        nums.count\n    }\n\n    /* Получение длины двусторонней очереди */\n    func size() -> Int {\n        _size\n    }\n\n    /* Проверка, пуста ли двусторонняя очередь */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* Вычислить индекс в кольцевом массиве */\n    private func index(i: Int) -> Int {\n        // С помощью операции взятия по модулю соединить начало и конец массива\n        // Когда i выходит за конец массива, он возвращается в начало\n        // Когда i выходит за начало массива, он возвращается в конец\n        (i + capacity()) % capacity()\n    }\n\n    /* Добавление в голову очереди */\n    func pushFirst(num: Int) {\n        if size() == capacity() {\n            print(\"Двусторонняя очередь заполнена\")\n            return\n        }\n        // Указатель головы сдвигается на одну позицию влево\n        // С помощью операции взятия по модулю front после выхода за начало массива возвращается в хвост\n        front = index(i: front - 1)\n        // Добавить num в голову очереди\n        nums[front] = num\n        _size += 1\n    }\n\n    /* Добавление в хвост очереди */\n    func pushLast(num: Int) {\n        if size() == capacity() {\n            print(\"Двусторонняя очередь заполнена\")\n            return\n        }\n        // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n        let rear = index(i: front + size())\n        // Добавить num в хвост очереди\n        nums[rear] = num\n        _size += 1\n    }\n\n    /* Извлечение из головы очереди */\n    func popFirst() -> Int {\n        let num = peekFirst()\n        // Указатель головы сдвигается на одну позицию назад\n        front = index(i: front + 1)\n        _size -= 1\n        return num\n    }\n\n    /* Извлечение из хвоста очереди */\n    func popLast() -> Int {\n        let num = peekLast()\n        _size -= 1\n        return num\n    }\n\n    /* Доступ к элементу в начале очереди */\n    func peekFirst() -> Int {\n        if isEmpty() {\n            fatalError(\"двусторонняя очередь пуста\")\n        }\n        return nums[front]\n    }\n\n    /* Доступ к элементу в конце очереди */\n    func peekLast() -> Int {\n        if isEmpty() {\n            fatalError(\"двусторонняя очередь пуста\")\n        }\n        // Вычислить индекс хвостового элемента\n        let last = index(i: front + size() - 1)\n        return nums[last]\n    }\n\n    /* Вернуть массив для вывода */\n    func toArray() -> [Int] {\n        // Преобразовывать только элементы списка в пределах фактической длины\n        (front ..< front + size()).map { nums[index(i: $0)] }\n    }\n}\n
        array_deque.js
        /* Двусторонняя очередь на основе кольцевого массива */\nclass ArrayDeque {\n    #nums; // Массив для хранения элементов двусторонней очереди\n    #front; // Указатель head, указывающий на первый элемент очереди\n    #queSize; // Длина двусторонней очереди\n\n    /* Конструктор */\n    constructor(capacity) {\n        this.#nums = new Array(capacity);\n        this.#front = 0;\n        this.#queSize = 0;\n    }\n\n    /* Получить вместимость двусторонней очереди */\n    capacity() {\n        return this.#nums.length;\n    }\n\n    /* Получение длины двусторонней очереди */\n    size() {\n        return this.#queSize;\n    }\n\n    /* Проверка, пуста ли двусторонняя очередь */\n    isEmpty() {\n        return this.#queSize === 0;\n    }\n\n    /* Вычислить индекс в кольцевом массиве */\n    index(i) {\n        // С помощью операции взятия по модулю соединить начало и конец массива\n        // Когда i выходит за конец массива, он возвращается в начало\n        // Когда i выходит за начало массива, он возвращается в конец\n        return (i + this.capacity()) % this.capacity();\n    }\n\n    /* Добавление в голову очереди */\n    pushFirst(num) {\n        if (this.#queSize === this.capacity()) {\n            console.log('Двусторонняя очередь заполнена');\n            return;\n        }\n        // Указатель головы сдвигается на одну позицию влево\n        // С помощью операции взятия по модулю front после выхода за начало массива возвращается в хвост\n        this.#front = this.index(this.#front - 1);\n        // Добавить num в голову очереди\n        this.#nums[this.#front] = num;\n        this.#queSize++;\n    }\n\n    /* Добавление в хвост очереди */\n    pushLast(num) {\n        if (this.#queSize === this.capacity()) {\n            console.log('Двусторонняя очередь заполнена');\n            return;\n        }\n        // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n        const rear = this.index(this.#front + this.#queSize);\n        // Добавить num в хвост очереди\n        this.#nums[rear] = num;\n        this.#queSize++;\n    }\n\n    /* Извлечение из головы очереди */\n    popFirst() {\n        const num = this.peekFirst();\n        // Указатель головы сдвигается на одну позицию назад\n        this.#front = this.index(this.#front + 1);\n        this.#queSize--;\n        return num;\n    }\n\n    /* Извлечение из хвоста очереди */\n    popLast() {\n        const num = this.peekLast();\n        this.#queSize--;\n        return num;\n    }\n\n    /* Доступ к элементу в начале очереди */\n    peekFirst() {\n        if (this.isEmpty()) throw new Error('The Deque Is Empty.');\n        return this.#nums[this.#front];\n    }\n\n    /* Доступ к элементу в конце очереди */\n    peekLast() {\n        if (this.isEmpty()) throw new Error('The Deque Is Empty.');\n        // Вычислить индекс хвостового элемента\n        const last = this.index(this.#front + this.#queSize - 1);\n        return this.#nums[last];\n    }\n\n    /* Вернуть массив для вывода */\n    toArray() {\n        // Преобразовывать только элементы списка в пределах фактической длины\n        const res = [];\n        for (let i = 0, j = this.#front; i < this.#queSize; i++, j++) {\n            res[i] = this.#nums[this.index(j)];\n        }\n        return res;\n    }\n}\n
        array_deque.ts
        /* Двусторонняя очередь на основе кольцевого массива */\nclass ArrayDeque {\n    private nums: number[]; // Массив для хранения элементов двусторонней очереди\n    private front: number; // Указатель head, указывающий на первый элемент очереди\n    private queSize: number; // Длина двусторонней очереди\n\n    /* Конструктор */\n    constructor(capacity: number) {\n        this.nums = new Array(capacity);\n        this.front = 0;\n        this.queSize = 0;\n    }\n\n    /* Получить вместимость двусторонней очереди */\n    capacity(): number {\n        return this.nums.length;\n    }\n\n    /* Получение длины двусторонней очереди */\n    size(): number {\n        return this.queSize;\n    }\n\n    /* Проверка, пуста ли двусторонняя очередь */\n    isEmpty(): boolean {\n        return this.queSize === 0;\n    }\n\n    /* Вычислить индекс в кольцевом массиве */\n    index(i: number): number {\n        // С помощью операции взятия по модулю соединить начало и конец массива\n        // Когда i выходит за конец массива, он возвращается в начало\n        // Когда i выходит за начало массива, он возвращается в конец\n        return (i + this.capacity()) % this.capacity();\n    }\n\n    /* Добавление в голову очереди */\n    pushFirst(num: number): void {\n        if (this.queSize === this.capacity()) {\n            console.log('Двусторонняя очередь заполнена');\n            return;\n        }\n        // Указатель головы сдвигается на одну позицию влево\n        // С помощью операции взятия по модулю front после выхода за начало массива возвращается в хвост\n        this.front = this.index(this.front - 1);\n        // Добавить num в голову очереди\n        this.nums[this.front] = num;\n        this.queSize++;\n    }\n\n    /* Добавление в хвост очереди */\n    pushLast(num: number): void {\n        if (this.queSize === this.capacity()) {\n            console.log('Двусторонняя очередь заполнена');\n            return;\n        }\n        // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n        const rear: number = this.index(this.front + this.queSize);\n        // Добавить num в хвост очереди\n        this.nums[rear] = num;\n        this.queSize++;\n    }\n\n    /* Извлечение из головы очереди */\n    popFirst(): number {\n        const num: number = this.peekFirst();\n        // Указатель головы сдвигается на одну позицию назад\n        this.front = this.index(this.front + 1);\n        this.queSize--;\n        return num;\n    }\n\n    /* Извлечение из хвоста очереди */\n    popLast(): number {\n        const num: number = this.peekLast();\n        this.queSize--;\n        return num;\n    }\n\n    /* Доступ к элементу в начале очереди */\n    peekFirst(): number {\n        if (this.isEmpty()) throw new Error('The Deque Is Empty.');\n        return this.nums[this.front];\n    }\n\n    /* Доступ к элементу в конце очереди */\n    peekLast(): number {\n        if (this.isEmpty()) throw new Error('The Deque Is Empty.');\n        // Вычислить индекс хвостового элемента\n        const last = this.index(this.front + this.queSize - 1);\n        return this.nums[last];\n    }\n\n    /* Вернуть массив для вывода */\n    toArray(): number[] {\n        // Преобразовывать только элементы списка в пределах фактической длины\n        const res: number[] = [];\n        for (let i = 0, j = this.front; i < this.queSize; i++, j++) {\n            res[i] = this.nums[this.index(j)];\n        }\n        return res;\n    }\n}\n
        array_deque.dart
        /* Двусторонняя очередь на основе кольцевого массива */\nclass ArrayDeque {\n  late List<int> _nums; // Массив для хранения элементов двусторонней очереди\n  late int _front; // Указатель head, указывающий на первый элемент очереди\n  late int _queSize; // Длина двусторонней очереди\n\n  /* Конструктор */\n  ArrayDeque(int capacity) {\n    this._nums = List.filled(capacity, 0);\n    this._front = this._queSize = 0;\n  }\n\n  /* Получить вместимость двусторонней очереди */\n  int capacity() {\n    return _nums.length;\n  }\n\n  /* Получение длины двусторонней очереди */\n  int size() {\n    return _queSize;\n  }\n\n  /* Проверка, пуста ли двусторонняя очередь */\n  bool isEmpty() {\n    return _queSize == 0;\n  }\n\n  /* Вычислить индекс в кольцевом массиве */\n  int index(int i) {\n    // С помощью операции взятия по модулю соединить начало и конец массива\n    // Когда i выходит за конец массива, он возвращается в начало\n    // Когда i выходит за начало массива, он возвращается в конец\n    return (i + capacity()) % capacity();\n  }\n\n  /* Добавление в голову очереди */\n  void pushFirst(int _num) {\n    if (_queSize == capacity()) {\n      throw Exception(\"Двусторонняя очередь заполнена\");\n    }\n    // Указатель головы сместить влево на одну позицию\n    // С помощью операции взятия остатка реализовать возврат _front к хвосту после выхода за начало массива\n    _front = index(_front - 1);\n    // Добавить _num в голову очереди\n    _nums[_front] = _num;\n    _queSize++;\n  }\n\n  /* Добавление в хвост очереди */\n  void pushLast(int _num) {\n    if (_queSize == capacity()) {\n      throw Exception(\"Двусторонняя очередь заполнена\");\n    }\n    // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n    int rear = index(_front + _queSize);\n    // Добавить _num в хвост очереди\n    _nums[rear] = _num;\n    _queSize++;\n  }\n\n  /* Извлечение из головы очереди */\n  int popFirst() {\n    int _num = peekFirst();\n    // Указатель головы сместить вправо на одну позицию\n    _front = index(_front + 1);\n    _queSize--;\n    return _num;\n  }\n\n  /* Извлечение из хвоста очереди */\n  int popLast() {\n    int _num = peekLast();\n    _queSize--;\n    return _num;\n  }\n\n  /* Доступ к элементу в начале очереди */\n  int peekFirst() {\n    if (isEmpty()) {\n      throw Exception(\"двусторонняя очередь пуста\");\n    }\n    return _nums[_front];\n  }\n\n  /* Доступ к элементу в конце очереди */\n  int peekLast() {\n    if (isEmpty()) {\n      throw Exception(\"двусторонняя очередь пуста\");\n    }\n    // Вычислить индекс хвостового элемента\n    int last = index(_front + _queSize - 1);\n    return _nums[last];\n  }\n\n  /* Вернуть массив для вывода */\n  List<int> toArray() {\n    // Преобразовывать только элементы списка в пределах фактической длины\n    List<int> res = List.filled(_queSize, 0);\n    for (int i = 0, j = _front; i < _queSize; i++, j++) {\n      res[i] = _nums[index(j)];\n    }\n    return res;\n  }\n}\n
        array_deque.rs
        /* Двусторонняя очередь на основе кольцевого массива */\nstruct ArrayDeque<T> {\n    nums: Vec<T>,    // Массив для хранения элементов двусторонней очереди\n    front: usize,    // Указатель head, указывающий на первый элемент очереди\n    que_size: usize, // Длина двусторонней очереди\n}\n\nimpl<T: Copy + Default> ArrayDeque<T> {\n    /* Конструктор */\n    pub fn new(capacity: usize) -> Self {\n        Self {\n            nums: vec![T::default(); capacity],\n            front: 0,\n            que_size: 0,\n        }\n    }\n\n    /* Получить вместимость двусторонней очереди */\n    pub fn capacity(&self) -> usize {\n        self.nums.len()\n    }\n\n    /* Получение длины двусторонней очереди */\n    pub fn size(&self) -> usize {\n        self.que_size\n    }\n\n    /* Проверка, пуста ли двусторонняя очередь */\n    pub fn is_empty(&self) -> bool {\n        self.que_size == 0\n    }\n\n    /* Вычислить индекс в кольцевом массиве */\n    fn index(&self, i: i32) -> usize {\n        // С помощью операции взятия по модулю соединить начало и конец массива\n        // Когда i выходит за конец массива, он возвращается в начало\n        // Когда i выходит за начало массива, он возвращается в конец\n        ((i + self.capacity() as i32) % self.capacity() as i32) as usize\n    }\n\n    /* Добавление в голову очереди */\n    pub fn push_first(&mut self, num: T) {\n        if self.que_size == self.capacity() {\n            println!(\"Двусторонняя очередь заполнена\");\n            return;\n        }\n        // Указатель головы сдвигается на одну позицию влево\n        // С помощью операции взятия по модулю front после выхода за начало массива возвращается в хвост\n        self.front = self.index(self.front as i32 - 1);\n        // Добавить num в голову очереди\n        self.nums[self.front] = num;\n        self.que_size += 1;\n    }\n\n    /* Добавление в хвост очереди */\n    pub fn push_last(&mut self, num: T) {\n        if self.que_size == self.capacity() {\n            println!(\"Двусторонняя очередь заполнена\");\n            return;\n        }\n        // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n        let rear = self.index(self.front as i32 + self.que_size as i32);\n        // Добавить num в хвост очереди\n        self.nums[rear] = num;\n        self.que_size += 1;\n    }\n\n    /* Извлечение из головы очереди */\n    fn pop_first(&mut self) -> T {\n        let num = self.peek_first();\n        // Указатель головы сдвигается на одну позицию назад\n        self.front = self.index(self.front as i32 + 1);\n        self.que_size -= 1;\n        num\n    }\n\n    /* Извлечение из хвоста очереди */\n    fn pop_last(&mut self) -> T {\n        let num = self.peek_last();\n        self.que_size -= 1;\n        num\n    }\n\n    /* Доступ к элементу в начале очереди */\n    fn peek_first(&self) -> T {\n        if self.is_empty() {\n            panic!(\"двусторонняя очередь пуста\")\n        };\n        self.nums[self.front]\n    }\n\n    /* Доступ к элементу в конце очереди */\n    fn peek_last(&self) -> T {\n        if self.is_empty() {\n            panic!(\"двусторонняя очередь пуста\")\n        };\n        // Вычислить индекс хвостового элемента\n        let last = self.index(self.front as i32 + self.que_size as i32 - 1);\n        self.nums[last]\n    }\n\n    /* Вернуть массив для вывода */\n    fn to_array(&self) -> Vec<T> {\n        // Преобразовывать только элементы списка в пределах фактической длины\n        let mut res = vec![T::default(); self.que_size];\n        let mut j = self.front;\n        for i in 0..self.que_size {\n            res[i] = self.nums[self.index(j as i32)];\n            j += 1;\n        }\n        res\n    }\n}\n
        array_deque.c
        /* Двусторонняя очередь на основе кольцевого массива */\ntypedef struct {\n    int *nums;       // Массив для хранения элементов очереди\n    int front;       // Указатель head, указывающий на первый элемент очереди\n    int queSize;     // Указатель хвоста, указывающий на позицию после хвоста\n    int queCapacity; // Вместимость очереди\n} ArrayDeque;\n\n/* Конструктор */\nArrayDeque *newArrayDeque(int capacity) {\n    ArrayDeque *deque = (ArrayDeque *)malloc(sizeof(ArrayDeque));\n    // Инициализация массива\n    deque->queCapacity = capacity;\n    deque->nums = (int *)malloc(sizeof(int) * deque->queCapacity);\n    deque->front = deque->queSize = 0;\n    return deque;\n}\n\n/* Деструктор */\nvoid delArrayDeque(ArrayDeque *deque) {\n    free(deque->nums);\n    free(deque);\n}\n\n/* Получить вместимость двусторонней очереди */\nint capacity(ArrayDeque *deque) {\n    return deque->queCapacity;\n}\n\n/* Получение длины двусторонней очереди */\nint size(ArrayDeque *deque) {\n    return deque->queSize;\n}\n\n/* Проверка, пуста ли двусторонняя очередь */\nbool empty(ArrayDeque *deque) {\n    return deque->queSize == 0;\n}\n\n/* Вычислить индекс в кольцевом массиве */\nint dequeIndex(ArrayDeque *deque, int i) {\n    // С помощью операции взятия остатка соединить начало и конец массива\n    // Когда i выходит за хвост массива, вернуться к началу\n    // Когда i выходит за голову массива, вернуться к концу\n    return ((i + capacity(deque)) % capacity(deque));\n}\n\n/* Добавление в голову очереди */\nvoid pushFirst(ArrayDeque *deque, int num) {\n    if (deque->queSize == capacity(deque)) {\n        printf(\"Дек заполнен\\r\\n\");\n        return;\n    }\n    // Указатель головы сместить влево на одну позицию\n    // С помощью операции взятия остатка реализовать возврат front к хвосту после выхода за начало массива\n    deque->front = dequeIndex(deque, deque->front - 1);\n    // Добавить num в голову очереди\n    deque->nums[deque->front] = num;\n    deque->queSize++;\n}\n\n/* Добавление в хвост очереди */\nvoid pushLast(ArrayDeque *deque, int num) {\n    if (deque->queSize == capacity(deque)) {\n        printf(\"Дек заполнен\\r\\n\");\n        return;\n    }\n    // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n    int rear = dequeIndex(deque, deque->front + deque->queSize);\n    // Добавить num в хвост очереди\n    deque->nums[rear] = num;\n    deque->queSize++;\n}\n\n/* Доступ к элементу в начале очереди */\nint peekFirst(ArrayDeque *deque) {\n    // Ошибка доступа: двусторонняя очередь пуста\n    assert(empty(deque) == 0);\n    return deque->nums[deque->front];\n}\n\n/* Доступ к элементу в конце очереди */\nint peekLast(ArrayDeque *deque) {\n    // Ошибка доступа: двусторонняя очередь пуста\n    assert(empty(deque) == 0);\n    int last = dequeIndex(deque, deque->front + deque->queSize - 1);\n    return deque->nums[last];\n}\n\n/* Извлечение из головы очереди */\nint popFirst(ArrayDeque *deque) {\n    int num = peekFirst(deque);\n    // Указатель головы сдвигается на одну позицию назад\n    deque->front = dequeIndex(deque, deque->front + 1);\n    deque->queSize--;\n    return num;\n}\n\n/* Извлечение из хвоста очереди */\nint popLast(ArrayDeque *deque) {\n    int num = peekLast(deque);\n    deque->queSize--;\n    return num;\n}\n\n/* Вернуть массив для вывода */\nint *toArray(ArrayDeque *deque, int *queSize) {\n    *queSize = deque->queSize;\n    int *res = (int *)calloc(deque->queSize, sizeof(int));\n    int j = deque->front;\n    for (int i = 0; i < deque->queSize; i++) {\n        res[i] = deque->nums[j % deque->queCapacity];\n        j++;\n    }\n    return res;\n}\n
        array_deque.kt
        /* Конструктор */\nclass ArrayDeque(capacity: Int) {\n    private var nums: IntArray = IntArray(capacity) // Массив для хранения элементов двусторонней очереди\n    private var front: Int = 0 // Указатель head, указывающий на первый элемент очереди\n    private var queSize: Int = 0 // Длина двусторонней очереди\n\n    /* Получить вместимость двусторонней очереди */\n    fun capacity(): Int {\n        return nums.size\n    }\n\n    /* Получение длины двусторонней очереди */\n    fun size(): Int {\n        return queSize\n    }\n\n    /* Проверка, пуста ли двусторонняя очередь */\n    fun isEmpty(): Boolean {\n        return queSize == 0\n    }\n\n    /* Вычислить индекс в кольцевом массиве */\n    private fun index(i: Int): Int {\n        // С помощью операции взятия по модулю соединить начало и конец массива\n        // Когда i выходит за конец массива, он возвращается в начало\n        // Когда i выходит за начало массива, он возвращается в конец\n        return (i + capacity()) % capacity()\n    }\n\n    /* Добавление в голову очереди */\n    fun pushFirst(num: Int) {\n        if (queSize == capacity()) {\n            println(\"Двусторонняя очередь заполнена\")\n            return\n        }\n        // Указатель головы сдвигается на одну позицию влево\n        // С помощью операции взятия по модулю front после выхода за начало массива возвращается в хвост\n        front = index(front - 1)\n        // Добавить num в голову очереди\n        nums[front] = num\n        queSize++\n    }\n\n    /* Добавление в хвост очереди */\n    fun pushLast(num: Int) {\n        if (queSize == capacity()) {\n            println(\"Двусторонняя очередь заполнена\")\n            return\n        }\n        // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n        val rear = index(front + queSize)\n        // Добавить num в хвост очереди\n        nums[rear] = num\n        queSize++\n    }\n\n    /* Извлечение из головы очереди */\n    fun popFirst(): Int {\n        val num = peekFirst()\n        // Указатель головы сдвигается на одну позицию назад\n        front = index(front + 1)\n        queSize--\n        return num\n    }\n\n    /* Извлечение из хвоста очереди */\n    fun popLast(): Int {\n        val num = peekLast()\n        queSize--\n        return num\n    }\n\n    /* Доступ к элементу в начале очереди */\n    fun peekFirst(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return nums[front]\n    }\n\n    /* Доступ к элементу в конце очереди */\n    fun peekLast(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        // Вычислить индекс хвостового элемента\n        val last = index(front + queSize - 1)\n        return nums[last]\n    }\n\n    /* Вернуть массив для вывода */\n    fun toArray(): IntArray {\n        // Преобразовывать только элементы списка в пределах фактической длины\n        val res = IntArray(queSize)\n        var i = 0\n        var j = front\n        while (i < queSize) {\n            res[i] = nums[index(j)]\n            i++\n            j++\n        }\n        return res\n    }\n}\n
        array_deque.rb
        ### Двусторонняя очередь на основе кольцевого массива ###\nclass ArrayDeque\n  ### Получение длины двусторонней очереди ###\n  attr_reader :size\n\n  ### Конструктор ###\n  def initialize(capacity)\n    @nums = Array.new(capacity, 0)\n    @front = 0\n    @size = 0\n  end\n\n  ### Получить вместимость двусторонней очереди ###\n  def capacity\n    @nums.length\n  end\n\n  ### Проверка, пуста ли двусторонняя очередь ###\n  def is_empty?\n    size.zero?\n  end\n\n  ### Добавление в голову очереди ###\n  def push_first(num)\n    if size == capacity\n      puts 'Двусторонняя очередь заполнена'\n      return\n    end\n\n    # Указатель головы сдвигается на одну позицию влево\n    # С помощью операции взятия по модулю front после выхода за начало массива возвращается в хвост\n    @front = index(@front - 1)\n    # Добавить num в голову очереди\n    @nums[@front] = num\n    @size += 1\n  end\n\n  ### Добавление в хвост очереди ###\n  def push_last(num)\n    if size == capacity\n      puts 'Двусторонняя очередь заполнена'\n      return\n    end\n\n    # Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n    rear = index(@front + size)\n    # Добавить num в хвост очереди\n    @nums[rear] = num\n    @size += 1\n  end\n\n  ### Извлечение из головы очереди ###\n  def pop_first\n    num = peek_first\n    # Указатель головы сдвигается на одну позицию назад\n    @front = index(@front + 1)\n    @size -= 1\n    num\n  end\n\n  ### Извлечение из хвоста очереди ###\n  def pop_last\n    num = peek_last\n    @size -= 1\n    num\n  end\n\n  ### Доступ к элементу в начале очереди ###\n  def peek_first\n    raise IndexError, 'двусторонняя очередь пуста' if is_empty?\n\n    @nums[@front]\n  end\n\n  ### Доступ к элементу в хвосте очереди ###\n  def peek_last\n    raise IndexError, 'двусторонняя очередь пуста' if is_empty?\n\n    # Вычислить индекс хвостового элемента\n    last = index(@front + size - 1)\n    @nums[last]\n  end\n\n  ### Вернуть массив для вывода ###\n  def to_array\n    # Преобразовывать только элементы списка в пределах фактической длины\n    res = []\n    for i in 0...size\n      res << @nums[index(@front + i)]\n    end\n    res\n  end\n\n  private\n\n  ### Вычислить индекс в кольцевом массиве ###\n  def index(i)\n    # С помощью операции взятия по модулю соединить начало и конец массива\n    # Когда i выходит за конец массива, он возвращается в начало\n    # Когда i выходит за начало массива, он возвращается в конец\n    (i + capacity) % capacity\n  end\nend\n
        ","path":["Глава 5. Стек и очередь","5.3   Двусторонняя очередь"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#533","level":2,"title":"5.3.3   Применение двусторонней очереди","text":"

        Двусторонняя очередь сочетает в себе логику стека и очереди, поэтому она может покрыть все сценарии применения обеих структур и при этом предоставляет более высокую степень свободы.

        Мы знаем, что функция \"undo\" в программном обеспечении обычно реализуется с помощью стека: система помещает каждое изменение в стек с помощью push , а затем использует pop для отмены. Однако, учитывая ограниченность системных ресурсов, программы обычно ограничивают число шагов отмены, например разрешают хранить только \\(50\\) шагов. Когда длина стека превышает этот предел, программе нужно удалить элемент с дна стека, то есть с головы очереди. Но стек не может реализовать такую операцию, и в этом случае его приходится заменять двусторонней очередью. Обрати внимание: основная логика \"undo\" по-прежнему следует стековому правилу LIFO, просто двусторонняя очередь позволяет более гибко реализовать некоторые дополнительные механизмы.

        ","path":["Глава 5. Стек и очередь","5.3   Двусторонняя очередь"],"tags":[]},{"location":"chapter_stack_and_queue/queue/","level":1,"title":"5.2   Очередь","text":"

        Очередь (queue) - это линейная структура данных, подчиняющаяся правилу \"первым пришел - первым вышел\". Как видно из названия, очередь моделирует обычную ситуацию ожидания: новые люди непрерывно присоединяются к хвосту очереди, а стоящие в начале по одному уходят.

        Как показано на рисунке 5-4, начало очереди называется головой очереди, а конец - хвостом очереди; операцию добавления элемента в хвост называют enqueue, а операцию удаления элемента из головы - dequeue.

        Рисунок 5-4   Правило FIFO для очереди

        ","path":["Глава 5. Стек и очередь","5.2   Очередь"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#521","level":2,"title":"5.2.1   Основные операции с очередью","text":"

        Распространенные операции с очередью показаны в таблице 5-2. Следует учитывать, что названия методов в разных языках могут различаться. Здесь мы используем те же названия, что и для стека.

        Таблица 5-2   Эффективность операций с очередью

        Имя метода Описание Временная сложность push() Поместить элемент в очередь, то есть добавить его в хвост \\(O(1)\\) pop() Извлечь элемент из головы очереди \\(O(1)\\) peek() Просмотреть элемент в голове очереди \\(O(1)\\)

        Обычно достаточно использовать готовые классы очереди, предоставляемые языками программирования:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby queue.py
        from collections import deque\n\n# Инициализация очереди\n# В Python обычно используют двустороннюю очередь deque как обычную очередь\n# Хотя queue.Queue() является \"чистой\" очередью, она не слишком удобна, поэтому ее не рекомендуют\nque: deque[int] = deque()\n\n# Поместить элементы в очередь\nque.append(1)\nque.append(3)\nque.append(2)\nque.append(5)\nque.append(4)\n\n# Просмотреть элемент в голове очереди\nfront: int = que[0]\n\n# Извлечь элемент из очереди\npop: int = que.popleft()\n\n# Получить длину очереди\nsize: int = len(que)\n\n# Проверить, пуста ли очередь\nis_empty: bool = len(que) == 0\n
        queue.cpp
        /* Инициализация очереди */\nqueue<int> queue;\n\n/* Поместить элементы в очередь */\nqueue.push(1);\nqueue.push(3);\nqueue.push(2);\nqueue.push(5);\nqueue.push(4);\n\n/* Просмотреть элемент в голове очереди */\nint front = queue.front();\n\n/* Извлечь элемент из очереди */\nqueue.pop();\n\n/* Получить длину очереди */\nint size = queue.size();\n\n/* Проверить, пуста ли очередь */\nbool empty = queue.empty();\n
        queue.java
        /* Инициализация очереди */\nQueue<Integer> queue = new LinkedList<>();\n\n/* Поместить элементы в очередь */\nqueue.offer(1);\nqueue.offer(3);\nqueue.offer(2);\nqueue.offer(5);\nqueue.offer(4);\n\n/* Просмотреть элемент в голове очереди */\nint peek = queue.peek();\n\n/* Извлечь элемент из очереди */\nint pop = queue.poll();\n\n/* Получить длину очереди */\nint size = queue.size();\n\n/* Проверить, пуста ли очередь */\nboolean isEmpty = queue.isEmpty();\n
        queue.cs
        /* Инициализация очереди */\nQueue<int> queue = new();\n\n/* Поместить элементы в очередь */\nqueue.Enqueue(1);\nqueue.Enqueue(3);\nqueue.Enqueue(2);\nqueue.Enqueue(5);\nqueue.Enqueue(4);\n\n/* Просмотреть элемент в голове очереди */\nint peek = queue.Peek();\n\n/* Извлечь элемент из очереди */\nint pop = queue.Dequeue();\n\n/* Получить длину очереди */\nint size = queue.Count;\n\n/* Проверить, пуста ли очередь */\nbool isEmpty = queue.Count == 0;\n
        queue_test.go
        /* Инициализация очереди */\n// В Go очередь обычно реализуют через list\nqueue := list.New()\n\n/* Поместить элементы в очередь */\nqueue.PushBack(1)\nqueue.PushBack(3)\nqueue.PushBack(2)\nqueue.PushBack(5)\nqueue.PushBack(4)\n\n/* Просмотреть элемент в голове очереди */\npeek := queue.Front()\n\n/* Извлечь элемент из очереди */\npop := queue.Front()\nqueue.Remove(pop)\n\n/* Получить длину очереди */\nsize := queue.Len()\n\n/* Проверить, пуста ли очередь */\nisEmpty := queue.Len() == 0\n
        queue.swift
        /* Инициализация очереди */\n// В Swift нет встроенного класса очереди, поэтому можно использовать Array как очередь\nvar queue: [Int] = []\n\n/* Поместить элементы в очередь */\nqueue.append(1)\nqueue.append(3)\nqueue.append(2)\nqueue.append(5)\nqueue.append(4)\n\n/* Просмотреть элемент в голове очереди */\nlet peek = queue.first!\n\n/* Извлечь элемент из очереди */\n// Поскольку в основе лежит массив, removeFirst имеет сложность O(n)\nlet pool = queue.removeFirst()\n\n/* Получить длину очереди */\nlet size = queue.count\n\n/* Проверить, пуста ли очередь */\nlet isEmpty = queue.isEmpty\n
        queue.js
        /* Инициализация очереди */\n// В JavaScript нет встроенной очереди, поэтому можно использовать Array как очередь\nconst queue = [];\n\n/* Поместить элементы в очередь */\nqueue.push(1);\nqueue.push(3);\nqueue.push(2);\nqueue.push(5);\nqueue.push(4);\n\n/* Просмотреть элемент в голове очереди */\nconst peek = queue[0];\n\n/* Извлечь элемент из очереди */\n// В основе лежит массив, поэтому shift() имеет сложность O(n)\nconst pop = queue.shift();\n\n/* Получить длину очереди */\nconst size = queue.length;\n\n/* Проверить, пуста ли очередь */\nconst empty = queue.length === 0;\n
        queue.ts
        /* Инициализация очереди */\n// В TypeScript нет встроенной очереди, поэтому можно использовать Array как очередь\nconst queue: number[] = [];\n\n/* Поместить элементы в очередь */\nqueue.push(1);\nqueue.push(3);\nqueue.push(2);\nqueue.push(5);\nqueue.push(4);\n\n/* Просмотреть элемент в голове очереди */\nconst peek = queue[0];\n\n/* Извлечь элемент из очереди */\n// В основе лежит массив, поэтому shift() имеет сложность O(n)\nconst pop = queue.shift();\n\n/* Получить длину очереди */\nconst size = queue.length;\n\n/* Проверить, пуста ли очередь */\nconst empty = queue.length === 0;\n
        queue.dart
        /* Инициализация очереди */\n// В Dart класс Queue является двусторонней очередью и может использоваться как обычная очередь\nQueue<int> queue = Queue();\n\n/* Поместить элементы в очередь */\nqueue.add(1);\nqueue.add(3);\nqueue.add(2);\nqueue.add(5);\nqueue.add(4);\n\n/* Просмотреть элемент в голове очереди */\nint peek = queue.first;\n\n/* Извлечь элемент из очереди */\nint pop = queue.removeFirst();\n\n/* Получить длину очереди */\nint size = queue.length;\n\n/* Проверить, пуста ли очередь */\nbool isEmpty = queue.isEmpty;\n
        queue.rs
        /* Инициализация двусторонней очереди */\n// В Rust двусторонняя очередь может использоваться как обычная очередь\nlet mut deque: VecDeque<u32> = VecDeque::new();\n\n/* Поместить элементы в очередь */\ndeque.push_back(1);\ndeque.push_back(3);\ndeque.push_back(2);\ndeque.push_back(5);\ndeque.push_back(4);\n\n/* Просмотреть элемент в голове очереди */\nif let Some(front) = deque.front() {\n}\n\n/* Извлечь элемент из очереди */\nif let Some(pop) = deque.pop_front() {\n}\n\n/* Получить длину очереди */\nlet size = deque.len();\n\n/* Проверить, пуста ли очередь */\nlet is_empty = deque.is_empty();\n
        queue.c
        // В C нет встроенной очереди\n
        queue.kt
        /* Инициализация очереди */\nval queue = LinkedList<Int>()\n\n/* Поместить элементы в очередь */\nqueue.offer(1)\nqueue.offer(3)\nqueue.offer(2)\nqueue.offer(5)\nqueue.offer(4)\n\n/* Просмотреть элемент в голове очереди */\nval peek = queue.peek()\n\n/* Извлечь элемент из очереди */\nval pop = queue.poll()\n\n/* Получить длину очереди */\nval size = queue.size\n\n/* Проверить, пуста ли очередь */\nval isEmpty = queue.isEmpty()\n
        queue.rb
        # Инициализация очереди\n# Встроенная очередь в Ruby (Thread::Queue) не имеет методов peek и traverse, поэтому можно использовать Array как очередь\nqueue = []\n\n# Поместить элементы в очередь\nqueue.push(1)\nqueue.push(3)\nqueue.push(2)\nqueue.push(5)\nqueue.push(4)\n\n# Просмотреть элемент очереди\npeek = queue.first\n\n# Извлечь элемент из очереди\n# Обрати внимание: поскольку это массив, метод Array#shift имеет сложность O(n)\npop = queue.shift\n\n# Получить длину очереди\nsize = queue.length\n\n# Проверить, пуста ли очередь\nis_empty = queue.empty?\n
        Визуализация выполнения

        https://pythontutor.com/render.html#code=from%20collections%20import%20deque%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C%0A%20%20%20%20%23%20%D0%92%20Python%20%D0%B4%D0%B2%D1%83%D1%81%D1%82%D0%BE%D1%80%D0%BE%D0%BD%D0%BD%D1%8E%D1%8E%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C%20deque%20%D0%BE%D0%B1%D1%8B%D1%87%D0%BD%D0%BE%20%D0%B8%D1%81%D0%BF%D0%BE%D0%BB%D1%8C%D0%B7%D1%83%D1%8E%D1%82%20%D0%BA%D0%B0%D0%BA%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C%0A%20%20%20%20%23%20%D0%A5%D0%BE%D1%82%D1%8F%20queue.Queue%28%29%20%D1%8F%D0%B2%D0%BB%D1%8F%D0%B5%D1%82%D1%81%D1%8F%20%D0%BD%D0%B0%D1%81%D1%82%D0%BE%D1%8F%D1%89%D0%B8%D0%BC%20%D0%BA%D0%BB%D0%B0%D1%81%D1%81%D0%BE%D0%BC%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%2C%20%D0%BF%D0%BE%D0%BB%D1%8C%D0%B7%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%D1%81%D1%8F%20%D0%B8%D0%BC%20%D0%BD%D0%B5%20%D1%81%D0%BB%D0%B8%D1%88%D0%BA%D0%BE%D0%BC%20%D1%83%D0%B4%D0%BE%D0%B1%D0%BD%D0%BE%0A%20%20%20%20que%20%3D%20deque%28%29%0A%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BC%D0%B5%D1%81%D1%82%D0%B8%D1%82%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B2%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C%0A%20%20%20%20que.append%281%29%0A%20%20%20%20que.append%283%29%0A%20%20%20%20que.append%282%29%0A%20%20%20%20que.append%285%29%0A%20%20%20%20que.append%284%29%0A%20%20%20%20print%28%22%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C%20que%20%3D%22%2C%20que%29%0A%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B8%D1%82%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B2%20%D0%BD%D0%B0%D1%87%D0%B0%D0%BB%D0%B5%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%0A%20%20%20%20front%20%3D%20que%5B0%5D%0A%20%20%20%20print%28%22%D0%AD%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B2%20%D0%BD%D0%B0%D1%87%D0%B0%D0%BB%D0%B5%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%20front%20%3D%22%2C%20front%29%0A%0A%20%20%20%20%23%20%D0%98%D0%B7%D0%B2%D0%BB%D0%B5%D1%87%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B8%D0%B7%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%0A%20%20%20%20pop%20%3D%20que.popleft%28%29%0A%20%20%20%20print%28%22%D0%98%D0%B7%D0%B2%D0%BB%D0%B5%D1%87%D0%B5%D0%BD%D0%BD%D1%8B%D0%B9%20%D0%B8%D0%B7%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20pop%20%3D%22%2C%20pop%29%0A%20%20%20%20print%28%22que%20%D0%BF%D0%BE%D1%81%D0%BB%D0%B5%20%D0%B8%D0%B7%D0%B2%D0%BB%D0%B5%D1%87%D0%B5%D0%BD%D0%B8%D1%8F%20%3D%22%2C%20que%29%0A%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B8%D1%82%D1%8C%20%D0%B4%D0%BB%D0%B8%D0%BD%D1%83%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%0A%20%20%20%20size%20%3D%20len%28que%29%0A%20%20%20%20print%28%22%D0%94%D0%BB%D0%B8%D0%BD%D0%B0%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%20size%20%3D%22%2C%20size%29%0A%0A%20%20%20%20%23%20%D0%9F%D1%80%D0%BE%D0%B2%D0%B5%D1%80%D0%B8%D1%82%D1%8C%2C%20%D0%BF%D1%83%D1%81%D1%82%D0%B0%20%D0%BB%D0%B8%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C%0A%20%20%20%20is_empty%20%3D%20len%28que%29%20%3D%3D%200%0A%20%20%20%20print%28%22%D0%9F%D1%83%D1%81%D1%82%D0%B0%20%D0%BB%D0%B8%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C%20%3D%22%2C%20is_empty%29&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

        ","path":["Глава 5. Стек и очередь","5.2   Очередь"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#522","level":2,"title":"5.2.2   Реализация очереди","text":"

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

        ","path":["Глава 5. Стек и очередь","5.2   Очередь"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#1","level":3,"title":"1.   Реализация на основе связного списка","text":"

        Как показано на рисунке 5-5, мы можем рассматривать головной узел и хвостовой узел связного списка как голову очереди и хвост очереди соответственно, договорившись, что добавлять узлы можно только в хвост, а удалять - только из головы.

        <1><2><3>

        Рисунок 5-5   Операции enqueue и dequeue в реализации очереди на связном списке

        Ниже приведен код реализации очереди на связном списке:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linkedlist_queue.py
        class LinkedListQueue:\n    \"\"\"Очередь на основе связного списка\"\"\"\n\n    def __init__(self):\n        \"\"\"Конструктор\"\"\"\n        self._front: ListNode | None = None  # Головной узел front\n        self._rear: ListNode | None = None  # Хвостовой узел rear\n        self._size: int = 0\n\n    def size(self) -> int:\n        \"\"\"Получение длины очереди\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"Проверка, пуста ли очередь\"\"\"\n        return self._size == 0\n\n    def push(self, num: int):\n        \"\"\"Поместить в очередь\"\"\"\n        # Добавить num после хвостового узла\n        node = ListNode(num)\n        # Если очередь пуста, сделать так, чтобы и head, и tail указывали на этот узел\n        if self._front is None:\n            self._front = node\n            self._rear = node\n        # Если очередь не пуста, добавить этот узел после хвостового узла\n        else:\n            self._rear.next = node\n            self._rear = node\n        self._size += 1\n\n    def pop(self) -> int:\n        \"\"\"Извлечь из очереди\"\"\"\n        num = self.peek()\n        # Удалить головной узел\n        self._front = self._front.next\n        self._size -= 1\n        return num\n\n    def peek(self) -> int:\n        \"\"\"Доступ к элементу в начале очереди\"\"\"\n        if self.is_empty():\n            raise IndexError(\"очередь пуста\")\n        return self._front.val\n\n    def to_list(self) -> list[int]:\n        \"\"\"Преобразовать в список для вывода\"\"\"\n        queue = []\n        temp = self._front\n        while temp:\n            queue.append(temp.val)\n            temp = temp.next\n        return queue\n
        linkedlist_queue.cpp
        /* Очередь на основе связного списка */\nclass LinkedListQueue {\n  private:\n    ListNode *front, *rear; // Головной узел front, хвостовой узел rear\n    int queSize;\n\n  public:\n    LinkedListQueue() {\n        front = nullptr;\n        rear = nullptr;\n        queSize = 0;\n    }\n\n    ~LinkedListQueue() {\n        // Обходить связный список, удалять узлы и освобождать память\n        freeMemoryLinkedList(front);\n    }\n\n    /* Получение длины очереди */\n    int size() {\n        return queSize;\n    }\n\n    /* Проверка, пуста ли очередь */\n    bool isEmpty() {\n        return queSize == 0;\n    }\n\n    /* Поместить в очередь */\n    void push(int num) {\n        // Добавить num после хвостового узла\n        ListNode *node = new ListNode(num);\n        // Если очередь пуста, сделать так, чтобы и head, и tail указывали на этот узел\n        if (front == nullptr) {\n            front = node;\n            rear = node;\n        }\n        // Если очередь не пуста, добавить этот узел после хвостового узла\n        else {\n            rear->next = node;\n            rear = node;\n        }\n        queSize++;\n    }\n\n    /* Извлечь из очереди */\n    int pop() {\n        int num = peek();\n        // Удалить головной узел\n        ListNode *tmp = front;\n        front = front->next;\n        // Освободить память\n        delete tmp;\n        queSize--;\n        return num;\n    }\n\n    /* Доступ к элементу в начале очереди */\n    int peek() {\n        if (size() == 0)\n            throw out_of_range(\"очередь пуста\");\n        return front->val;\n    }\n\n    /* Преобразовать связный список в Vector и вернуть */\n    vector<int> toVector() {\n        ListNode *node = front;\n        vector<int> res(size());\n        for (int i = 0; i < res.size(); i++) {\n            res[i] = node->val;\n            node = node->next;\n        }\n        return res;\n    }\n};\n
        linkedlist_queue.java
        /* Очередь на основе связного списка */\nclass LinkedListQueue {\n    private ListNode front, rear; // Головной узел front, хвостовой узел rear\n    private int queSize = 0;\n\n    public LinkedListQueue() {\n        front = null;\n        rear = null;\n    }\n\n    /* Получение длины очереди */\n    public int size() {\n        return queSize;\n    }\n\n    /* Проверка, пуста ли очередь */\n    public boolean isEmpty() {\n        return size() == 0;\n    }\n\n    /* Поместить в очередь */\n    public void push(int num) {\n        // Добавить num после хвостового узла\n        ListNode node = new ListNode(num);\n        // Если очередь пуста, сделать так, чтобы и head, и tail указывали на этот узел\n        if (front == null) {\n            front = node;\n            rear = node;\n        // Если очередь не пуста, добавить этот узел после хвостового узла\n        } else {\n            rear.next = node;\n            rear = node;\n        }\n        queSize++;\n    }\n\n    /* Извлечь из очереди */\n    public int pop() {\n        int num = peek();\n        // Удалить головной узел\n        front = front.next;\n        queSize--;\n        return num;\n    }\n\n    /* Доступ к элементу в начале очереди */\n    public int peek() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return front.val;\n    }\n\n    /* Преобразовать связный список в Array и вернуть */\n    public int[] toArray() {\n        ListNode node = front;\n        int[] res = new int[size()];\n        for (int i = 0; i < res.length; i++) {\n            res[i] = node.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
        linkedlist_queue.cs
        /* Очередь на основе связного списка */\nclass LinkedListQueue {\n    ListNode? front, rear;  // Головной узел front, хвостовой узел rear\n    int queSize = 0;\n\n    public LinkedListQueue() {\n        front = null;\n        rear = null;\n    }\n\n    /* Получение длины очереди */\n    public int Size() {\n        return queSize;\n    }\n\n    /* Проверка, пуста ли очередь */\n    public bool IsEmpty() {\n        return Size() == 0;\n    }\n\n    /* Поместить в очередь */\n    public void Push(int num) {\n        // Добавить num после хвостового узла\n        ListNode node = new(num);\n        // Если очередь пуста, сделать так, чтобы и head, и tail указывали на этот узел\n        if (front == null) {\n            front = node;\n            rear = node;\n            // Если очередь не пуста, добавить этот узел после хвостового узла\n        } else if (rear != null) {\n            rear.next = node;\n            rear = node;\n        }\n        queSize++;\n    }\n\n    /* Извлечь из очереди */\n    public int Pop() {\n        int num = Peek();\n        // Удалить головной узел\n        front = front?.next;\n        queSize--;\n        return num;\n    }\n\n    /* Доступ к элементу в начале очереди */\n    public int Peek() {\n        if (IsEmpty())\n            throw new Exception();\n        return front!.val;\n    }\n\n    /* Преобразовать связный список в Array и вернуть */\n    public int[] ToArray() {\n        if (front == null)\n            return [];\n\n        ListNode? node = front;\n        int[] res = new int[Size()];\n        for (int i = 0; i < res.Length; i++) {\n            res[i] = node!.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
        linkedlist_queue.go
        /* Очередь на основе связного списка */\ntype linkedListQueue struct {\n    // Использовать встроенный пакет list для реализации очереди\n    data *list.List\n}\n\n/* Инициализация очереди */\nfunc newLinkedListQueue() *linkedListQueue {\n    return &linkedListQueue{\n        data: list.New(),\n    }\n}\n\n/* Поместить в очередь */\nfunc (s *linkedListQueue) push(value any) {\n    s.data.PushBack(value)\n}\n\n/* Извлечь из очереди */\nfunc (s *linkedListQueue) pop() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Front()\n    s.data.Remove(e)\n    return e.Value\n}\n\n/* Доступ к элементу в начале очереди */\nfunc (s *linkedListQueue) peek() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Front()\n    return e.Value\n}\n\n/* Получение длины очереди */\nfunc (s *linkedListQueue) size() int {\n    return s.data.Len()\n}\n\n/* Проверка, пуста ли очередь */\nfunc (s *linkedListQueue) isEmpty() bool {\n    return s.data.Len() == 0\n}\n\n/* Получить List для вывода */\nfunc (s *linkedListQueue) toList() *list.List {\n    return s.data\n}\n
        linkedlist_queue.swift
        /* Очередь на основе связного списка */\nclass LinkedListQueue {\n    private var front: ListNode? // Головной узел\n    private var rear: ListNode? // Хвостовой узел\n    private var _size: Int\n\n    init() {\n        _size = 0\n    }\n\n    /* Получение длины очереди */\n    func size() -> Int {\n        _size\n    }\n\n    /* Проверка, пуста ли очередь */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* Поместить в очередь */\n    func push(num: Int) {\n        // Добавить num после хвостового узла\n        let node = ListNode(x: num)\n        // Если очередь пуста, сделать так, чтобы и head, и tail указывали на этот узел\n        if front == nil {\n            front = node\n            rear = node\n        }\n        // Если очередь не пуста, добавить этот узел после хвостового узла\n        else {\n            rear?.next = node\n            rear = node\n        }\n        _size += 1\n    }\n\n    /* Извлечь из очереди */\n    @discardableResult\n    func pop() -> Int {\n        let num = peek()\n        // Удалить головной узел\n        front = front?.next\n        _size -= 1\n        return num\n    }\n\n    /* Доступ к элементу в начале очереди */\n    func peek() -> Int {\n        if isEmpty() {\n            fatalError(\"очередь пуста\")\n        }\n        return front!.val\n    }\n\n    /* Преобразовать связный список в Array и вернуть */\n    func toArray() -> [Int] {\n        var node = front\n        var res = Array(repeating: 0, count: size())\n        for i in res.indices {\n            res[i] = node!.val\n            node = node?.next\n        }\n        return res\n    }\n}\n
        linkedlist_queue.js
        /* Очередь на основе связного списка */\nclass LinkedListQueue {\n    #front; // Головной узел #front\n    #rear; // Хвостовой узел #rear\n    #queSize = 0;\n\n    constructor() {\n        this.#front = null;\n        this.#rear = null;\n    }\n\n    /* Получение длины очереди */\n    get size() {\n        return this.#queSize;\n    }\n\n    /* Проверка, пуста ли очередь */\n    isEmpty() {\n        return this.size === 0;\n    }\n\n    /* Поместить в очередь */\n    push(num) {\n        // Добавить num после хвостового узла\n        const node = new ListNode(num);\n        // Если очередь пуста, сделать так, чтобы и head, и tail указывали на этот узел\n        if (!this.#front) {\n            this.#front = node;\n            this.#rear = node;\n            // Если очередь не пуста, добавить этот узел после хвостового узла\n        } else {\n            this.#rear.next = node;\n            this.#rear = node;\n        }\n        this.#queSize++;\n    }\n\n    /* Извлечь из очереди */\n    pop() {\n        const num = this.peek();\n        // Удалить головной узел\n        this.#front = this.#front.next;\n        this.#queSize--;\n        return num;\n    }\n\n    /* Доступ к элементу в начале очереди */\n    peek() {\n        if (this.size === 0) throw new Error('очередь пуста');\n        return this.#front.val;\n    }\n\n    /* Преобразовать связный список в Array и вернуть */\n    toArray() {\n        let node = this.#front;\n        const res = new Array(this.size);\n        for (let i = 0; i < res.length; i++) {\n            res[i] = node.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
        linkedlist_queue.ts
        /* Очередь на основе связного списка */\nclass LinkedListQueue {\n    private front: ListNode | null; // Головной узел front\n    private rear: ListNode | null; // Хвостовой узел rear\n    private queSize: number = 0;\n\n    constructor() {\n        this.front = null;\n        this.rear = null;\n    }\n\n    /* Получение длины очереди */\n    get size(): number {\n        return this.queSize;\n    }\n\n    /* Проверка, пуста ли очередь */\n    isEmpty(): boolean {\n        return this.size === 0;\n    }\n\n    /* Поместить в очередь */\n    push(num: number): void {\n        // Добавить num после хвостового узла\n        const node = new ListNode(num);\n        // Если очередь пуста, сделать так, чтобы и head, и tail указывали на этот узел\n        if (!this.front) {\n            this.front = node;\n            this.rear = node;\n            // Если очередь не пуста, добавить этот узел после хвостового узла\n        } else {\n            this.rear!.next = node;\n            this.rear = node;\n        }\n        this.queSize++;\n    }\n\n    /* Извлечь из очереди */\n    pop(): number {\n        const num = this.peek();\n        if (!this.front) throw new Error('очередь пуста');\n        // Удалить головной узел\n        this.front = this.front.next;\n        this.queSize--;\n        return num;\n    }\n\n    /* Доступ к элементу в начале очереди */\n    peek(): number {\n        if (this.size === 0) throw new Error('очередь пуста');\n        return this.front!.val;\n    }\n\n    /* Преобразовать связный список в Array и вернуть */\n    toArray(): number[] {\n        let node = this.front;\n        const res = new Array<number>(this.size);\n        for (let i = 0; i < res.length; i++) {\n            res[i] = node!.val;\n            node = node!.next;\n        }\n        return res;\n    }\n}\n
        linkedlist_queue.dart
        /* Очередь на основе связного списка */\nclass LinkedListQueue {\n  ListNode? _front; // Головной узел _front\n  ListNode? _rear; // Хвостовой узел _rear\n  int _queSize = 0; // Длина очереди\n\n  LinkedListQueue() {\n    _front = null;\n    _rear = null;\n  }\n\n  /* Получение длины очереди */\n  int size() {\n    return _queSize;\n  }\n\n  /* Проверка, пуста ли очередь */\n  bool isEmpty() {\n    return _queSize == 0;\n  }\n\n  /* Поместить в очередь */\n  void push(int _num) {\n    // Добавить _num после хвостового узла\n    final node = ListNode(_num);\n    // Если очередь пуста, сделать так, чтобы и head, и tail указывали на этот узел\n    if (_front == null) {\n      _front = node;\n      _rear = node;\n    } else {\n      // Если очередь не пуста, добавить этот узел после хвостового узла\n      _rear!.next = node;\n      _rear = node;\n    }\n    _queSize++;\n  }\n\n  /* Извлечь из очереди */\n  int pop() {\n    final int _num = peek();\n    // Удалить головной узел\n    _front = _front!.next;\n    _queSize--;\n    return _num;\n  }\n\n  /* Доступ к элементу в начале очереди */\n  int peek() {\n    if (_queSize == 0) {\n      throw Exception('очередь пуста');\n    }\n    return _front!.val;\n  }\n\n  /* Преобразовать связный список в Array и вернуть */\n  List<int> toArray() {\n    ListNode? node = _front;\n    final List<int> queue = [];\n    while (node != null) {\n      queue.add(node.val);\n      node = node.next;\n    }\n    return queue;\n  }\n}\n
        linkedlist_queue.rs
        /* Очередь на основе связного списка */\n#[allow(dead_code)]\npub struct LinkedListQueue<T> {\n    front: Option<Rc<RefCell<ListNode<T>>>>, // Головной узел front\n    rear: Option<Rc<RefCell<ListNode<T>>>>,  // Хвостовой узел rear\n    que_size: usize,                         // Длина очереди\n}\n\nimpl<T: Copy> LinkedListQueue<T> {\n    pub fn new() -> Self {\n        Self {\n            front: None,\n            rear: None,\n            que_size: 0,\n        }\n    }\n\n    /* Получение длины очереди */\n    pub fn size(&self) -> usize {\n        return self.que_size;\n    }\n\n    /* Проверка, пуста ли очередь */\n    pub fn is_empty(&self) -> bool {\n        return self.que_size == 0;\n    }\n\n    /* Поместить в очередь */\n    pub fn push(&mut self, num: T) {\n        // Добавить num после хвостового узла\n        let new_rear = ListNode::new(num);\n        match self.rear.take() {\n            // Если очередь не пуста, добавить этот узел после хвостового узла\n            Some(old_rear) => {\n                old_rear.borrow_mut().next = Some(new_rear.clone());\n                self.rear = Some(new_rear);\n            }\n            // Если очередь пуста, сделать так, чтобы и head, и tail указывали на этот узел\n            None => {\n                self.front = Some(new_rear.clone());\n                self.rear = Some(new_rear);\n            }\n        }\n        self.que_size += 1;\n    }\n\n    /* Извлечь из очереди */\n    pub fn pop(&mut self) -> Option<T> {\n        self.front.take().map(|old_front| {\n            match old_front.borrow_mut().next.take() {\n                Some(new_front) => {\n                    self.front = Some(new_front);\n                }\n                None => {\n                    self.rear.take();\n                }\n            }\n            self.que_size -= 1;\n            old_front.borrow().val\n        })\n    }\n\n    /* Доступ к элементу в начале очереди */\n    pub fn peek(&self) -> Option<&Rc<RefCell<ListNode<T>>>> {\n        self.front.as_ref()\n    }\n\n    /* Преобразовать связный список в Array и вернуть */\n    pub fn to_array(&self, head: Option<&Rc<RefCell<ListNode<T>>>>) -> Vec<T> {\n        let mut res: Vec<T> = Vec::new();\n\n        fn recur<T: Copy>(cur: Option<&Rc<RefCell<ListNode<T>>>>, res: &mut Vec<T>) {\n            if let Some(cur) = cur {\n                res.push(cur.borrow().val);\n                recur(cur.borrow().next.as_ref(), res);\n            }\n        }\n\n        recur(head, &mut res);\n\n        res\n    }\n}\n
        linkedlist_queue.c
        /* Очередь на основе связного списка */\ntypedef struct {\n    ListNode *front, *rear;\n    int queSize;\n} LinkedListQueue;\n\n/* Конструктор */\nLinkedListQueue *newLinkedListQueue() {\n    LinkedListQueue *queue = (LinkedListQueue *)malloc(sizeof(LinkedListQueue));\n    queue->front = NULL;\n    queue->rear = NULL;\n    queue->queSize = 0;\n    return queue;\n}\n\n/* Деструктор */\nvoid delLinkedListQueue(LinkedListQueue *queue) {\n    // Освободить все узлы\n    while (queue->front != NULL) {\n        ListNode *tmp = queue->front;\n        queue->front = queue->front->next;\n        free(tmp);\n    }\n    // Освободить структуру queue\n    free(queue);\n}\n\n/* Получение длины очереди */\nint size(LinkedListQueue *queue) {\n    return queue->queSize;\n}\n\n/* Проверка, пуста ли очередь */\nbool empty(LinkedListQueue *queue) {\n    return (size(queue) == 0);\n}\n\n/* Поместить в очередь */\nvoid push(LinkedListQueue *queue, int num) {\n    // Добавить node в хвост\n    ListNode *node = newListNode(num);\n    // Если очередь пуста, сделать так, чтобы и head, и tail указывали на этот узел\n    if (queue->front == NULL) {\n        queue->front = node;\n        queue->rear = node;\n    }\n    // Если очередь не пуста, добавить этот узел после хвостового узла\n    else {\n        queue->rear->next = node;\n        queue->rear = node;\n    }\n    queue->queSize++;\n}\n\n/* Доступ к элементу в начале очереди */\nint peek(LinkedListQueue *queue) {\n    assert(size(queue) && queue->front);\n    return queue->front->val;\n}\n\n/* Извлечь из очереди */\nint pop(LinkedListQueue *queue) {\n    int num = peek(queue);\n    ListNode *tmp = queue->front;\n    queue->front = queue->front->next;\n    free(tmp);\n    queue->queSize--;\n    return num;\n}\n\n/* Вывести очередь */\nvoid printLinkedListQueue(LinkedListQueue *queue) {\n    int *arr = malloc(sizeof(int) * queue->queSize);\n    // Скопировать данные связного списка в массив\n    int i;\n    ListNode *node;\n    for (i = 0, node = queue->front; i < queue->queSize; i++) {\n        arr[i] = node->val;\n        node = node->next;\n    }\n    printArray(arr, queue->queSize);\n    free(arr);\n}\n
        linkedlist_queue.kt
        /* Очередь на основе связного списка */\nclass LinkedListQueue(\n    // Головной узел front, хвостовой узел rear\n    private var front: ListNode? = null,\n    private var rear: ListNode? = null,\n    private var queSize: Int = 0\n) {\n\n    /* Получение длины очереди */\n    fun size(): Int {\n        return queSize\n    }\n\n    /* Проверка, пуста ли очередь */\n    fun isEmpty(): Boolean {\n        return size() == 0\n    }\n\n    /* Поместить в очередь */\n    fun push(num: Int) {\n        // Добавить num после хвостового узла\n        val node = ListNode(num)\n        // Если очередь пуста, сделать так, чтобы и head, и tail указывали на этот узел\n        if (front == null) {\n            front = node\n            rear = node\n            // Если очередь не пуста, добавить этот узел после хвостового узла\n        } else {\n            rear?.next = node\n            rear = node\n        }\n        queSize++\n    }\n\n    /* Извлечь из очереди */\n    fun pop(): Int {\n        val num = peek()\n        // Удалить головной узел\n        front = front?.next\n        queSize--\n        return num\n    }\n\n    /* Доступ к элементу в начале очереди */\n    fun peek(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return front!!._val\n    }\n\n    /* Преобразовать связный список в Array и вернуть */\n    fun toArray(): IntArray {\n        var node = front\n        val res = IntArray(size())\n        for (i in res.indices) {\n            res[i] = node!!._val\n            node = node.next\n        }\n        return res\n    }\n}\n
        linkedlist_queue.rb
        ### Очередь на основе связного списка ###\nclass LinkedListQueue\n  ### Получение длины очереди ###\n  attr_reader :size\n\n  ### Конструктор ###\n  def initialize\n    @front = nil  # Головной узел front\n    @rear = nil   # Хвостовой узел rear\n    @size = 0\n  end\n\n  ### Проверка, пуста ли очередь ###\n  def is_empty?\n    @front.nil?\n  end\n\n  ### Добавление в очередь ###\n  def push(num)\n    # Добавить num после хвостового узла\n    node = ListNode.new(num)\n\n    # Если очередь пуста, сделать так, чтобы и head, и tail указывали на этот узел\n    if @front.nil?\n      @front = node\n      @rear = node\n    # Если очередь не пуста, добавить этот узел после хвостового узла\n    else\n      @rear.next = node\n      @rear = node\n    end\n\n    @size += 1\n  end\n\n  ### Извлечение из очереди ###\n  def pop\n    num = peek\n    # Удалить головной узел\n    @front = @front.next\n    @size -= 1\n    num\n  end\n\n  ### Доступ к элементу в начале очереди ###\n  def peek\n    raise IndexError, 'очередь пуста' if is_empty?\n\n    @front.val\n  end\n\n  ### Преобразовать связный список в Array и вернуть ###\n  def to_array\n    queue = []\n    temp = @front\n    while temp\n      queue << temp.val\n      temp = temp.next\n    end\n    queue\n  end\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 5. Стек и очередь","5.2   Очередь"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#2","level":3,"title":"2.   Реализация на основе массива","text":"

        Удаление первого элемента из массива имеет временную сложность \\(O(n)\\) , из-за чего операция dequeue оказывается неэффективной. Однако этого можно избежать с помощью следующего приема.

        Мы можем использовать переменную front , указывающую на индекс элемента в голове очереди, и поддерживать переменную size , которая хранит длину очереди. Определим rear = front + size ; эта формула дает позицию rear, указывающую на ячейку сразу после хвоста очереди.

        Исходя из этого, эффективный диапазон элементов массива равен [front, rear - 1], а различные операции реализуются, как показано на рисунке 5-6.

        • Операция enqueue: записать входной элемент по индексу rear и увеличить size на 1.
        • Операция dequeue: просто увеличить front на 1 и уменьшить size на 1.

        Можно увидеть, что и enqueue , и dequeue требуют всего одной операции, а значит обе имеют временную сложность \\(O(1)\\) .

        <1><2><3>

        Рисунок 5-6   Операции enqueue и dequeue в реализации очереди на массиве

        Ты можешь заметить еще одну проблему: при непрерывных операциях enqueue и dequeue значения front и rear оба движутся вправо, и когда они доходят до конца массива, дальше сдвигаться уже нельзя. Чтобы решить эту проблему, можно рассматривать массив как кольцевой массив, у которого начало и конец соединены.

        Для кольцевого массива нужно сделать так, чтобы front или rear, перешагнув конец массива, сразу возвращались к его началу и продолжали движение. Такую периодичность удобно реализовать с помощью операции взятия остатка, как показано в коде ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_queue.py
        class ArrayQueue:\n    \"\"\"Очередь на основе кольцевого массива\"\"\"\n\n    def __init__(self, size: int):\n        \"\"\"Конструктор\"\"\"\n        self._nums: list[int] = [0] * size  # Массив для хранения элементов очереди\n        self._front: int = 0  # Указатель head, указывающий на первый элемент очереди\n        self._size: int = 0  # Длина очереди\n\n    def capacity(self) -> int:\n        \"\"\"Получить вместимость очереди\"\"\"\n        return len(self._nums)\n\n    def size(self) -> int:\n        \"\"\"Получение длины очереди\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"Проверка, пуста ли очередь\"\"\"\n        return self._size == 0\n\n    def push(self, num: int):\n        \"\"\"Поместить в очередь\"\"\"\n        if self._size == self.capacity():\n            raise IndexError(\"очередь заполнена\")\n        # Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n        # С помощью операции взятия по модулю вернуть rear к началу после выхода за конец массива\n        rear: int = (self._front + self._size) % self.capacity()\n        # Добавить num в хвост очереди\n        self._nums[rear] = num\n        self._size += 1\n\n    def pop(self) -> int:\n        \"\"\"Извлечь из очереди\"\"\"\n        num: int = self.peek()\n        # Указатель head сдвигается на одну позицию назад; если он выходит за конец, то возвращается в начало массива\n        self._front = (self._front + 1) % self.capacity()\n        self._size -= 1\n        return num\n\n    def peek(self) -> int:\n        \"\"\"Доступ к элементу в начале очереди\"\"\"\n        if self.is_empty():\n            raise IndexError(\"очередь пуста\")\n        return self._nums[self._front]\n\n    def to_list(self) -> list[int]:\n        \"\"\"Вернуть список для вывода\"\"\"\n        res = [0] * self.size()\n        j: int = self._front\n        for i in range(self.size()):\n            res[i] = self._nums[(j % self.capacity())]\n            j += 1\n        return res\n
        array_queue.cpp
        /* Очередь на основе кольцевого массива */\nclass ArrayQueue {\n  private:\n    int *nums;       // Массив для хранения элементов очереди\n    int front;       // Указатель head, указывающий на первый элемент очереди\n    int queSize;     // Длина очереди\n    int queCapacity; // Вместимость очереди\n\n  public:\n    ArrayQueue(int capacity) {\n        // Инициализация массива\n        nums = new int[capacity];\n        queCapacity = capacity;\n        front = queSize = 0;\n    }\n\n    ~ArrayQueue() {\n        delete[] nums;\n    }\n\n    /* Получить вместимость очереди */\n    int capacity() {\n        return queCapacity;\n    }\n\n    /* Получение длины очереди */\n    int size() {\n        return queSize;\n    }\n\n    /* Проверка, пуста ли очередь */\n    bool isEmpty() {\n        return size() == 0;\n    }\n\n    /* Поместить в очередь */\n    void push(int num) {\n        if (queSize == queCapacity) {\n            cout << \"Очередь заполнена\" << endl;\n            return;\n        }\n        // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n        // С помощью операции взятия по модулю вернуть rear к началу после выхода за конец массива\n        int rear = (front + queSize) % queCapacity;\n        // Добавить num в хвост очереди\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* Извлечь из очереди */\n    int pop() {\n        int num = peek();\n        // Указатель head сдвигается на одну позицию назад; если он выходит за конец, то возвращается в начало массива\n        front = (front + 1) % queCapacity;\n        queSize--;\n        return num;\n    }\n\n    /* Доступ к элементу в начале очереди */\n    int peek() {\n        if (isEmpty())\n            throw out_of_range(\"очередь пуста\");\n        return nums[front];\n    }\n\n    /* Преобразовать массив в Vector и вернуть */\n    vector<int> toVector() {\n        // Преобразовывать только элементы списка в пределах фактической длины\n        vector<int> arr(queSize);\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            arr[i] = nums[j % queCapacity];\n        }\n        return arr;\n    }\n};\n
        array_queue.java
        /* Очередь на основе кольцевого массива */\nclass ArrayQueue {\n    private int[] nums; // Массив для хранения элементов очереди\n    private int front; // Указатель head, указывающий на первый элемент очереди\n    private int queSize; // Длина очереди\n\n    public ArrayQueue(int capacity) {\n        nums = new int[capacity];\n        front = queSize = 0;\n    }\n\n    /* Получить вместимость очереди */\n    public int capacity() {\n        return nums.length;\n    }\n\n    /* Получение длины очереди */\n    public int size() {\n        return queSize;\n    }\n\n    /* Проверка, пуста ли очередь */\n    public boolean isEmpty() {\n        return queSize == 0;\n    }\n\n    /* Поместить в очередь */\n    public void push(int num) {\n        if (queSize == capacity()) {\n            System.out.println(\"Очередь заполнена\");\n            return;\n        }\n        // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n        // С помощью операции взятия по модулю вернуть rear к началу после выхода за конец массива\n        int rear = (front + queSize) % capacity();\n        // Добавить num в хвост очереди\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* Извлечь из очереди */\n    public int pop() {\n        int num = peek();\n        // Указатель head сдвигается на одну позицию назад; если он выходит за конец, то возвращается в начало массива\n        front = (front + 1) % capacity();\n        queSize--;\n        return num;\n    }\n\n    /* Доступ к элементу в начале очереди */\n    public int peek() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return nums[front];\n    }\n\n    /* Вернуть массив */\n    public int[] toArray() {\n        // Преобразовывать только элементы списка в пределах фактической длины\n        int[] res = new int[queSize];\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            res[i] = nums[j % capacity()];\n        }\n        return res;\n    }\n}\n
        array_queue.cs
        /* Очередь на основе кольцевого массива */\nclass ArrayQueue {\n    int[] nums;  // Массив для хранения элементов очереди\n    int front;   // Указатель head, указывающий на первый элемент очереди\n    int queSize; // Длина очереди\n\n    public ArrayQueue(int capacity) {\n        nums = new int[capacity];\n        front = queSize = 0;\n    }\n\n    /* Получить вместимость очереди */\n    int Capacity() {\n        return nums.Length;\n    }\n\n    /* Получение длины очереди */\n    public int Size() {\n        return queSize;\n    }\n\n    /* Проверка, пуста ли очередь */\n    public bool IsEmpty() {\n        return queSize == 0;\n    }\n\n    /* Поместить в очередь */\n    public void Push(int num) {\n        if (queSize == Capacity()) {\n            Console.WriteLine(\"Очередь заполнена\");\n            return;\n        }\n        // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n        // С помощью операции взятия по модулю вернуть rear к началу после выхода за конец массива\n        int rear = (front + queSize) % Capacity();\n        // Добавить num в хвост очереди\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* Извлечь из очереди */\n    public int Pop() {\n        int num = Peek();\n        // Указатель head сдвигается на одну позицию назад; если он выходит за конец, то возвращается в начало массива\n        front = (front + 1) % Capacity();\n        queSize--;\n        return num;\n    }\n\n    /* Доступ к элементу в начале очереди */\n    public int Peek() {\n        if (IsEmpty())\n            throw new Exception();\n        return nums[front];\n    }\n\n    /* Вернуть массив */\n    public int[] ToArray() {\n        // Преобразовывать только элементы списка в пределах фактической длины\n        int[] res = new int[queSize];\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            res[i] = nums[j % this.Capacity()];\n        }\n        return res;\n    }\n}\n
        array_queue.go
        /* Очередь на основе кольцевого массива */\ntype arrayQueue struct {\n    nums        []int // Массив для хранения элементов очереди\n    front       int   // Указатель head, указывающий на первый элемент очереди\n    queSize     int   // Длина очереди\n    queCapacity int   // Вместимость очереди (то есть максимальное число элементов)\n}\n\n/* Инициализация очереди */\nfunc newArrayQueue(queCapacity int) *arrayQueue {\n    return &arrayQueue{\n        nums:        make([]int, queCapacity),\n        queCapacity: queCapacity,\n        front:       0,\n        queSize:     0,\n    }\n}\n\n/* Получение длины очереди */\nfunc (q *arrayQueue) size() int {\n    return q.queSize\n}\n\n/* Проверка, пуста ли очередь */\nfunc (q *arrayQueue) isEmpty() bool {\n    return q.queSize == 0\n}\n\n/* Поместить в очередь */\nfunc (q *arrayQueue) push(num int) {\n    // Когда rear == queCapacity, очередь заполнена\n    if q.queSize == q.queCapacity {\n        return\n    }\n    // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n    // С помощью операции взятия по модулю вернуть rear к началу после выхода за конец массива\n    rear := (q.front + q.queSize) % q.queCapacity\n    // Добавить num в хвост очереди\n    q.nums[rear] = num\n    q.queSize++\n}\n\n/* Извлечь из очереди */\nfunc (q *arrayQueue) pop() any {\n    num := q.peek()\n    if num == nil {\n        return nil\n    }\n\n    // Указатель head сдвигается на одну позицию назад; если он выходит за конец, то возвращается в начало массива\n    q.front = (q.front + 1) % q.queCapacity\n    q.queSize--\n    return num\n}\n\n/* Доступ к элементу в начале очереди */\nfunc (q *arrayQueue) peek() any {\n    if q.isEmpty() {\n        return nil\n    }\n    return q.nums[q.front]\n}\n\n/* Получить Slice для вывода */\nfunc (q *arrayQueue) toSlice() []int {\n    rear := (q.front + q.queSize)\n    if rear >= q.queCapacity {\n        rear %= q.queCapacity\n        return append(q.nums[q.front:], q.nums[:rear]...)\n    }\n    return q.nums[q.front:rear]\n}\n
        array_queue.swift
        /* Очередь на основе кольцевого массива */\nclass ArrayQueue {\n    private var nums: [Int] // Массив для хранения элементов очереди\n    private var front: Int // Указатель head, указывающий на первый элемент очереди\n    private var _size: Int // Длина очереди\n\n    init(capacity: Int) {\n        // Инициализация массива\n        nums = Array(repeating: 0, count: capacity)\n        front = 0\n        _size = 0\n    }\n\n    /* Получить вместимость очереди */\n    func capacity() -> Int {\n        nums.count\n    }\n\n    /* Получение длины очереди */\n    func size() -> Int {\n        _size\n    }\n\n    /* Проверка, пуста ли очередь */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* Поместить в очередь */\n    func push(num: Int) {\n        if size() == capacity() {\n            print(\"Очередь заполнена\")\n            return\n        }\n        // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n        // С помощью операции взятия по модулю вернуть rear к началу после выхода за конец массива\n        let rear = (front + size()) % capacity()\n        // Добавить num в хвост очереди\n        nums[rear] = num\n        _size += 1\n    }\n\n    /* Извлечь из очереди */\n    @discardableResult\n    func pop() -> Int {\n        let num = peek()\n        // Указатель head сдвигается на одну позицию назад; если он выходит за конец, то возвращается в начало массива\n        front = (front + 1) % capacity()\n        _size -= 1\n        return num\n    }\n\n    /* Доступ к элементу в начале очереди */\n    func peek() -> Int {\n        if isEmpty() {\n            fatalError(\"очередь пуста\")\n        }\n        return nums[front]\n    }\n\n    /* Вернуть массив */\n    func toArray() -> [Int] {\n        // Преобразовывать только элементы списка в пределах фактической длины\n        (front ..< front + size()).map { nums[$0 % capacity()] }\n    }\n}\n
        array_queue.js
        /* Очередь на основе кольцевого массива */\nclass ArrayQueue {\n    #nums; // Массив для хранения элементов очереди\n    #front = 0; // Указатель head, указывающий на первый элемент очереди\n    #queSize = 0; // Длина очереди\n\n    constructor(capacity) {\n        this.#nums = new Array(capacity);\n    }\n\n    /* Получить вместимость очереди */\n    get capacity() {\n        return this.#nums.length;\n    }\n\n    /* Получение длины очереди */\n    get size() {\n        return this.#queSize;\n    }\n\n    /* Проверка, пуста ли очередь */\n    isEmpty() {\n        return this.#queSize === 0;\n    }\n\n    /* Поместить в очередь */\n    push(num) {\n        if (this.size === this.capacity) {\n            console.log('Очередь заполнена');\n            return;\n        }\n        // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n        // С помощью операции взятия по модулю вернуть rear к началу после выхода за конец массива\n        const rear = (this.#front + this.size) % this.capacity;\n        // Добавить num в хвост очереди\n        this.#nums[rear] = num;\n        this.#queSize++;\n    }\n\n    /* Извлечь из очереди */\n    pop() {\n        const num = this.peek();\n        // Указатель head сдвигается на одну позицию назад; если он выходит за конец, то возвращается в начало массива\n        this.#front = (this.#front + 1) % this.capacity;\n        this.#queSize--;\n        return num;\n    }\n\n    /* Доступ к элементу в начале очереди */\n    peek() {\n        if (this.isEmpty()) throw new Error('очередь пуста');\n        return this.#nums[this.#front];\n    }\n\n    /* Вернуть Array */\n    toArray() {\n        // Преобразовывать только элементы списка в пределах фактической длины\n        const arr = new Array(this.size);\n        for (let i = 0, j = this.#front; i < this.size; i++, j++) {\n            arr[i] = this.#nums[j % this.capacity];\n        }\n        return arr;\n    }\n}\n
        array_queue.ts
        /* Очередь на основе кольцевого массива */\nclass ArrayQueue {\n    private nums: number[]; // Массив для хранения элементов очереди\n    private front: number; // Указатель head, указывающий на первый элемент очереди\n    private queSize: number; // Длина очереди\n\n    constructor(capacity: number) {\n        this.nums = new Array(capacity);\n        this.front = this.queSize = 0;\n    }\n\n    /* Получить вместимость очереди */\n    get capacity(): number {\n        return this.nums.length;\n    }\n\n    /* Получение длины очереди */\n    get size(): number {\n        return this.queSize;\n    }\n\n    /* Проверка, пуста ли очередь */\n    isEmpty(): boolean {\n        return this.queSize === 0;\n    }\n\n    /* Поместить в очередь */\n    push(num: number): void {\n        if (this.size === this.capacity) {\n            console.log('Очередь заполнена');\n            return;\n        }\n        // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n        // С помощью операции взятия по модулю вернуть rear к началу после выхода за конец массива\n        const rear = (this.front + this.queSize) % this.capacity;\n        // Добавить num в хвост очереди\n        this.nums[rear] = num;\n        this.queSize++;\n    }\n\n    /* Извлечь из очереди */\n    pop(): number {\n        const num = this.peek();\n        // Указатель head сдвигается на одну позицию назад; если он выходит за конец, то возвращается в начало массива\n        this.front = (this.front + 1) % this.capacity;\n        this.queSize--;\n        return num;\n    }\n\n    /* Доступ к элементу в начале очереди */\n    peek(): number {\n        if (this.isEmpty()) throw new Error('очередь пуста');\n        return this.nums[this.front];\n    }\n\n    /* Вернуть Array */\n    toArray(): number[] {\n        // Преобразовывать только элементы списка в пределах фактической длины\n        const arr = new Array(this.size);\n        for (let i = 0, j = this.front; i < this.size; i++, j++) {\n            arr[i] = this.nums[j % this.capacity];\n        }\n        return arr;\n    }\n}\n
        array_queue.dart
        /* Очередь на основе кольцевого массива */\nclass ArrayQueue {\n  late List<int> _nums; // Массив для хранения элементов очереди\n  late int _front; // Указатель head, указывающий на первый элемент очереди\n  late int _queSize; // Длина очереди\n\n  ArrayQueue(int capacity) {\n    _nums = List.filled(capacity, 0);\n    _front = _queSize = 0;\n  }\n\n  /* Получить вместимость очереди */\n  int capaCity() {\n    return _nums.length;\n  }\n\n  /* Получение длины очереди */\n  int size() {\n    return _queSize;\n  }\n\n  /* Проверка, пуста ли очередь */\n  bool isEmpty() {\n    return _queSize == 0;\n  }\n\n  /* Поместить в очередь */\n  void push(int _num) {\n    if (_queSize == capaCity()) {\n      throw Exception(\"Очередь заполнена\");\n    }\n    // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n    // С помощью операции взятия по модулю вернуть rear к началу после выхода за конец массива\n    int rear = (_front + _queSize) % capaCity();\n    // Добавить _num в хвост очереди\n    _nums[rear] = _num;\n    _queSize++;\n  }\n\n  /* Извлечь из очереди */\n  int pop() {\n    int _num = peek();\n    // Указатель head сдвигается на одну позицию назад; если он выходит за конец, то возвращается в начало массива\n    _front = (_front + 1) % capaCity();\n    _queSize--;\n    return _num;\n  }\n\n  /* Доступ к элементу в начале очереди */\n  int peek() {\n    if (isEmpty()) {\n      throw Exception(\"очередь пуста\");\n    }\n    return _nums[_front];\n  }\n\n  /* Вернуть Array */\n  List<int> toArray() {\n    // Преобразовывать только элементы списка в пределах фактической длины\n    final List<int> res = List.filled(_queSize, 0);\n    for (int i = 0, j = _front; i < _queSize; i++, j++) {\n      res[i] = _nums[j % capaCity()];\n    }\n    return res;\n  }\n}\n
        array_queue.rs
        /* Очередь на основе кольцевого массива */\nstruct ArrayQueue<T> {\n    nums: Vec<T>,      // Массив для хранения элементов очереди\n    front: i32,        // Указатель head, указывающий на первый элемент очереди\n    que_size: i32,     // Длина очереди\n    que_capacity: i32, // Вместимость очереди\n}\n\nimpl<T: Copy + Default> ArrayQueue<T> {\n    /* Конструктор */\n    fn new(capacity: i32) -> ArrayQueue<T> {\n        ArrayQueue {\n            nums: vec![T::default(); capacity as usize],\n            front: 0,\n            que_size: 0,\n            que_capacity: capacity,\n        }\n    }\n\n    /* Получить вместимость очереди */\n    fn capacity(&self) -> i32 {\n        self.que_capacity\n    }\n\n    /* Получение длины очереди */\n    fn size(&self) -> i32 {\n        self.que_size\n    }\n\n    /* Проверка, пуста ли очередь */\n    fn is_empty(&self) -> bool {\n        self.que_size == 0\n    }\n\n    /* Поместить в очередь */\n    fn push(&mut self, num: T) {\n        if self.que_size == self.capacity() {\n            println!(\"Очередь заполнена\");\n            return;\n        }\n        // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n        // С помощью операции взятия по модулю вернуть rear к началу после выхода за конец массива\n        let rear = (self.front + self.que_size) % self.que_capacity;\n        // Добавить num в хвост очереди\n        self.nums[rear as usize] = num;\n        self.que_size += 1;\n    }\n\n    /* Извлечь из очереди */\n    fn pop(&mut self) -> T {\n        let num = self.peek();\n        // Указатель head сдвигается на одну позицию назад; если он выходит за конец, то возвращается в начало массива\n        self.front = (self.front + 1) % self.que_capacity;\n        self.que_size -= 1;\n        num\n    }\n\n    /* Доступ к элементу в начале очереди */\n    fn peek(&self) -> T {\n        if self.is_empty() {\n            panic!(\"index out of bounds\");\n        }\n        self.nums[self.front as usize]\n    }\n\n    /* Вернуть массив */\n    fn to_vector(&self) -> Vec<T> {\n        let cap = self.que_capacity;\n        let mut j = self.front;\n        let mut arr = vec![T::default(); cap as usize];\n        for i in 0..self.que_size {\n            arr[i as usize] = self.nums[(j % cap) as usize];\n            j += 1;\n        }\n        arr\n    }\n}\n
        array_queue.c
        /* Очередь на основе кольцевого массива */\ntypedef struct {\n    int *nums;       // Массив для хранения элементов очереди\n    int front;       // Указатель head, указывающий на первый элемент очереди\n    int queSize;     // Текущее количество элементов в очереди\n    int queCapacity; // Вместимость очереди\n} ArrayQueue;\n\n/* Конструктор */\nArrayQueue *newArrayQueue(int capacity) {\n    ArrayQueue *queue = (ArrayQueue *)malloc(sizeof(ArrayQueue));\n    // Инициализация массива\n    queue->queCapacity = capacity;\n    queue->nums = (int *)malloc(sizeof(int) * queue->queCapacity);\n    queue->front = queue->queSize = 0;\n    return queue;\n}\n\n/* Деструктор */\nvoid delArrayQueue(ArrayQueue *queue) {\n    free(queue->nums);\n    free(queue);\n}\n\n/* Получить вместимость очереди */\nint capacity(ArrayQueue *queue) {\n    return queue->queCapacity;\n}\n\n/* Получение длины очереди */\nint size(ArrayQueue *queue) {\n    return queue->queSize;\n}\n\n/* Проверка, пуста ли очередь */\nbool empty(ArrayQueue *queue) {\n    return queue->queSize == 0;\n}\n\n/* Доступ к элементу в начале очереди */\nint peek(ArrayQueue *queue) {\n    assert(size(queue) != 0);\n    return queue->nums[queue->front];\n}\n\n/* Поместить в очередь */\nvoid push(ArrayQueue *queue, int num) {\n    if (size(queue) == capacity(queue)) {\n        printf(\"Очередь заполнена\\r\\n\");\n        return;\n    }\n    // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n    // С помощью операции взятия по модулю вернуть rear к началу после выхода за конец массива\n    int rear = (queue->front + queue->queSize) % queue->queCapacity;\n    // Добавить num в хвост очереди\n    queue->nums[rear] = num;\n    queue->queSize++;\n}\n\n/* Извлечь из очереди */\nint pop(ArrayQueue *queue) {\n    int num = peek(queue);\n    // Указатель head сдвигается на одну позицию назад; если он выходит за конец, то возвращается в начало массива\n    queue->front = (queue->front + 1) % queue->queCapacity;\n    queue->queSize--;\n    return num;\n}\n\n/* Вернуть массив для вывода */\nint *toArray(ArrayQueue *queue, int *queSize) {\n    *queSize = queue->queSize;\n    int *res = (int *)calloc(queue->queSize, sizeof(int));\n    int j = queue->front;\n    for (int i = 0; i < queue->queSize; i++) {\n        res[i] = queue->nums[j % queue->queCapacity];\n        j++;\n    }\n    return res;\n}\n
        array_queue.kt
        /* Очередь на основе кольцевого массива */\nclass ArrayQueue(capacity: Int) {\n    private val nums: IntArray = IntArray(capacity) // Массив для хранения элементов очереди\n    private var front: Int = 0 // Указатель head, указывающий на первый элемент очереди\n    private var queSize: Int = 0 // Длина очереди\n\n    /* Получить вместимость очереди */\n    fun capacity(): Int {\n        return nums.size\n    }\n\n    /* Получение длины очереди */\n    fun size(): Int {\n        return queSize\n    }\n\n    /* Проверка, пуста ли очередь */\n    fun isEmpty(): Boolean {\n        return queSize == 0\n    }\n\n    /* Поместить в очередь */\n    fun push(num: Int) {\n        if (queSize == capacity()) {\n            println(\"Очередь заполнена\")\n            return\n        }\n        // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n        // С помощью операции взятия по модулю вернуть rear к началу после выхода за конец массива\n        val rear = (front + queSize) % capacity()\n        // Добавить num в хвост очереди\n        nums[rear] = num\n        queSize++\n    }\n\n    /* Извлечь из очереди */\n    fun pop(): Int {\n        val num = peek()\n        // Указатель head сдвигается на одну позицию назад; если он выходит за конец, то возвращается в начало массива\n        front = (front + 1) % capacity()\n        queSize--\n        return num\n    }\n\n    /* Доступ к элементу в начале очереди */\n    fun peek(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return nums[front]\n    }\n\n    /* Вернуть массив */\n    fun toArray(): IntArray {\n        // Преобразовывать только элементы списка в пределах фактической длины\n        val res = IntArray(queSize)\n        var i = 0\n        var j = front\n        while (i < queSize) {\n            res[i] = nums[j % capacity()]\n            i++\n            j++\n        }\n        return res\n    }\n}\n
        array_queue.rb
        ### Очередь на основе кольцевого массива ###\nclass ArrayQueue\n  ### Получение длины очереди ###\n  attr_reader :size\n\n  ### Конструктор ###\n  def initialize(size)\n    @nums = Array.new(size, 0) # Массив для хранения элементов очереди\n    @front = 0 # Указатель head, указывающий на первый элемент очереди\n    @size = 0 # Длина очереди\n  end\n\n  ### Получить вместимость очереди ###\n  def capacity\n    @nums.length\n  end\n\n  ### Проверка, пуста ли очередь ###\n  def is_empty?\n    size.zero?\n  end\n\n  ### Добавление в очередь ###\n  def push(num)\n    raise IndexError, 'очередь заполнена' if size == capacity\n\n    # Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n    # С помощью операции взятия по модулю вернуть rear к началу после выхода за конец массива\n    rear = (@front + size) % capacity\n    # Добавить num в хвост очереди\n    @nums[rear] = num\n    @size += 1\n  end\n\n  ### Извлечение из очереди ###\n  def pop\n    num = peek\n    # Указатель head сдвигается на одну позицию назад; если он выходит за конец, то возвращается в начало массива\n    @front = (@front + 1) % capacity\n    @size -= 1\n    num\n  end\n\n  ### Доступ к элементу в начале очереди ###\n  def peek\n    raise IndexError, 'очередь пуста' if is_empty?\n\n    @nums[@front]\n  end\n\n  ### Вернуть список для вывода ###\n  def to_array\n    res = Array.new(size, 0)\n    j = @front\n\n    for i in 0...size\n      res[i] = @nums[j % capacity]\n      j += 1\n    end\n\n    res\n  end\nend\n
        Визуализация кода

        Во весь экран >

        Даже такая реализация очереди остается ограниченной: ее длина неизменяема. Однако это несложно исправить, заменив массив на динамический массив и тем самым введя механизм расширения. Заинтересованные читатели могут попробовать реализовать это самостоятельно.

        Выводы сравнения двух реализаций в целом такие же, как и для стека, поэтому здесь мы не будем повторяться.

        ","path":["Глава 5. Стек и очередь","5.2   Очередь"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#523","level":2,"title":"5.2.3   Типичные применения очереди","text":"
        • Очереди заказов. После оформления заказа покупателем заказ попадает в очередь, а затем система обрабатывает заказы по порядку. Во время крупных распродаж за короткое время возникает огромный поток заказов, и высокая конкурентная нагрузка становится ключевой инженерной проблемой.
        • Различные отложенные задачи. Любой сценарий, где нужно реализовать принцип \"кто раньше пришел, тот раньше обслуживается\", например очередь заданий принтера или очередь блюд на кухне ресторана, хорошо моделируется очередью, которая эффективно поддерживает нужный порядок обработки.
        ","path":["Глава 5. Стек и очередь","5.2   Очередь"],"tags":[]},{"location":"chapter_stack_and_queue/stack/","level":1,"title":"5.1   Стек","text":"

        Стек (stack) - это линейная структура данных, подчиняющаяся логике \"последним пришел - первым вышел\".

        Стек можно сравнить со стопкой тарелок на столе. Если разрешено перемещать только одну тарелку за раз, то, чтобы достать тарелку снизу, сначала придется по одной убрать все тарелки сверху. Если заменить тарелки различными элементами, например целыми числами, символами, объектами и т.д., получится структура данных \"стек\".

        Как показано на рисунке 5-1, верхнюю часть стопки элементов мы называем вершиной стека, а нижнюю - основанием стека. Операция добавления элемента на вершину называется push, а операция удаления верхнего элемента - pop.

        Рисунок 5-1   Правило LIFO для стека

        ","path":["Глава 5. Стек и очередь","5.1   Стек"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#511","level":2,"title":"5.1.1   Основные операции со стеком","text":"

        Основные операции со стеком показаны в таблице 5-1. Конкретные имена методов зависят от используемого языка программирования. Здесь в качестве примера используются распространенные названия push() , pop() и peek() .

        Таблица 5-1   Эффективность операций со стеком

        Метод Описание Временная сложность push() Поместить элемент в стек (на вершину) \\(O(1)\\) pop() Извлечь верхний элемент стека \\(O(1)\\) peek() Просмотреть верхний элемент \\(O(1)\\)

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

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby stack.py
        # Инициализация стека\n# В Python нет встроенного класса стека, поэтому можно использовать list как стек\nstack: list[int] = []\n\n# Поместить элементы в стек\nstack.append(1)\nstack.append(3)\nstack.append(2)\nstack.append(5)\nstack.append(4)\n\n# Просмотреть верхний элемент\npeek: int = stack[-1]\n\n# Извлечь элемент\npop: int = stack.pop()\n\n# Получить длину стека\nsize: int = len(stack)\n\n# Проверить, пуст ли стек\nis_empty: bool = len(stack) == 0\n
        stack.cpp
        /* Инициализация стека */\nstack<int> stack;\n\n/* Поместить элементы в стек */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* Просмотреть верхний элемент */\nint top = stack.top();\n\n/* Извлечь элемент */\nstack.pop(); // Без возвращаемого значения\n\n/* Получить длину стека */\nint size = stack.size();\n\n/* Проверить, пуст ли стек */\nbool empty = stack.empty();\n
        stack.java
        /* Инициализация стека */\nStack<Integer> stack = new Stack<>();\n\n/* Поместить элементы в стек */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* Просмотреть верхний элемент */\nint peek = stack.peek();\n\n/* Извлечь элемент */\nint pop = stack.pop();\n\n/* Получить длину стека */\nint size = stack.size();\n\n/* Проверить, пуст ли стек */\nboolean isEmpty = stack.isEmpty();\n
        stack.cs
        /* Инициализация стека */\nStack<int> stack = new();\n\n/* Поместить элементы в стек */\nstack.Push(1);\nstack.Push(3);\nstack.Push(2);\nstack.Push(5);\nstack.Push(4);\n\n/* Просмотреть верхний элемент */\nint peek = stack.Peek();\n\n/* Извлечь элемент */\nint pop = stack.Pop();\n\n/* Получить длину стека */\nint size = stack.Count;\n\n/* Проверить, пуст ли стек */\nbool isEmpty = stack.Count == 0;\n
        stack_test.go
        /* Инициализация стека */\n// В Go рекомендуется использовать Slice как стек\nvar stack []int\n\n/* Поместить элементы в стек */\nstack = append(stack, 1)\nstack = append(stack, 3)\nstack = append(stack, 2)\nstack = append(stack, 5)\nstack = append(stack, 4)\n\n/* Просмотреть верхний элемент */\npeek := stack[len(stack)-1]\n\n/* Извлечь элемент */\npop := stack[len(stack)-1]\nstack = stack[:len(stack)-1]\n\n/* Получить длину стека */\nsize := len(stack)\n\n/* Проверить, пуст ли стек */\nisEmpty := len(stack) == 0\n
        stack.swift
        /* Инициализация стека */\n// В Swift нет встроенного класса стека, поэтому можно использовать Array как стек\nvar stack: [Int] = []\n\n/* Поместить элементы в стек */\nstack.append(1)\nstack.append(3)\nstack.append(2)\nstack.append(5)\nstack.append(4)\n\n/* Просмотреть верхний элемент */\nlet peek = stack.last!\n\n/* Извлечь элемент */\nlet pop = stack.removeLast()\n\n/* Получить длину стека */\nlet size = stack.count\n\n/* Проверить, пуст ли стек */\nlet isEmpty = stack.isEmpty\n
        stack.js
        /* Инициализация стека */\n// В JavaScript нет встроенного класса стека, поэтому можно использовать Array как стек\nconst stack = [];\n\n/* Поместить элементы в стек */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* Просмотреть верхний элемент */\nconst peek = stack[stack.length-1];\n\n/* Извлечь элемент */\nconst pop = stack.pop();\n\n/* Получить длину стека */\nconst size = stack.length;\n\n/* Проверить, пуст ли стек */\nconst is_empty = stack.length === 0;\n
        stack.ts
        /* Инициализация стека */\n// В TypeScript нет встроенного класса стека, поэтому можно использовать Array как стек\nconst stack: number[] = [];\n\n/* Поместить элементы в стек */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* Просмотреть верхний элемент */\nconst peek = stack[stack.length - 1];\n\n/* Извлечь элемент */\nconst pop = stack.pop();\n\n/* Получить длину стека */\nconst size = stack.length;\n\n/* Проверить, пуст ли стек */\nconst is_empty = stack.length === 0;\n
        stack.dart
        /* Инициализация стека */\n// В Dart нет встроенного класса стека, поэтому можно использовать List как стек\nList<int> stack = [];\n\n/* Поместить элементы в стек */\nstack.add(1);\nstack.add(3);\nstack.add(2);\nstack.add(5);\nstack.add(4);\n\n/* Просмотреть верхний элемент */\nint peek = stack.last;\n\n/* Извлечь элемент */\nint pop = stack.removeLast();\n\n/* Получить длину стека */\nint size = stack.length;\n\n/* Проверить, пуст ли стек */\nbool isEmpty = stack.isEmpty;\n
        stack.rs
        /* Инициализация стека */\n// Используем Vec как стек\nlet mut stack: Vec<i32> = Vec::new();\n\n/* Поместить элементы в стек */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* Просмотреть верхний элемент */\nlet top = stack.last().unwrap();\n\n/* Извлечь элемент */\nlet pop = stack.pop().unwrap();\n\n/* Получить длину стека */\nlet size = stack.len();\n\n/* Проверить, пуст ли стек */\nlet is_empty = stack.is_empty();\n
        stack.c
        // В C нет встроенного стека\n
        stack.kt
        /* Инициализация стека */\nval stack = Stack<Int>()\n\n/* Поместить элементы в стек */\nstack.push(1)\nstack.push(3)\nstack.push(2)\nstack.push(5)\nstack.push(4)\n\n/* Просмотреть верхний элемент */\nval peek = stack.peek()\n\n/* Извлечь элемент */\nval pop = stack.pop()\n\n/* Получить длину стека */\nval size = stack.size\n\n/* Проверить, пуст ли стек */\nval isEmpty = stack.isEmpty()\n
        stack.rb
        # Инициализация стека\n# В Ruby нет встроенного класса стека, поэтому можно использовать Array как стек\nstack = []\n\n# Поместить элементы в стек\nstack << 1\nstack << 3\nstack << 2\nstack << 5\nstack << 4\n\n# Просмотреть верхний элемент\npeek = stack.last\n\n# Извлечь элемент\npop = stack.pop\n\n# Получить длину стека\nsize = stack.length\n\n# Проверить, пуст ли стек\nis_empty = stack.empty?\n
        Визуализация выполнения

        https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%81%D1%82%D0%B5%D0%BA%0A%20%20%20%20%23%20%D0%92%20Python%20%D0%BD%D0%B5%D1%82%20%D0%B2%D1%81%D1%82%D1%80%D0%BE%D0%B5%D0%BD%D0%BD%D0%BE%D0%B3%D0%BE%20%D0%BA%D0%BB%D0%B0%D1%81%D1%81%D0%B0%20%D1%81%D1%82%D0%B5%D0%BA%D0%B0%2C%20%D0%BF%D0%BE%D1%8D%D1%82%D0%BE%D0%BC%D1%83%20list%20%D0%BC%D0%BE%D0%B6%D0%BD%D0%BE%20%D0%B8%D1%81%D0%BF%D0%BE%D0%BB%D1%8C%D0%B7%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D0%BA%D0%B0%D0%BA%20%D1%81%D1%82%D0%B5%D0%BA%0A%20%20%20%20stack%20%3D%20%5B%5D%0A%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BC%D0%B5%D1%81%D1%82%D0%B8%D1%82%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B2%20%D1%81%D1%82%D0%B5%D0%BA%0A%20%20%20%20stack.append%281%29%0A%20%20%20%20stack.append%283%29%0A%20%20%20%20stack.append%282%29%0A%20%20%20%20stack.append%285%29%0A%20%20%20%20stack.append%284%29%0A%20%20%20%20print%28%22%D1%81%D1%82%D0%B5%D0%BA%20stack%20%3D%22%2C%20stack%29%0A%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B8%D1%82%D1%8C%20%D0%B2%D0%B5%D1%80%D1%85%D0%BD%D0%B8%D0%B9%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D1%81%D1%82%D0%B5%D0%BA%D0%B0%0A%20%20%20%20peek%20%3D%20stack%5B-1%5D%0A%20%20%20%20print%28%22%D0%92%D0%B5%D1%80%D1%85%D0%BD%D0%B8%D0%B9%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D1%81%D1%82%D0%B5%D0%BA%D0%B0%20peek%20%3D%22%2C%20peek%29%0A%0A%20%20%20%20%23%20%D0%98%D0%B7%D0%B2%D0%BB%D0%B5%D1%87%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B8%D0%B7%20%D1%81%D1%82%D0%B5%D0%BA%D0%B0%0A%20%20%20%20pop%20%3D%20stack.pop%28%29%0A%20%20%20%20print%28%22%D0%98%D0%B7%D0%B2%D0%BB%D0%B5%D1%87%D0%B5%D0%BD%D0%BD%D1%8B%D0%B9%20%D0%B8%D0%B7%20%D1%81%D1%82%D0%B5%D0%BA%D0%B0%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20pop%20%3D%22%2C%20pop%29%0A%20%20%20%20print%28%22%D0%9F%D0%BE%D1%81%D0%BB%D0%B5%20%D0%B8%D0%B7%D0%B2%D0%BB%D0%B5%D1%87%D0%B5%D0%BD%D0%B8%D1%8F%20stack%20%3D%22%2C%20stack%29%0A%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B8%D1%82%D1%8C%20%D0%B4%D0%BB%D0%B8%D0%BD%D1%83%20%D1%81%D1%82%D0%B5%D0%BA%D0%B0%0A%20%20%20%20size%20%3D%20len%28stack%29%0A%20%20%20%20print%28%22%D0%94%D0%BB%D0%B8%D0%BD%D0%B0%20%D1%81%D1%82%D0%B5%D0%BA%D0%B0%20size%20%3D%22%2C%20size%29%0A%0A%20%20%20%20%23%20%D0%9F%D1%80%D0%BE%D0%B2%D0%B5%D1%80%D0%B8%D1%82%D1%8C%2C%20%D0%BF%D1%83%D1%81%D1%82%D0%B0%20%D0%BB%D0%B8%20%D1%81%D1%82%D1%80%D1%83%D0%BA%D1%82%D1%83%D1%80%D0%B0%0A%20%20%20%20is_empty%20%3D%20len%28stack%29%20%3D%3D%200%0A%20%20%20%20print%28%22%D0%9F%D1%83%D1%81%D1%82%20%D0%BB%D0%B8%20%D1%81%D1%82%D0%B5%D0%BA%20%3D%22%2C%20is_empty%29&cumulative=false&curInstr=2&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

        ","path":["Глава 5. Стек и очередь","5.1   Стек"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#512","level":2,"title":"5.1.2   Реализация стека","text":"

        Чтобы глубже понять механизм работы стека, попробуем самостоятельно реализовать класс стека.

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

        ","path":["Глава 5. Стек и очередь","5.1   Стек"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#1","level":3,"title":"1.   Реализация на основе связного списка","text":"

        Если реализовывать стек на основе связного списка, то головной узел списка можно рассматривать как вершину стека, а хвостовой - как основание.

        Как показано на рисунке 5-2, для операции push достаточно вставить элемент в голову связного списка. Такой способ вставки называется вставкой в голову. Для операции pop достаточно удалить головной узел из списка.

        <1><2><3>

        Рисунок 5-2   Операции push и pop в реализации стека на связном списке

        Ниже приведен пример кода реализации стека на основе связного списка:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linkedlist_stack.py
        class LinkedListStack:\n    \"\"\"Стек на основе связного списка\"\"\"\n\n    def __init__(self):\n        \"\"\"Конструктор\"\"\"\n        self._peek: ListNode | None = None\n        self._size: int = 0\n\n    def size(self) -> int:\n        \"\"\"Получение длины стека\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"Проверка, пуст ли стек\"\"\"\n        return self._size == 0\n\n    def push(self, val: int):\n        \"\"\"Поместить в стек\"\"\"\n        node = ListNode(val)\n        node.next = self._peek\n        self._peek = node\n        self._size += 1\n\n    def pop(self) -> int:\n        \"\"\"Извлечь из стека\"\"\"\n        num = self.peek()\n        self._peek = self._peek.next\n        self._size -= 1\n        return num\n\n    def peek(self) -> int:\n        \"\"\"Доступ к верхнему элементу стека\"\"\"\n        if self.is_empty():\n            raise IndexError(\"стек пуст\")\n        return self._peek.val\n\n    def to_list(self) -> list[int]:\n        \"\"\"Преобразовать в список для вывода\"\"\"\n        arr = []\n        node = self._peek\n        while node:\n            arr.append(node.val)\n            node = node.next\n        arr.reverse()\n        return arr\n
        linkedlist_stack.cpp
        /* Стек на основе связного списка */\nclass LinkedListStack {\n  private:\n    ListNode *stackTop; // Использовать головной узел как вершину стека\n    int stkSize;        // Длина стека\n\n  public:\n    LinkedListStack() {\n        stackTop = nullptr;\n        stkSize = 0;\n    }\n\n    ~LinkedListStack() {\n        // Обходить связный список, удалять узлы и освобождать память\n        freeMemoryLinkedList(stackTop);\n    }\n\n    /* Получение длины стека */\n    int size() {\n        return stkSize;\n    }\n\n    /* Проверка, пуст ли стек */\n    bool isEmpty() {\n        return size() == 0;\n    }\n\n    /* Поместить в стек */\n    void push(int num) {\n        ListNode *node = new ListNode(num);\n        node->next = stackTop;\n        stackTop = node;\n        stkSize++;\n    }\n\n    /* Извлечь из стека */\n    int pop() {\n        int num = top();\n        ListNode *tmp = stackTop;\n        stackTop = stackTop->next;\n        // Освободить память\n        delete tmp;\n        stkSize--;\n        return num;\n    }\n\n    /* Доступ к верхнему элементу стека */\n    int top() {\n        if (isEmpty())\n            throw out_of_range(\"стек пуст\");\n        return stackTop->val;\n    }\n\n    /* Преобразовать List в Array и вернуть */\n    vector<int> toVector() {\n        ListNode *node = stackTop;\n        vector<int> res(size());\n        for (int i = res.size() - 1; i >= 0; i--) {\n            res[i] = node->val;\n            node = node->next;\n        }\n        return res;\n    }\n};\n
        linkedlist_stack.java
        /* Стек на основе связного списка */\nclass LinkedListStack {\n    private ListNode stackPeek; // Использовать головной узел как вершину стека\n    private int stkSize = 0; // Длина стека\n\n    public LinkedListStack() {\n        stackPeek = null;\n    }\n\n    /* Получение длины стека */\n    public int size() {\n        return stkSize;\n    }\n\n    /* Проверка, пуст ли стек */\n    public boolean isEmpty() {\n        return size() == 0;\n    }\n\n    /* Поместить в стек */\n    public void push(int num) {\n        ListNode node = new ListNode(num);\n        node.next = stackPeek;\n        stackPeek = node;\n        stkSize++;\n    }\n\n    /* Извлечь из стека */\n    public int pop() {\n        int num = peek();\n        stackPeek = stackPeek.next;\n        stkSize--;\n        return num;\n    }\n\n    /* Доступ к верхнему элементу стека */\n    public int peek() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return stackPeek.val;\n    }\n\n    /* Преобразовать List в Array и вернуть */\n    public int[] toArray() {\n        ListNode node = stackPeek;\n        int[] res = new int[size()];\n        for (int i = res.length - 1; i >= 0; i--) {\n            res[i] = node.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
        linkedlist_stack.cs
        /* Стек на основе связного списка */\nclass LinkedListStack {\n    ListNode? stackPeek;  // Использовать головной узел как вершину стека\n    int stkSize = 0;   // Длина стека\n\n    public LinkedListStack() {\n        stackPeek = null;\n    }\n\n    /* Получение длины стека */\n    public int Size() {\n        return stkSize;\n    }\n\n    /* Проверка, пуст ли стек */\n    public bool IsEmpty() {\n        return Size() == 0;\n    }\n\n    /* Поместить в стек */\n    public void Push(int num) {\n        ListNode node = new(num) {\n            next = stackPeek\n        };\n        stackPeek = node;\n        stkSize++;\n    }\n\n    /* Извлечь из стека */\n    public int Pop() {\n        int num = Peek();\n        stackPeek = stackPeek!.next;\n        stkSize--;\n        return num;\n    }\n\n    /* Доступ к верхнему элементу стека */\n    public int Peek() {\n        if (IsEmpty())\n            throw new Exception();\n        return stackPeek!.val;\n    }\n\n    /* Преобразовать List в Array и вернуть */\n    public int[] ToArray() {\n        if (stackPeek == null)\n            return [];\n\n        ListNode? node = stackPeek;\n        int[] res = new int[Size()];\n        for (int i = res.Length - 1; i >= 0; i--) {\n            res[i] = node!.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
        linkedlist_stack.go
        /* Стек на основе связного списка */\ntype linkedListStack struct {\n    // Использовать встроенный пакет list для реализации стека\n    data *list.List\n}\n\n/* Инициализация стека */\nfunc newLinkedListStack() *linkedListStack {\n    return &linkedListStack{\n        data: list.New(),\n    }\n}\n\n/* Поместить в стек */\nfunc (s *linkedListStack) push(value int) {\n    s.data.PushBack(value)\n}\n\n/* Извлечь из стека */\nfunc (s *linkedListStack) pop() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Back()\n    s.data.Remove(e)\n    return e.Value\n}\n\n/* Доступ к верхнему элементу стека */\nfunc (s *linkedListStack) peek() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Back()\n    return e.Value\n}\n\n/* Получение длины стека */\nfunc (s *linkedListStack) size() int {\n    return s.data.Len()\n}\n\n/* Проверка, пуст ли стек */\nfunc (s *linkedListStack) isEmpty() bool {\n    return s.data.Len() == 0\n}\n\n/* Получить List для вывода */\nfunc (s *linkedListStack) toList() *list.List {\n    return s.data\n}\n
        linkedlist_stack.swift
        /* Стек на основе связного списка */\nclass LinkedListStack {\n    private var _peek: ListNode? // Использовать головной узел как вершину стека\n    private var _size: Int // Длина стека\n\n    init() {\n        _size = 0\n    }\n\n    /* Получение длины стека */\n    func size() -> Int {\n        _size\n    }\n\n    /* Проверка, пуст ли стек */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* Поместить в стек */\n    func push(num: Int) {\n        let node = ListNode(x: num)\n        node.next = _peek\n        _peek = node\n        _size += 1\n    }\n\n    /* Извлечь из стека */\n    @discardableResult\n    func pop() -> Int {\n        let num = peek()\n        _peek = _peek?.next\n        _size -= 1\n        return num\n    }\n\n    /* Доступ к верхнему элементу стека */\n    func peek() -> Int {\n        if isEmpty() {\n            fatalError(\"стек пуст\")\n        }\n        return _peek!.val\n    }\n\n    /* Преобразовать List в Array и вернуть */\n    func toArray() -> [Int] {\n        var node = _peek\n        var res = Array(repeating: 0, count: size())\n        for i in res.indices.reversed() {\n            res[i] = node!.val\n            node = node?.next\n        }\n        return res\n    }\n}\n
        linkedlist_stack.js
        /* Стек на основе связного списка */\nclass LinkedListStack {\n    #stackPeek; // Использовать головной узел как вершину стека\n    #stkSize = 0; // Длина стека\n\n    constructor() {\n        this.#stackPeek = null;\n    }\n\n    /* Получение длины стека */\n    get size() {\n        return this.#stkSize;\n    }\n\n    /* Проверка, пуст ли стек */\n    isEmpty() {\n        return this.size === 0;\n    }\n\n    /* Поместить в стек */\n    push(num) {\n        const node = new ListNode(num);\n        node.next = this.#stackPeek;\n        this.#stackPeek = node;\n        this.#stkSize++;\n    }\n\n    /* Извлечь из стека */\n    pop() {\n        const num = this.peek();\n        this.#stackPeek = this.#stackPeek.next;\n        this.#stkSize--;\n        return num;\n    }\n\n    /* Доступ к верхнему элементу стека */\n    peek() {\n        if (!this.#stackPeek) throw new Error('стек пуст');\n        return this.#stackPeek.val;\n    }\n\n    /* Преобразовать связный список в Array и вернуть */\n    toArray() {\n        let node = this.#stackPeek;\n        const res = new Array(this.size);\n        for (let i = res.length - 1; i >= 0; i--) {\n            res[i] = node.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
        linkedlist_stack.ts
        /* Стек на основе связного списка */\nclass LinkedListStack {\n    private stackPeek: ListNode | null; // Использовать головной узел как вершину стека\n    private stkSize: number = 0; // Длина стека\n\n    constructor() {\n        this.stackPeek = null;\n    }\n\n    /* Получение длины стека */\n    get size(): number {\n        return this.stkSize;\n    }\n\n    /* Проверка, пуст ли стек */\n    isEmpty(): boolean {\n        return this.size === 0;\n    }\n\n    /* Поместить в стек */\n    push(num: number): void {\n        const node = new ListNode(num);\n        node.next = this.stackPeek;\n        this.stackPeek = node;\n        this.stkSize++;\n    }\n\n    /* Извлечь из стека */\n    pop(): number {\n        const num = this.peek();\n        if (!this.stackPeek) throw new Error('стек пуст');\n        this.stackPeek = this.stackPeek.next;\n        this.stkSize--;\n        return num;\n    }\n\n    /* Доступ к верхнему элементу стека */\n    peek(): number {\n        if (!this.stackPeek) throw new Error('стек пуст');\n        return this.stackPeek.val;\n    }\n\n    /* Преобразовать связный список в Array и вернуть */\n    toArray(): number[] {\n        let node = this.stackPeek;\n        const res = new Array<number>(this.size);\n        for (let i = res.length - 1; i >= 0; i--) {\n            res[i] = node!.val;\n            node = node!.next;\n        }\n        return res;\n    }\n}\n
        linkedlist_stack.dart
        /* Стек на основе класса связного списка */\nclass LinkedListStack {\n  ListNode? _stackPeek; // Использовать головной узел как вершину стека\n  int _stkSize = 0; // Длина стека\n\n  LinkedListStack() {\n    _stackPeek = null;\n  }\n\n  /* Получение длины стека */\n  int size() {\n    return _stkSize;\n  }\n\n  /* Проверка, пуст ли стек */\n  bool isEmpty() {\n    return _stkSize == 0;\n  }\n\n  /* Поместить в стек */\n  void push(int _num) {\n    final ListNode node = ListNode(_num);\n    node.next = _stackPeek;\n    _stackPeek = node;\n    _stkSize++;\n  }\n\n  /* Извлечь из стека */\n  int pop() {\n    final int _num = peek();\n    _stackPeek = _stackPeek!.next;\n    _stkSize--;\n    return _num;\n  }\n\n  /* Доступ к верхнему элементу стека */\n  int peek() {\n    if (_stackPeek == null) {\n      throw Exception(\"стек пуст\");\n    }\n    return _stackPeek!.val;\n  }\n\n  /* Преобразовать связный список в List и вернуть */\n  List<int> toList() {\n    ListNode? node = _stackPeek;\n    List<int> list = [];\n    while (node != null) {\n      list.add(node.val);\n      node = node.next;\n    }\n    list = list.reversed.toList();\n    return list;\n  }\n}\n
        linkedlist_stack.rs
        /* Стек на основе связного списка */\n#[allow(dead_code)]\npub struct LinkedListStack<T> {\n    stack_peek: Option<Rc<RefCell<ListNode<T>>>>, // Использовать головной узел как вершину стека\n    stk_size: usize,                              // Длина стека\n}\n\nimpl<T: Copy> LinkedListStack<T> {\n    pub fn new() -> Self {\n        Self {\n            stack_peek: None,\n            stk_size: 0,\n        }\n    }\n\n    /* Получение длины стека */\n    pub fn size(&self) -> usize {\n        return self.stk_size;\n    }\n\n    /* Проверка, пуст ли стек */\n    pub fn is_empty(&self) -> bool {\n        return self.size() == 0;\n    }\n\n    /* Поместить в стек */\n    pub fn push(&mut self, num: T) {\n        let node = ListNode::new(num);\n        node.borrow_mut().next = self.stack_peek.take();\n        self.stack_peek = Some(node);\n        self.stk_size += 1;\n    }\n\n    /* Извлечь из стека */\n    pub fn pop(&mut self) -> Option<T> {\n        self.stack_peek.take().map(|old_head| {\n            self.stack_peek = old_head.borrow_mut().next.take();\n            self.stk_size -= 1;\n\n            old_head.borrow().val\n        })\n    }\n\n    /* Доступ к верхнему элементу стека */\n    pub fn peek(&self) -> Option<&Rc<RefCell<ListNode<T>>>> {\n        self.stack_peek.as_ref()\n    }\n\n    /* Преобразовать List в Array и вернуть */\n    pub fn to_array(&self) -> Vec<T> {\n        fn _to_array<T: Sized + Copy>(head: Option<&Rc<RefCell<ListNode<T>>>>) -> Vec<T> {\n            if let Some(node) = head {\n                let mut nums = _to_array(node.borrow().next.as_ref());\n                nums.push(node.borrow().val);\n                return nums;\n            }\n            return Vec::new();\n        }\n\n        _to_array(self.peek())\n    }\n}\n
        linkedlist_stack.c
        /* Стек на основе связного списка */\ntypedef struct {\n    ListNode *top; // Использовать головной узел как вершину стека\n    int size;      // Длина стека\n} LinkedListStack;\n\n/* Конструктор */\nLinkedListStack *newLinkedListStack() {\n    LinkedListStack *s = malloc(sizeof(LinkedListStack));\n    s->top = NULL;\n    s->size = 0;\n    return s;\n}\n\n/* Деструктор */\nvoid delLinkedListStack(LinkedListStack *s) {\n    while (s->top) {\n        ListNode *n = s->top->next;\n        free(s->top);\n        s->top = n;\n    }\n    free(s);\n}\n\n/* Получение длины стека */\nint size(LinkedListStack *s) {\n    return s->size;\n}\n\n/* Проверка, пуст ли стек */\nbool isEmpty(LinkedListStack *s) {\n    return size(s) == 0;\n}\n\n/* Поместить в стек */\nvoid push(LinkedListStack *s, int num) {\n    ListNode *node = (ListNode *)malloc(sizeof(ListNode));\n    node->next = s->top; // Обновить поле указателя нового узла\n    node->val = num;     // Обновить поле данных нового узла\n    s->top = node;       // Обновить вершину стека\n    s->size++;           // Обновить размер стека\n}\n\n/* Доступ к верхнему элементу стека */\nint peek(LinkedListStack *s) {\n    if (s->size == 0) {\n        printf(\"стек пуст\\n\");\n        return INT_MAX;\n    }\n    return s->top->val;\n}\n\n/* Извлечь из стека */\nint pop(LinkedListStack *s) {\n    int val = peek(s);\n    ListNode *tmp = s->top;\n    s->top = s->top->next;\n    // Освободить память\n    free(tmp);\n    s->size--;\n    return val;\n}\n
        linkedlist_stack.kt
        /* Стек на основе связного списка */\nclass LinkedListStack(\n    private var stackPeek: ListNode? = null, // Использовать головной узел как вершину стека\n    private var stkSize: Int = 0 // Длина стека\n) {\n\n    /* Получение длины стека */\n    fun size(): Int {\n        return stkSize\n    }\n\n    /* Проверка, пуст ли стек */\n    fun isEmpty(): Boolean {\n        return size() == 0\n    }\n\n    /* Поместить в стек */\n    fun push(num: Int) {\n        val node = ListNode(num)\n        node.next = stackPeek\n        stackPeek = node\n        stkSize++\n    }\n\n    /* Извлечь из стека */\n    fun pop(): Int? {\n        val num = peek()\n        stackPeek = stackPeek?.next\n        stkSize--\n        return num\n    }\n\n    /* Доступ к верхнему элементу стека */\n    fun peek(): Int? {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return stackPeek?._val\n    }\n\n    /* Преобразовать List в Array и вернуть */\n    fun toArray(): IntArray {\n        var node = stackPeek\n        val res = IntArray(size())\n        for (i in res.size - 1 downTo 0) {\n            res[i] = node?._val!!\n            node = node.next\n        }\n        return res\n    }\n}\n
        linkedlist_stack.rb
        ### Стек на основе связного списка ###\nclass LinkedListStack\n  attr_reader :size\n\n  ### Конструктор ###\n  def initialize\n    @size = 0\n  end\n\n  ### Проверка, пуст ли стек ###\n  def is_empty?\n    @peek.nil?\n  end\n\n  ### Помещение в стек ###\n  def push(val)\n    node = ListNode.new(val)\n    node.next = @peek\n    @peek = node\n    @size += 1\n  end\n\n  ### Извлечение из стека ###\n  def pop\n    num = peek\n    @peek = @peek.next\n    @size -= 1\n    num\n  end\n\n  ### Доступ к верхнему элементу стека ###\n  def peek\n    raise IndexError, 'стек пуст' if is_empty?\n\n    @peek.val\n  end\n\n  ### Преобразовать связный список в Array и вернуть ###\n  def to_array\n    arr = []\n    node = @peek\n    while node\n      arr << node.val\n      node = node.next\n    end\n    arr.reverse\n  end\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 5. Стек и очередь","5.1   Стек"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#2","level":3,"title":"2.   Реализация на основе массива","text":"

        Если реализовывать стек на основе массива, то хвост массива можно рассматривать как вершину стека. Как показано на рисунке 5-3, операции push и pop соответствуют добавлению элемента в конец массива и удалению элемента из конца, обе имеют временную сложность \\(O(1)\\) .

        <1><2><3>

        Рисунок 5-3   Операции push и pop в реализации стека на массиве

        Поскольку количество элементов, помещаемых в стек, может непрерывно расти, мы можем использовать динамический массив и тем самым не заниматься расширением массива вручную. Ниже приведен пример кода:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_stack.py
        class ArrayStack:\n    \"\"\"Стек на основе массива\"\"\"\n\n    def __init__(self):\n        \"\"\"Конструктор\"\"\"\n        self._stack: list[int] = []\n\n    def size(self) -> int:\n        \"\"\"Получение длины стека\"\"\"\n        return len(self._stack)\n\n    def is_empty(self) -> bool:\n        \"\"\"Проверка, пуст ли стек\"\"\"\n        return self.size() == 0\n\n    def push(self, item: int):\n        \"\"\"Поместить в стек\"\"\"\n        self._stack.append(item)\n\n    def pop(self) -> int:\n        \"\"\"Извлечь из стека\"\"\"\n        if self.is_empty():\n            raise IndexError(\"стек пуст\")\n        return self._stack.pop()\n\n    def peek(self) -> int:\n        \"\"\"Доступ к верхнему элементу стека\"\"\"\n        if self.is_empty():\n            raise IndexError(\"стек пуст\")\n        return self._stack[-1]\n\n    def to_list(self) -> list[int]:\n        \"\"\"Вернуть список для вывода\"\"\"\n        return self._stack\n
        array_stack.cpp
        /* Стек на основе массива */\nclass ArrayStack {\n  private:\n    vector<int> stack;\n\n  public:\n    /* Получение длины стека */\n    int size() {\n        return stack.size();\n    }\n\n    /* Проверка, пуст ли стек */\n    bool isEmpty() {\n        return stack.size() == 0;\n    }\n\n    /* Поместить в стек */\n    void push(int num) {\n        stack.push_back(num);\n    }\n\n    /* Извлечь из стека */\n    int pop() {\n        int num = top();\n        stack.pop_back();\n        return num;\n    }\n\n    /* Доступ к верхнему элементу стека */\n    int top() {\n        if (isEmpty())\n            throw out_of_range(\"стек пуст\");\n        return stack.back();\n    }\n\n    /* Вернуть Vector */\n    vector<int> toVector() {\n        return stack;\n    }\n};\n
        array_stack.java
        /* Стек на основе массива */\nclass ArrayStack {\n    private ArrayList<Integer> stack;\n\n    public ArrayStack() {\n        // Инициализация списка (динамического массива)\n        stack = new ArrayList<>();\n    }\n\n    /* Получение длины стека */\n    public int size() {\n        return stack.size();\n    }\n\n    /* Проверка, пуст ли стек */\n    public boolean isEmpty() {\n        return size() == 0;\n    }\n\n    /* Поместить в стек */\n    public void push(int num) {\n        stack.add(num);\n    }\n\n    /* Извлечь из стека */\n    public int pop() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return stack.remove(size() - 1);\n    }\n\n    /* Доступ к верхнему элементу стека */\n    public int peek() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return stack.get(size() - 1);\n    }\n\n    /* Преобразовать List в Array и вернуть */\n    public Object[] toArray() {\n        return stack.toArray();\n    }\n}\n
        array_stack.cs
        /* Стек на основе массива */\nclass ArrayStack {\n    List<int> stack;\n    public ArrayStack() {\n        // Инициализация списка (динамического массива)\n        stack = [];\n    }\n\n    /* Получение длины стека */\n    public int Size() {\n        return stack.Count;\n    }\n\n    /* Проверка, пуст ли стек */\n    public bool IsEmpty() {\n        return Size() == 0;\n    }\n\n    /* Поместить в стек */\n    public void Push(int num) {\n        stack.Add(num);\n    }\n\n    /* Извлечь из стека */\n    public int Pop() {\n        if (IsEmpty())\n            throw new Exception();\n        var val = Peek();\n        stack.RemoveAt(Size() - 1);\n        return val;\n    }\n\n    /* Доступ к верхнему элементу стека */\n    public int Peek() {\n        if (IsEmpty())\n            throw new Exception();\n        return stack[Size() - 1];\n    }\n\n    /* Преобразовать List в Array и вернуть */\n    public int[] ToArray() {\n        return [.. stack];\n    }\n}\n
        array_stack.go
        /* Стек на основе массива */\ntype arrayStack struct {\n    data []int // Данные\n}\n\n/* Инициализация стека */\nfunc newArrayStack() *arrayStack {\n    return &arrayStack{\n        // Установить длину стека равной 0, а емкость равной 16\n        data: make([]int, 0, 16),\n    }\n}\n\n/* Длина стека */\nfunc (s *arrayStack) size() int {\n    return len(s.data)\n}\n\n/* Пуст ли стек */\nfunc (s *arrayStack) isEmpty() bool {\n    return s.size() == 0\n}\n\n/* Поместить в стек */\nfunc (s *arrayStack) push(v int) {\n    // Срез автоматически расширяется\n    s.data = append(s.data, v)\n}\n\n/* Извлечь из стека */\nfunc (s *arrayStack) pop() any {\n    val := s.peek()\n    s.data = s.data[:len(s.data)-1]\n    return val\n}\n\n/* Получить элемент на вершине стека */\nfunc (s *arrayStack) peek() any {\n    if s.isEmpty() {\n        return nil\n    }\n    val := s.data[len(s.data)-1]\n    return val\n}\n\n/* Получить Slice для вывода */\nfunc (s *arrayStack) toSlice() []int {\n    return s.data\n}\n
        array_stack.swift
        /* Стек на основе массива */\nclass ArrayStack {\n    private var stack: [Int]\n\n    init() {\n        // Инициализация списка (динамического массива)\n        stack = []\n    }\n\n    /* Получение длины стека */\n    func size() -> Int {\n        stack.count\n    }\n\n    /* Проверка, пуст ли стек */\n    func isEmpty() -> Bool {\n        stack.isEmpty\n    }\n\n    /* Поместить в стек */\n    func push(num: Int) {\n        stack.append(num)\n    }\n\n    /* Извлечь из стека */\n    @discardableResult\n    func pop() -> Int {\n        if isEmpty() {\n            fatalError(\"стек пуст\")\n        }\n        return stack.removeLast()\n    }\n\n    /* Доступ к верхнему элементу стека */\n    func peek() -> Int {\n        if isEmpty() {\n            fatalError(\"стек пуст\")\n        }\n        return stack.last!\n    }\n\n    /* Преобразовать List в Array и вернуть */\n    func toArray() -> [Int] {\n        stack\n    }\n}\n
        array_stack.js
        /* Стек на основе массива */\nclass ArrayStack {\n    #stack;\n    constructor() {\n        this.#stack = [];\n    }\n\n    /* Получение длины стека */\n    get size() {\n        return this.#stack.length;\n    }\n\n    /* Проверка, пуст ли стек */\n    isEmpty() {\n        return this.#stack.length === 0;\n    }\n\n    /* Поместить в стек */\n    push(num) {\n        this.#stack.push(num);\n    }\n\n    /* Извлечь из стека */\n    pop() {\n        if (this.isEmpty()) throw new Error('стек пуст');\n        return this.#stack.pop();\n    }\n\n    /* Доступ к верхнему элементу стека */\n    top() {\n        if (this.isEmpty()) throw new Error('стек пуст');\n        return this.#stack[this.#stack.length - 1];\n    }\n\n    /* Вернуть Array */\n    toArray() {\n        return this.#stack;\n    }\n}\n
        array_stack.ts
        /* Стек на основе массива */\nclass ArrayStack {\n    private stack: number[];\n    constructor() {\n        this.stack = [];\n    }\n\n    /* Получение длины стека */\n    get size(): number {\n        return this.stack.length;\n    }\n\n    /* Проверка, пуст ли стек */\n    isEmpty(): boolean {\n        return this.stack.length === 0;\n    }\n\n    /* Поместить в стек */\n    push(num: number): void {\n        this.stack.push(num);\n    }\n\n    /* Извлечь из стека */\n    pop(): number | undefined {\n        if (this.isEmpty()) throw new Error('стек пуст');\n        return this.stack.pop();\n    }\n\n    /* Доступ к верхнему элементу стека */\n    top(): number | undefined {\n        if (this.isEmpty()) throw new Error('стек пуст');\n        return this.stack[this.stack.length - 1];\n    }\n\n    /* Вернуть Array */\n    toArray() {\n        return this.stack;\n    }\n}\n
        array_stack.dart
        /* Стек на основе массива */\nclass ArrayStack {\n  late List<int> _stack;\n  ArrayStack() {\n    _stack = [];\n  }\n\n  /* Получение длины стека */\n  int size() {\n    return _stack.length;\n  }\n\n  /* Проверка, пуст ли стек */\n  bool isEmpty() {\n    return _stack.isEmpty;\n  }\n\n  /* Поместить в стек */\n  void push(int _num) {\n    _stack.add(_num);\n  }\n\n  /* Извлечь из стека */\n  int pop() {\n    if (isEmpty()) {\n      throw Exception(\"стек пуст\");\n    }\n    return _stack.removeLast();\n  }\n\n  /* Доступ к верхнему элементу стека */\n  int peek() {\n    if (isEmpty()) {\n      throw Exception(\"стек пуст\");\n    }\n    return _stack.last;\n  }\n\n  /* Преобразовать стек в Array и вернуть */\n  List<int> toArray() => _stack;\n}\n
        array_stack.rs
        /* Стек на основе массива */\nstruct ArrayStack<T> {\n    stack: Vec<T>,\n}\n\nimpl<T> ArrayStack<T> {\n    /* Инициализация стека */\n    fn new() -> ArrayStack<T> {\n        ArrayStack::<T> {\n            stack: Vec::<T>::new(),\n        }\n    }\n\n    /* Получение длины стека */\n    fn size(&self) -> usize {\n        self.stack.len()\n    }\n\n    /* Проверка, пуст ли стек */\n    fn is_empty(&self) -> bool {\n        self.size() == 0\n    }\n\n    /* Поместить в стек */\n    fn push(&mut self, num: T) {\n        self.stack.push(num);\n    }\n\n    /* Извлечь из стека */\n    fn pop(&mut self) -> Option<T> {\n        self.stack.pop()\n    }\n\n    /* Доступ к верхнему элементу стека */\n    fn peek(&self) -> Option<&T> {\n        if self.is_empty() {\n            panic!(\"стек пуст\")\n        };\n        self.stack.last()\n    }\n\n    /* Вернуть &Vec */\n    fn to_array(&self) -> &Vec<T> {\n        &self.stack\n    }\n}\n
        array_stack.c
        /* Стек на основе массива */\ntypedef struct {\n    int *data;\n    int size;\n} ArrayStack;\n\n/* Конструктор */\nArrayStack *newArrayStack() {\n    ArrayStack *stack = malloc(sizeof(ArrayStack));\n    // Инициализировать большую вместимость, чтобы избежать расширения\n    stack->data = malloc(sizeof(int) * MAX_SIZE);\n    stack->size = 0;\n    return stack;\n}\n\n/* Деструктор */\nvoid delArrayStack(ArrayStack *stack) {\n    free(stack->data);\n    free(stack);\n}\n\n/* Получение длины стека */\nint size(ArrayStack *stack) {\n    return stack->size;\n}\n\n/* Проверка, пуст ли стек */\nbool isEmpty(ArrayStack *stack) {\n    return stack->size == 0;\n}\n\n/* Поместить в стек */\nvoid push(ArrayStack *stack, int num) {\n    if (stack->size == MAX_SIZE) {\n        printf(\"Стек заполнен\\n\");\n        return;\n    }\n    stack->data[stack->size] = num;\n    stack->size++;\n}\n\n/* Доступ к верхнему элементу стека */\nint peek(ArrayStack *stack) {\n    if (stack->size == 0) {\n        printf(\"стек пуст\\n\");\n        return INT_MAX;\n    }\n    return stack->data[stack->size - 1];\n}\n\n/* Извлечь из стека */\nint pop(ArrayStack *stack) {\n    int val = peek(stack);\n    stack->size--;\n    return val;\n}\n
        array_stack.kt
        /* Стек на основе массива */\nclass ArrayStack {\n    // Инициализация списка (динамического массива)\n    private val stack = mutableListOf<Int>()\n\n    /* Получение длины стека */\n    fun size(): Int {\n        return stack.size\n    }\n\n    /* Проверка, пуст ли стек */\n    fun isEmpty(): Boolean {\n        return size() == 0\n    }\n\n    /* Поместить в стек */\n    fun push(num: Int) {\n        stack.add(num)\n    }\n\n    /* Извлечь из стека */\n    fun pop(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return stack.removeAt(size() - 1)\n    }\n\n    /* Доступ к верхнему элементу стека */\n    fun peek(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return stack[size() - 1]\n    }\n\n    /* Преобразовать List в Array и вернуть */\n    fun toArray(): Array<Any> {\n        return stack.toTypedArray()\n    }\n}\n
        array_stack.rb
        ### Стек на основе массива ###\nclass ArrayStack\n  ### Конструктор ###\n  def initialize\n    @stack = []\n  end\n\n  ### Получить длину стека ###\n  def size\n    @stack.length\n  end\n\n  ### Проверка, пуст ли стек ###\n  def is_empty?\n    @stack.empty?\n  end\n\n  ### Помещение в стек ###\n  def push(item)\n    @stack << item\n  end\n\n  ### Извлечение из стека ###\n  def pop\n    raise IndexError, 'стек пуст' if is_empty?\n\n    @stack.pop\n  end\n\n  ### Доступ к верхнему элементу стека ###\n  def peek\n    raise IndexError, 'стек пуст' if is_empty?\n\n    @stack.last\n  end\n\n  ### Вернуть список для вывода ###\n  def to_array\n    @stack\n  end\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 5. Стек и очередь","5.1   Стек"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#513","level":2,"title":"5.1.3   Сравнение двух реализаций","text":"

        Поддерживаемые операции

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

        Временная эффективность

        В реализации на массиве и push , и pop выполняются в заранее выделенной непрерывной памяти, которая хорошо использует локальность кэша, поэтому такие операции обычно эффективнее. Однако если при push емкость массива оказывается превышена, включается механизм расширения, и временная сложность именно этой операции становится \\(O(n)\\) .

        В реализации на связном списке расширение выполняется очень гибко, и проблемы падения эффективности из-за расширения массива здесь нет. Но сама операция push требует инициализации объекта-узла и изменения указателей, поэтому в среднем она немного менее эффективна. Впрочем, если помещаемые в стек элементы уже сами являются объектами-узлами, шаг инициализации можно пропустить и тем самым повысить эффективность.

        Итак, когда элементами, помещаемыми и извлекаемыми из стека, являются базовые типы данных, например int или double , можно сделать следующие выводы.

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

        Пространственная эффективность

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

        Однако, поскольку узлы связного списка должны дополнительно хранить указатели, узлы списка сами по себе занимают больше пространства.

        В итоге нельзя просто сказать, какая из реализаций более экономна по памяти; это нужно анализировать в контексте конкретной задачи.

        ","path":["Глава 5. Стек и очередь","5.1   Стек"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#514","level":2,"title":"5.1.4   Типичные применения стека","text":"
        • Кнопки \"назад\" и \"вперед\" в браузере, undo и redo в программах. Каждый раз, когда мы открываем новую страницу, браузер помещает предыдущую страницу в стек, чтобы по операции \"назад\" можно было вернуться к ней. Операция \"назад\" по сути является pop . Если нужно одновременно поддерживать и \"назад\", и \"вперед\", то обычно используются два стека.
        • Управление памятью программы. Каждый раз при вызове функции система помещает на вершину стека стековый кадр, в котором хранится контекст функции. В рекурсивной функции на этапе углубления рекурсии непрерывно выполняются операции push , а на этапе возврата - операции pop .
        ","path":["Глава 5. Стек и очередь","5.1   Стек"],"tags":[]},{"location":"chapter_stack_and_queue/summary/","level":1,"title":"5.4   Резюме","text":"","path":["Глава 5. Стек и очередь","5.4   Резюме"],"tags":[]},{"location":"chapter_stack_and_queue/summary/#1","level":3,"title":"1.   Основные выводы","text":"
        • Стек - это структура данных, следующая правилу \"последним пришел - первым вышел\", и его можно реализовать с помощью массива или связного списка.
        • С точки зрения временной эффективности реализация стека на массиве обычно работает быстрее в среднем, но во время расширения емкости временная сложность отдельной операции push может ухудшаться до \\(O(n)\\) . Напротив, реализация стека на связном списке дает более стабильные характеристики.
        • С точки зрения использования памяти реализация стека на массиве может приводить к некоторой потере пространства. Однако следует учитывать, что узлы связного списка занимают больше памяти, чем элементы массива.
        • Очередь - это структура данных, следующая правилу \"первым пришел - первым вышел\", и ее также можно реализовать с помощью массива или связного списка. Сравнение временной и пространственной эффективности для очереди в целом приводит к тем же выводам, что и для стека.
        • Двусторонняя очередь - это очередь с более высокой степенью свободы, которая позволяет добавлять и удалять элементы с обоих концов.
        ","path":["Глава 5. Стек и очередь","5.4   Резюме"],"tags":[]},{"location":"chapter_stack_and_queue/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

        Q: Реализованы ли кнопки \"вперед\" и \"назад\" в браузере с помощью двусвязного списка?

        По сути, функция переходов \"вперед/назад\" в браузере отражает логику стека. Когда пользователь открывает новую страницу, она помещается на вершину стека; когда пользователь нажимает кнопку \"назад\", эта страница снимается с вершины стека. Двусторонняя очередь позволяет удобно реализовать некоторые дополнительные операции, об этом уже упоминалось в разделе \"Двусторонняя очередь\".

        Q: Нужно ли освобождать память узла после извлечения его из стека?

        Если извлеченный узел еще понадобится, память освобождать не нужно. Если он больше не нужен, то в языках Java и Python есть автоматический сборщик мусора, поэтому ручное освобождение памяти не требуется; в C и C++ память нужно освобождать вручную.

        Q: Двусторонняя очередь выглядит как два соединенных стека. Для чего она нужна?

        Двусторонняя очередь похожа на комбинацию стека и очереди или на два соединенных стека. Она объединяет логику обеих структур, поэтому может покрыть все их применения и при этом остается более гибкой.

        Q: Как именно реализуются отмена (undo) и повтор (redo)?

        Используются два стека: стек A для отмены и стек B для повтора.

        1. Каждый раз, когда пользователь выполняет действие, это действие помещается в стек A , а стек B очищается.
        2. Когда пользователь выполняет \"undo\", последнее действие извлекается из стека A и помещается в стек B .
        3. Когда пользователь выполняет \"redo\", последнее действие извлекается из стека B и помещается обратно в стек A .
        ","path":["Глава 5. Стек и очередь","5.4   Резюме"],"tags":[]},{"location":"chapter_tree/","level":1,"title":"Глава 7.   Деревья","text":"

        Abstract

        Высокое дерево полно жизни: мощные корни, густая листва и раскидистые ветви.

        Оно наглядно показывает нам живую форму данных, построенную на принципе \"разделяй и властвуй\".

        ","path":["Глава 7. Деревья","Глава 7.   Деревья"],"tags":[]},{"location":"chapter_tree/#_1","level":2,"title":"Содержание главы","text":"
        • 7.1   Двоичное дерево
        • 7.2   Обход двоичного дерева
        • 7.3   Представление двоичного дерева массивом
        • 7.4   Двоичное дерево поиска
        • 7.5   AVL-дерево *
        • 7.6   Краткие итоги
        ","path":["Глава 7. Деревья","Глава 7.   Деревья"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/","level":1,"title":"7.3   Представление двоичного дерева массивом","text":"

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

        Возникает вопрос: можно ли представить двоичное дерево с помощью массива? Ответ: да.

        ","path":["Глава 7. Деревья","7.3   Представление двоичного дерева массивом"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/#731","level":2,"title":"7.3.1   Представление идеального двоичного дерева","text":"

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

        Из свойств обхода по уровням можно вывести формулу соответствия между индексом родителя и индексами дочерних узлов: если индекс некоторого узла равен \\(i\\) , то индекс его левого дочернего узла равен \\(2i + 1\\) , а правого - \\(2i + 2\\) . На рисунке 7-12 показано соответствие между индексами разных узлов.

        Рисунок 7-12   Представление идеального двоичного дерева массивом

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

        ","path":["Глава 7. Деревья","7.3   Представление двоичного дерева массивом"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/#732","level":2,"title":"7.3.2   Представление произвольного двоичного дерева","text":"

        Идеальное двоичное дерево - лишь частный случай; в обычной двоичной структуре на промежуточных уровнях часто существует множество None . Поскольку последовательность обхода по уровням не содержит этих None , мы не можем по одной лишь этой последовательности определить их количество и расположение. Это означает, что одному и тому же обходу по уровням может соответствовать сразу несколько различных структур двоичного дерева.

        Как показано на рисунке 7-13, для неполной двоичной структуры описанный выше способ представления массивом уже перестает работать.

        Рисунок 7-13   Одной последовательности обхода по уровням соответствуют разные двоичные структуры

        Чтобы решить эту проблему, мы можем явно записывать все None в последовательности обхода по уровням . Как показано на рисунке 7-14, после такой обработки последовательность обхода по уровням уже сможет однозначно задавать двоичное дерево. Пример кода приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
        # Представление двоичного дерева массивом\n# Используем None для обозначения пустых позиций\ntree = [1, 2, 3, 4, None, 6, 7, 8, 9, None, None, 12, None, None, 15]\n
        /* Представление двоичного дерева массивом */\n// Используем максимальное значение int, INT_MAX, для обозначения пустых позиций\nvector<int> tree = {1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15};\n
        /* Представление двоичного дерева массивом */\n// Используя обертку Integer для int, можно применять null для обозначения пустых позиций\nInteger[] tree = { 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 };\n
        /* Представление двоичного дерева массивом */\n// Используя nullable-тип int? , можно применять null для обозначения пустых позиций\nint?[] tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n
        /* Представление двоичного дерева массивом */\n// Используем срез типа any, чтобы можно было применять nil для обозначения пустых позиций\ntree := []any{1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15}\n
        /* Представление двоичного дерева массивом */\n// Используя nullable-тип Int? , можно применять nil для обозначения пустых позиций\nlet tree: [Int?] = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15]\n
        /* Представление двоичного дерева массивом */\n// Используем null для обозначения пустых позиций\nlet tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n
        /* Представление двоичного дерева массивом */\n// Используем null для обозначения пустых позиций\nlet tree: (number | null)[] = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n
        /* Представление двоичного дерева массивом */\n// Используя nullable-тип int? , можно применять null для обозначения пустых позиций\nList<int?> tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n
        /* Представление двоичного дерева массивом */\n// Используем None для обозначения пустых позиций\nlet tree = [Some(1), Some(2), Some(3), Some(4), None, Some(6), Some(7), Some(8), Some(9), None, None, Some(12), None, None, Some(15)];\n
        /* Представление двоичного дерева массивом */\n// Используем максимальное значение int для обозначения пустых позиций, поэтому узлы не должны принимать значение INT_MAX\nint tree[] = {1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15};\n
        /* Представление двоичного дерева массивом */\n// Используем null для обозначения пустых позиций\nval tree = arrayOf( 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 )\n
        ### Представление двоичного дерева массивом ###\n# Используем nil для обозначения пустых позиций\ntree = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15]\n

        Рисунок 7-14   Представление произвольного двоичного дерева массивом

        Стоит отметить, что полное двоичное дерево очень удобно представлять массивом . Если вспомнить определение полного двоичного дерева, то None появляются только на самом нижнем уровне и справа, а значит, все None обязательно находятся в конце последовательности обхода по уровням.

        Это означает, что при представлении полного двоичного дерева массивом можно не хранить все None , что очень удобно. На рисунке 7-15 приведен пример.

        Рисунок 7-15   Представление полного двоичного дерева массивом

        Ниже приведен код реализации двоичного дерева, представленного массивом. Он включает следующие операции.

        • Для заданного узла получить его значение, левого дочернего узла, правого дочернего узла и родительский узел.
        • Получить последовательности прямого, симметричного, обратного обходов и обхода по уровням.
        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_binary_tree.py
        class ArrayBinaryTree:\n    \"\"\"Класс двоичного дерева в массивном представлении\"\"\"\n\n    def __init__(self, arr: list[int | None]):\n        \"\"\"Конструктор\"\"\"\n        self._tree = list(arr)\n\n    def size(self):\n        \"\"\"Вместимость списка\"\"\"\n        return len(self._tree)\n\n    def val(self, i: int) -> int | None:\n        \"\"\"Получить значение узла с индексом i\"\"\"\n        # Если индекс выходит за границы, вернуть None, обозначающий пустую позицию\n        if i < 0 or i >= self.size():\n            return None\n        return self._tree[i]\n\n    def left(self, i: int) -> int | None:\n        \"\"\"Получить индекс левого дочернего узла узла с индексом i\"\"\"\n        return 2 * i + 1\n\n    def right(self, i: int) -> int | None:\n        \"\"\"Получить индекс правого дочернего узла узла с индексом i\"\"\"\n        return 2 * i + 2\n\n    def parent(self, i: int) -> int | None:\n        \"\"\"Получить индекс родительского узла узла с индексом i\"\"\"\n        return (i - 1) // 2\n\n    def level_order(self) -> list[int]:\n        \"\"\"Обход в ширину\"\"\"\n        self.res = []\n        # Непосредственно обходить массив\n        for i in range(self.size()):\n            if self.val(i) is not None:\n                self.res.append(self.val(i))\n        return self.res\n\n    def dfs(self, i: int, order: str):\n        \"\"\"Обход в глубину\"\"\"\n        if self.val(i) is None:\n            return\n        # Предварительный обход\n        if order == \"pre\":\n            self.res.append(self.val(i))\n        self.dfs(self.left(i), order)\n        # Симметричный обход\n        if order == \"in\":\n            self.res.append(self.val(i))\n        self.dfs(self.right(i), order)\n        # Обратный обход\n        if order == \"post\":\n            self.res.append(self.val(i))\n\n    def pre_order(self) -> list[int]:\n        \"\"\"Предварительный обход\"\"\"\n        self.res = []\n        self.dfs(0, order=\"pre\")\n        return self.res\n\n    def in_order(self) -> list[int]:\n        \"\"\"Симметричный обход\"\"\"\n        self.res = []\n        self.dfs(0, order=\"in\")\n        return self.res\n\n    def post_order(self) -> list[int]:\n        \"\"\"Обратный обход\"\"\"\n        self.res = []\n        self.dfs(0, order=\"post\")\n        return self.res\n
        array_binary_tree.cpp
        /* Класс двоичного дерева в массивном представлении */\nclass ArrayBinaryTree {\n  public:\n    /* Конструктор */\n    ArrayBinaryTree(vector<int> arr) {\n        tree = arr;\n    }\n\n    /* Вместимость списка */\n    int size() {\n        return tree.size();\n    }\n\n    /* Получить значение узла с индексом i */\n    int val(int i) {\n        // Если индекс выходит за границы, вернуть INT_MAX, обозначающий пустую позицию\n        if (i < 0 || i >= size())\n            return INT_MAX;\n        return tree[i];\n    }\n\n    /* Получить индекс левого дочернего узла узла с индексом i */\n    int left(int i) {\n        return 2 * i + 1;\n    }\n\n    /* Получить индекс правого дочернего узла узла с индексом i */\n    int right(int i) {\n        return 2 * i + 2;\n    }\n\n    /* Получить индекс родительского узла узла с индексом i */\n    int parent(int i) {\n        return (i - 1) / 2;\n    }\n\n    /* Обход в ширину */\n    vector<int> levelOrder() {\n        vector<int> res;\n        // Непосредственно обходить массив\n        for (int i = 0; i < size(); i++) {\n            if (val(i) != INT_MAX)\n                res.push_back(val(i));\n        }\n        return res;\n    }\n\n    /* Предварительный обход */\n    vector<int> preOrder() {\n        vector<int> res;\n        dfs(0, \"pre\", res);\n        return res;\n    }\n\n    /* Симметричный обход */\n    vector<int> inOrder() {\n        vector<int> res;\n        dfs(0, \"in\", res);\n        return res;\n    }\n\n    /* Обратный обход */\n    vector<int> postOrder() {\n        vector<int> res;\n        dfs(0, \"post\", res);\n        return res;\n    }\n\n  private:\n    vector<int> tree;\n\n    /* Обход в глубину */\n    void dfs(int i, string order, vector<int> &res) {\n        // Если это пустая позиция, вернуть\n        if (val(i) == INT_MAX)\n            return;\n        // Предварительный обход\n        if (order == \"pre\")\n            res.push_back(val(i));\n        dfs(left(i), order, res);\n        // Симметричный обход\n        if (order == \"in\")\n            res.push_back(val(i));\n        dfs(right(i), order, res);\n        // Обратный обход\n        if (order == \"post\")\n            res.push_back(val(i));\n    }\n};\n
        array_binary_tree.java
        /* Класс двоичного дерева в массивном представлении */\nclass ArrayBinaryTree {\n    private List<Integer> tree;\n\n    /* Конструктор */\n    public ArrayBinaryTree(List<Integer> arr) {\n        tree = new ArrayList<>(arr);\n    }\n\n    /* Вместимость списка */\n    public int size() {\n        return tree.size();\n    }\n\n    /* Получить значение узла с индексом i */\n    public Integer val(int i) {\n        // Если индекс выходит за границы, вернуть null, обозначающий пустую позицию\n        if (i < 0 || i >= size())\n            return null;\n        return tree.get(i);\n    }\n\n    /* Получить индекс левого дочернего узла узла с индексом i */\n    public Integer left(int i) {\n        return 2 * i + 1;\n    }\n\n    /* Получить индекс правого дочернего узла узла с индексом i */\n    public Integer right(int i) {\n        return 2 * i + 2;\n    }\n\n    /* Получить индекс родительского узла узла с индексом i */\n    public Integer parent(int i) {\n        return (i - 1) / 2;\n    }\n\n    /* Обход в ширину */\n    public List<Integer> levelOrder() {\n        List<Integer> res = new ArrayList<>();\n        // Непосредственно обходить массив\n        for (int i = 0; i < size(); i++) {\n            if (val(i) != null)\n                res.add(val(i));\n        }\n        return res;\n    }\n\n    /* Обход в глубину */\n    private void dfs(Integer i, String order, List<Integer> res) {\n        // Если это пустая позиция, вернуть\n        if (val(i) == null)\n            return;\n        // Предварительный обход\n        if (\"pre\".equals(order))\n            res.add(val(i));\n        dfs(left(i), order, res);\n        // Симметричный обход\n        if (\"in\".equals(order))\n            res.add(val(i));\n        dfs(right(i), order, res);\n        // Обратный обход\n        if (\"post\".equals(order))\n            res.add(val(i));\n    }\n\n    /* Предварительный обход */\n    public List<Integer> preOrder() {\n        List<Integer> res = new ArrayList<>();\n        dfs(0, \"pre\", res);\n        return res;\n    }\n\n    /* Симметричный обход */\n    public List<Integer> inOrder() {\n        List<Integer> res = new ArrayList<>();\n        dfs(0, \"in\", res);\n        return res;\n    }\n\n    /* Обратный обход */\n    public List<Integer> postOrder() {\n        List<Integer> res = new ArrayList<>();\n        dfs(0, \"post\", res);\n        return res;\n    }\n}\n
        array_binary_tree.cs
        /* Класс двоичного дерева в массивном представлении */\nclass ArrayBinaryTree(List<int?> arr) {\n    List<int?> tree = new(arr);\n\n    /* Вместимость списка */\n    public int Size() {\n        return tree.Count;\n    }\n\n    /* Получить значение узла с индексом i */\n    public int? Val(int i) {\n        // Если индекс выходит за границы, вернуть null, обозначающий пустую позицию\n        if (i < 0 || i >= Size())\n            return null;\n        return tree[i];\n    }\n\n    /* Получить индекс левого дочернего узла узла с индексом i */\n    public int Left(int i) {\n        return 2 * i + 1;\n    }\n\n    /* Получить индекс правого дочернего узла узла с индексом i */\n    public int Right(int i) {\n        return 2 * i + 2;\n    }\n\n    /* Получить индекс родительского узла узла с индексом i */\n    public int Parent(int i) {\n        return (i - 1) / 2;\n    }\n\n    /* Обход в ширину */\n    public List<int> LevelOrder() {\n        List<int> res = [];\n        // Непосредственно обходить массив\n        for (int i = 0; i < Size(); i++) {\n            if (Val(i).HasValue)\n                res.Add(Val(i)!.Value);\n        }\n        return res;\n    }\n\n    /* Обход в глубину */\n    void DFS(int i, string order, List<int> res) {\n        // Если это пустая позиция, вернуть\n        if (!Val(i).HasValue)\n            return;\n        // Предварительный обход\n        if (order == \"pre\")\n            res.Add(Val(i)!.Value);\n        DFS(Left(i), order, res);\n        // Симметричный обход\n        if (order == \"in\")\n            res.Add(Val(i)!.Value);\n        DFS(Right(i), order, res);\n        // Обратный обход\n        if (order == \"post\")\n            res.Add(Val(i)!.Value);\n    }\n\n    /* Предварительный обход */\n    public List<int> PreOrder() {\n        List<int> res = [];\n        DFS(0, \"pre\", res);\n        return res;\n    }\n\n    /* Симметричный обход */\n    public List<int> InOrder() {\n        List<int> res = [];\n        DFS(0, \"in\", res);\n        return res;\n    }\n\n    /* Обратный обход */\n    public List<int> PostOrder() {\n        List<int> res = [];\n        DFS(0, \"post\", res);\n        return res;\n    }\n}\n
        array_binary_tree.go
        /* Класс двоичного дерева в массивном представлении */\ntype arrayBinaryTree struct {\n    tree []any\n}\n\n/* Конструктор */\nfunc newArrayBinaryTree(arr []any) *arrayBinaryTree {\n    return &arrayBinaryTree{\n        tree: arr,\n    }\n}\n\n/* Вместимость списка */\nfunc (abt *arrayBinaryTree) size() int {\n    return len(abt.tree)\n}\n\n/* Получить значение узла с индексом i */\nfunc (abt *arrayBinaryTree) val(i int) any {\n    // Если индекс выходит за границы, вернуть null, обозначающий пустую позицию\n    if i < 0 || i >= abt.size() {\n        return nil\n    }\n    return abt.tree[i]\n}\n\n/* Получить индекс левого дочернего узла узла с индексом i */\nfunc (abt *arrayBinaryTree) left(i int) int {\n    return 2*i + 1\n}\n\n/* Получить индекс правого дочернего узла узла с индексом i */\nfunc (abt *arrayBinaryTree) right(i int) int {\n    return 2*i + 2\n}\n\n/* Получить индекс родительского узла узла с индексом i */\nfunc (abt *arrayBinaryTree) parent(i int) int {\n    return (i - 1) / 2\n}\n\n/* Обход в ширину */\nfunc (abt *arrayBinaryTree) levelOrder() []any {\n    var res []any\n    // Непосредственно обходить массив\n    for i := 0; i < abt.size(); i++ {\n        if abt.val(i) != nil {\n            res = append(res, abt.val(i))\n        }\n    }\n    return res\n}\n\n/* Обход в глубину */\nfunc (abt *arrayBinaryTree) dfs(i int, order string, res *[]any) {\n    // Если это пустая позиция, вернуть\n    if abt.val(i) == nil {\n        return\n    }\n    // Предварительный обход\n    if order == \"pre\" {\n        *res = append(*res, abt.val(i))\n    }\n    abt.dfs(abt.left(i), order, res)\n    // Симметричный обход\n    if order == \"in\" {\n        *res = append(*res, abt.val(i))\n    }\n    abt.dfs(abt.right(i), order, res)\n    // Обратный обход\n    if order == \"post\" {\n        *res = append(*res, abt.val(i))\n    }\n}\n\n/* Предварительный обход */\nfunc (abt *arrayBinaryTree) preOrder() []any {\n    var res []any\n    abt.dfs(0, \"pre\", &res)\n    return res\n}\n\n/* Симметричный обход */\nfunc (abt *arrayBinaryTree) inOrder() []any {\n    var res []any\n    abt.dfs(0, \"in\", &res)\n    return res\n}\n\n/* Обратный обход */\nfunc (abt *arrayBinaryTree) postOrder() []any {\n    var res []any\n    abt.dfs(0, \"post\", &res)\n    return res\n}\n
        array_binary_tree.swift
        /* Класс двоичного дерева в массивном представлении */\nclass ArrayBinaryTree {\n    private var tree: [Int?]\n\n    /* Конструктор */\n    init(arr: [Int?]) {\n        tree = arr\n    }\n\n    /* Вместимость списка */\n    func size() -> Int {\n        tree.count\n    }\n\n    /* Получить значение узла с индексом i */\n    func val(i: Int) -> Int? {\n        // Если индекс выходит за границы, вернуть null, обозначающий пустую позицию\n        if i < 0 || i >= size() {\n            return nil\n        }\n        return tree[i]\n    }\n\n    /* Получить индекс левого дочернего узла узла с индексом i */\n    func left(i: Int) -> Int {\n        2 * i + 1\n    }\n\n    /* Получить индекс правого дочернего узла узла с индексом i */\n    func right(i: Int) -> Int {\n        2 * i + 2\n    }\n\n    /* Получить индекс родительского узла узла с индексом i */\n    func parent(i: Int) -> Int {\n        (i - 1) / 2\n    }\n\n    /* Обход в ширину */\n    func levelOrder() -> [Int] {\n        var res: [Int] = []\n        // Непосредственно обходить массив\n        for i in 0 ..< size() {\n            if let val = val(i: i) {\n                res.append(val)\n            }\n        }\n        return res\n    }\n\n    /* Обход в глубину */\n    private func dfs(i: Int, order: String, res: inout [Int]) {\n        // Если это пустая позиция, вернуть\n        guard let val = val(i: i) else {\n            return\n        }\n        // Предварительный обход\n        if order == \"pre\" {\n            res.append(val)\n        }\n        dfs(i: left(i: i), order: order, res: &res)\n        // Симметричный обход\n        if order == \"in\" {\n            res.append(val)\n        }\n        dfs(i: right(i: i), order: order, res: &res)\n        // Обратный обход\n        if order == \"post\" {\n            res.append(val)\n        }\n    }\n\n    /* Предварительный обход */\n    func preOrder() -> [Int] {\n        var res: [Int] = []\n        dfs(i: 0, order: \"pre\", res: &res)\n        return res\n    }\n\n    /* Симметричный обход */\n    func inOrder() -> [Int] {\n        var res: [Int] = []\n        dfs(i: 0, order: \"in\", res: &res)\n        return res\n    }\n\n    /* Обратный обход */\n    func postOrder() -> [Int] {\n        var res: [Int] = []\n        dfs(i: 0, order: \"post\", res: &res)\n        return res\n    }\n}\n
        array_binary_tree.js
        /* Класс двоичного дерева в массивном представлении */\nclass ArrayBinaryTree {\n    #tree;\n\n    /* Конструктор */\n    constructor(arr) {\n        this.#tree = arr;\n    }\n\n    /* Вместимость списка */\n    size() {\n        return this.#tree.length;\n    }\n\n    /* Получить значение узла с индексом i */\n    val(i) {\n        // Если индекс выходит за границы, вернуть null, обозначающий пустую позицию\n        if (i < 0 || i >= this.size()) return null;\n        return this.#tree[i];\n    }\n\n    /* Получить индекс левого дочернего узла узла с индексом i */\n    left(i) {\n        return 2 * i + 1;\n    }\n\n    /* Получить индекс правого дочернего узла узла с индексом i */\n    right(i) {\n        return 2 * i + 2;\n    }\n\n    /* Получить индекс родительского узла узла с индексом i */\n    parent(i) {\n        return Math.floor((i - 1) / 2); // Округление вниз при делении\n    }\n\n    /* Обход в ширину */\n    levelOrder() {\n        let res = [];\n        // Непосредственно обходить массив\n        for (let i = 0; i < this.size(); i++) {\n            if (this.val(i) !== null) res.push(this.val(i));\n        }\n        return res;\n    }\n\n    /* Обход в глубину */\n    #dfs(i, order, res) {\n        // Если это пустая позиция, вернуть\n        if (this.val(i) === null) return;\n        // Предварительный обход\n        if (order === 'pre') res.push(this.val(i));\n        this.#dfs(this.left(i), order, res);\n        // Симметричный обход\n        if (order === 'in') res.push(this.val(i));\n        this.#dfs(this.right(i), order, res);\n        // Обратный обход\n        if (order === 'post') res.push(this.val(i));\n    }\n\n    /* Предварительный обход */\n    preOrder() {\n        const res = [];\n        this.#dfs(0, 'pre', res);\n        return res;\n    }\n\n    /* Симметричный обход */\n    inOrder() {\n        const res = [];\n        this.#dfs(0, 'in', res);\n        return res;\n    }\n\n    /* Обратный обход */\n    postOrder() {\n        const res = [];\n        this.#dfs(0, 'post', res);\n        return res;\n    }\n}\n
        array_binary_tree.ts
        /* Класс двоичного дерева в массивном представлении */\nclass ArrayBinaryTree {\n    #tree: (number | null)[];\n\n    /* Конструктор */\n    constructor(arr: (number | null)[]) {\n        this.#tree = arr;\n    }\n\n    /* Вместимость списка */\n    size(): number {\n        return this.#tree.length;\n    }\n\n    /* Получить значение узла с индексом i */\n    val(i: number): number | null {\n        // Если индекс выходит за границы, вернуть null, обозначающий пустую позицию\n        if (i < 0 || i >= this.size()) return null;\n        return this.#tree[i];\n    }\n\n    /* Получить индекс левого дочернего узла узла с индексом i */\n    left(i: number): number {\n        return 2 * i + 1;\n    }\n\n    /* Получить индекс правого дочернего узла узла с индексом i */\n    right(i: number): number {\n        return 2 * i + 2;\n    }\n\n    /* Получить индекс родительского узла узла с индексом i */\n    parent(i: number): number {\n        return Math.floor((i - 1) / 2); // Округление вниз при делении\n    }\n\n    /* Обход в ширину */\n    levelOrder(): number[] {\n        let res = [];\n        // Непосредственно обходить массив\n        for (let i = 0; i < this.size(); i++) {\n            if (this.val(i) !== null) res.push(this.val(i));\n        }\n        return res;\n    }\n\n    /* Обход в глубину */\n    #dfs(i: number, order: Order, res: (number | null)[]): void {\n        // Если это пустая позиция, вернуть\n        if (this.val(i) === null) return;\n        // Предварительный обход\n        if (order === 'pre') res.push(this.val(i));\n        this.#dfs(this.left(i), order, res);\n        // Симметричный обход\n        if (order === 'in') res.push(this.val(i));\n        this.#dfs(this.right(i), order, res);\n        // Обратный обход\n        if (order === 'post') res.push(this.val(i));\n    }\n\n    /* Предварительный обход */\n    preOrder(): (number | null)[] {\n        const res = [];\n        this.#dfs(0, 'pre', res);\n        return res;\n    }\n\n    /* Симметричный обход */\n    inOrder(): (number | null)[] {\n        const res = [];\n        this.#dfs(0, 'in', res);\n        return res;\n    }\n\n    /* Обратный обход */\n    postOrder(): (number | null)[] {\n        const res = [];\n        this.#dfs(0, 'post', res);\n        return res;\n    }\n}\n
        array_binary_tree.dart
        /* Класс двоичного дерева в массивном представлении */\nclass ArrayBinaryTree {\n  late List<int?> _tree;\n\n  /* Конструктор */\n  ArrayBinaryTree(this._tree);\n\n  /* Вместимость списка */\n  int size() {\n    return _tree.length;\n  }\n\n  /* Получить значение узла с индексом i */\n  int? val(int i) {\n    // Если индекс выходит за границы, вернуть null, обозначающий пустую позицию\n    if (i < 0 || i >= size()) {\n      return null;\n    }\n    return _tree[i];\n  }\n\n  /* Получить индекс левого дочернего узла узла с индексом i */\n  int? left(int i) {\n    return 2 * i + 1;\n  }\n\n  /* Получить индекс правого дочернего узла узла с индексом i */\n  int? right(int i) {\n    return 2 * i + 2;\n  }\n\n  /* Получить индекс родительского узла узла с индексом i */\n  int? parent(int i) {\n    return (i - 1) ~/ 2;\n  }\n\n  /* Обход в ширину */\n  List<int> levelOrder() {\n    List<int> res = [];\n    for (int i = 0; i < size(); i++) {\n      if (val(i) != null) {\n        res.add(val(i)!);\n      }\n    }\n    return res;\n  }\n\n  /* Обход в глубину */\n  void dfs(int i, String order, List<int?> res) {\n    // Если это пустая позиция, вернуть\n    if (val(i) == null) {\n      return;\n    }\n    // Предварительный обход\n    if (order == 'pre') {\n      res.add(val(i));\n    }\n    dfs(left(i)!, order, res);\n    // Симметричный обход\n    if (order == 'in') {\n      res.add(val(i));\n    }\n    dfs(right(i)!, order, res);\n    // Обратный обход\n    if (order == 'post') {\n      res.add(val(i));\n    }\n  }\n\n  /* Предварительный обход */\n  List<int?> preOrder() {\n    List<int?> res = [];\n    dfs(0, 'pre', res);\n    return res;\n  }\n\n  /* Симметричный обход */\n  List<int?> inOrder() {\n    List<int?> res = [];\n    dfs(0, 'in', res);\n    return res;\n  }\n\n  /* Обратный обход */\n  List<int?> postOrder() {\n    List<int?> res = [];\n    dfs(0, 'post', res);\n    return res;\n  }\n}\n
        array_binary_tree.rs
        /* Класс двоичного дерева в массивном представлении */\nstruct ArrayBinaryTree {\n    tree: Vec<Option<i32>>,\n}\n\nimpl ArrayBinaryTree {\n    /* Конструктор */\n    fn new(arr: Vec<Option<i32>>) -> Self {\n        Self { tree: arr }\n    }\n\n    /* Вместимость списка */\n    fn size(&self) -> i32 {\n        self.tree.len() as i32\n    }\n\n    /* Получить значение узла с индексом i */\n    fn val(&self, i: i32) -> Option<i32> {\n        // Если индекс выходит за границы, вернуть None, обозначающий пустую позицию\n        if i < 0 || i >= self.size() {\n            None\n        } else {\n            self.tree[i as usize]\n        }\n    }\n\n    /* Получить индекс левого дочернего узла узла с индексом i */\n    fn left(&self, i: i32) -> i32 {\n        2 * i + 1\n    }\n\n    /* Получить индекс правого дочернего узла узла с индексом i */\n    fn right(&self, i: i32) -> i32 {\n        2 * i + 2\n    }\n\n    /* Получить индекс родительского узла узла с индексом i */\n    fn parent(&self, i: i32) -> i32 {\n        (i - 1) / 2\n    }\n\n    /* Обход в ширину */\n    fn level_order(&self) -> Vec<i32> {\n        self.tree.iter().filter_map(|&x| x).collect()\n    }\n\n    /* Обход в глубину */\n    fn dfs(&self, i: i32, order: &'static str, res: &mut Vec<i32>) {\n        if self.val(i).is_none() {\n            return;\n        }\n        let val = self.val(i).unwrap();\n        // Предварительный обход\n        if order == \"pre\" {\n            res.push(val);\n        }\n        self.dfs(self.left(i), order, res);\n        // Симметричный обход\n        if order == \"in\" {\n            res.push(val);\n        }\n        self.dfs(self.right(i), order, res);\n        // Обратный обход\n        if order == \"post\" {\n            res.push(val);\n        }\n    }\n\n    /* Предварительный обход */\n    fn pre_order(&self) -> Vec<i32> {\n        let mut res = vec![];\n        self.dfs(0, \"pre\", &mut res);\n        res\n    }\n\n    /* Симметричный обход */\n    fn in_order(&self) -> Vec<i32> {\n        let mut res = vec![];\n        self.dfs(0, \"in\", &mut res);\n        res\n    }\n\n    /* Обратный обход */\n    fn post_order(&self) -> Vec<i32> {\n        let mut res = vec![];\n        self.dfs(0, \"post\", &mut res);\n        res\n    }\n}\n
        array_binary_tree.c
        /* Структура двоичного дерева в представлении массивом */\ntypedef struct {\n    int *tree;\n    int size;\n} ArrayBinaryTree;\n\n/* Конструктор */\nArrayBinaryTree *newArrayBinaryTree(int *arr, int arrSize) {\n    ArrayBinaryTree *abt = (ArrayBinaryTree *)malloc(sizeof(ArrayBinaryTree));\n    abt->tree = malloc(sizeof(int) * arrSize);\n    memcpy(abt->tree, arr, sizeof(int) * arrSize);\n    abt->size = arrSize;\n    return abt;\n}\n\n/* Деструктор */\nvoid delArrayBinaryTree(ArrayBinaryTree *abt) {\n    free(abt->tree);\n    free(abt);\n}\n\n/* Вместимость списка */\nint size(ArrayBinaryTree *abt) {\n    return abt->size;\n}\n\n/* Получить значение узла с индексом i */\nint val(ArrayBinaryTree *abt, int i) {\n    // Если индекс выходит за границы, вернуть INT_MAX, обозначающий пустую позицию\n    if (i < 0 || i >= size(abt))\n        return INT_MAX;\n    return abt->tree[i];\n}\n\n/* Обход в ширину */\nint *levelOrder(ArrayBinaryTree *abt, int *returnSize) {\n    int *res = (int *)malloc(sizeof(int) * size(abt));\n    int index = 0;\n    // Непосредственно обходить массив\n    for (int i = 0; i < size(abt); i++) {\n        if (val(abt, i) != INT_MAX)\n            res[index++] = val(abt, i);\n    }\n    *returnSize = index;\n    return res;\n}\n\n/* Обход в глубину */\nvoid dfs(ArrayBinaryTree *abt, int i, char *order, int *res, int *index) {\n    // Если это пустая позиция, вернуть\n    if (val(abt, i) == INT_MAX)\n        return;\n    // Предварительный обход\n    if (strcmp(order, \"pre\") == 0)\n        res[(*index)++] = val(abt, i);\n    dfs(abt, left(i), order, res, index);\n    // Симметричный обход\n    if (strcmp(order, \"in\") == 0)\n        res[(*index)++] = val(abt, i);\n    dfs(abt, right(i), order, res, index);\n    // Обратный обход\n    if (strcmp(order, \"post\") == 0)\n        res[(*index)++] = val(abt, i);\n}\n\n/* Предварительный обход */\nint *preOrder(ArrayBinaryTree *abt, int *returnSize) {\n    int *res = (int *)malloc(sizeof(int) * size(abt));\n    int index = 0;\n    dfs(abt, 0, \"pre\", res, &index);\n    *returnSize = index;\n    return res;\n}\n\n/* Симметричный обход */\nint *inOrder(ArrayBinaryTree *abt, int *returnSize) {\n    int *res = (int *)malloc(sizeof(int) * size(abt));\n    int index = 0;\n    dfs(abt, 0, \"in\", res, &index);\n    *returnSize = index;\n    return res;\n}\n\n/* Обратный обход */\nint *postOrder(ArrayBinaryTree *abt, int *returnSize) {\n    int *res = (int *)malloc(sizeof(int) * size(abt));\n    int index = 0;\n    dfs(abt, 0, \"post\", res, &index);\n    *returnSize = index;\n    return res;\n}\n
        array_binary_tree.kt
        /* Класс двоичного дерева в массивном представлении */\nclass ArrayBinaryTree(val tree: MutableList<Int?>) {\n    /* Вместимость списка */\n    fun size(): Int {\n        return tree.size\n    }\n\n    /* Получить значение узла с индексом i */\n    fun _val(i: Int): Int? {\n        // Если индекс выходит за границы, вернуть null, обозначающий пустую позицию\n        if (i < 0 || i >= size()) return null\n        return tree[i]\n    }\n\n    /* Получить индекс левого дочернего узла узла с индексом i */\n    fun left(i: Int): Int {\n        return 2 * i + 1\n    }\n\n    /* Получить индекс правого дочернего узла узла с индексом i */\n    fun right(i: Int): Int {\n        return 2 * i + 2\n    }\n\n    /* Получить индекс родительского узла узла с индексом i */\n    fun parent(i: Int): Int {\n        return (i - 1) / 2\n    }\n\n    /* Обход в ширину */\n    fun levelOrder(): MutableList<Int?> {\n        val res = mutableListOf<Int?>()\n        // Непосредственно обходить массив\n        for (i in 0..<size()) {\n            if (_val(i) != null)\n                res.add(_val(i))\n        }\n        return res\n    }\n\n    /* Обход в глубину */\n    fun dfs(i: Int, order: String, res: MutableList<Int?>) {\n        // Если это пустая позиция, вернуть\n        if (_val(i) == null)\n            return\n        // Предварительный обход\n        if (\"pre\" == order)\n            res.add(_val(i))\n        dfs(left(i), order, res)\n        // Симметричный обход\n        if (\"in\" == order)\n            res.add(_val(i))\n        dfs(right(i), order, res)\n        // Обратный обход\n        if (\"post\" == order)\n            res.add(_val(i))\n    }\n\n    /* Предварительный обход */\n    fun preOrder(): MutableList<Int?> {\n        val res = mutableListOf<Int?>()\n        dfs(0, \"pre\", res)\n        return res\n    }\n\n    /* Симметричный обход */\n    fun inOrder(): MutableList<Int?> {\n        val res = mutableListOf<Int?>()\n        dfs(0, \"in\", res)\n        return res\n    }\n\n    /* Обратный обход */\n    fun postOrder(): MutableList<Int?> {\n        val res = mutableListOf<Int?>()\n        dfs(0, \"post\", res)\n        return res\n    }\n}\n
        array_binary_tree.rb
        ### Класс двоичного дерева в массивном представлении ###\nclass ArrayBinaryTree\n  ### Конструктор ###\n  def initialize(arr)\n    @tree = arr.to_a\n  end\n\n  ### Вместимость списка ###\n  def size\n    @tree.length\n  end\n\n  ### Получить значение узла с индексом i ###\n  def val(i)\n    # Если индекс выходит за границы, вернуть nil, обозначающий пустую ячейку\n    return if i < 0 || i >= size\n\n    @tree[i]\n  end\n\n  ### Получить индекс левого дочернего узла узла с индексом i ###\n  def left(i)\n    2 * i + 1\n  end\n\n  ### Получить индекс правого дочернего узла узла с индексом i ###\n  def right(i)\n    2 * i + 2\n  end\n\n  ### Получить индекс родительского узла узла с индексом i ###\n  def parent(i)\n    (i - 1) / 2\n  end\n\n  ### Обход в ширину ###\n  def level_order\n    @res = []\n\n    # Непосредственно обходить массив\n    for i in 0...size\n      @res << val(i) unless val(i).nil?\n    end\n\n    @res\n  end\n\n  ### Обход в глубину ###\n  def dfs(i, order)\n    return if val(i).nil?\n    # Предварительный обход\n    @res << val(i) if order == :pre\n    dfs(left(i), order)\n    # Симметричный обход\n    @res << val(i) if order == :in\n    dfs(right(i), order)\n    # Обратный обход\n    @res << val(i) if order == :post\n  end\n\n  ### Предварительный обход ###\n  def pre_order\n    @res = []\n    dfs(0, :pre)\n    @res\n  end\n\n  ### Симметричный обход ###\n  def in_order\n    @res = []\n    dfs(0, :in)\n    @res\n  end\n\n  ### Обратный обход ###\n  def post_order\n    @res = []\n    dfs(0, :post)\n    @res\n  end\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 7. Деревья","7.3   Представление двоичного дерева массивом"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/#733","level":2,"title":"7.3.3   Преимущества и ограничения","text":"

        Представление двоичного дерева массивом имеет в основном следующие преимущества.

        • Массив хранится в непрерывной области памяти, хорошо работает с кешем и обеспечивает высокую скорость доступа и обхода.
        • Не нужно хранить указатели, поэтому память расходуется экономнее.
        • Разрешается произвольный доступ к узлам.

        Однако у представления массивом есть и некоторые ограничения.

        • Для хранения массива требуется непрерывная область памяти, поэтому такой способ не подходит для деревьев с очень большим объемом данных.
        • Добавление и удаление узлов приходится реализовывать через вставку и удаление элементов массива, а это не слишком эффективно.
        • Когда в двоичном дереве имеется большое число None , доля действительно полезных данных в массиве оказывается низкой, и эффективность использования пространства падает.
        ","path":["Глава 7. Деревья","7.3   Представление двоичного дерева массивом"],"tags":[]},{"location":"chapter_tree/avl_tree/","level":1,"title":"7.5   AVL-дерево *","text":"

        В разделе \"Двоичное дерево поиска\" мы упоминали, что после многократных операций вставки и удаления узлов двоичное дерево поиска может выродиться в связный список. В таком случае временная сложность всех операций ухудшается с \\(O(\\log n)\\) до \\(O(n)\\) .

        Как показано на рисунке 7-24, после двух операций удаления узлов это двоичное дерево поиска вырождается в связный список.

        Рисунок 7-24   Деградация AVL-дерева после удаления узлов

        Другой пример: если в идеальное двоичное дерево, показанное на рисунке 7-25, вставить два узла, то дерево сильно наклонится влево, а временная сложность поиска тоже ухудшится.

        Рисунок 7-25   Деградация AVL-дерева после вставки узлов

        В 1962 году Г. М. Adelson-Velsky и Е. М. Landis в статье \"An algorithm for the organization of information\" предложили AVL-дерево. В статье подробно описан набор операций, гарантирующий, что при непрерывном добавлении и удалении узлов AVL-дерево не вырождается, благодаря чему временная сложность различных операций сохраняется на уровне \\(O(\\log n)\\) . Иначе говоря, в сценариях, где часто выполняются вставка, удаление, поиск и изменение, AVL-дерево всегда поддерживает эффективную работу с данными и потому имеет высокую практическую ценность.

        ","path":["Глава 7. Деревья","7.5   AVL-дерево *"],"tags":[]},{"location":"chapter_tree/avl_tree/#751-avl-","level":2,"title":"7.5.1   Распространенные термины AVL-дерева","text":"

        AVL-дерево одновременно является и двоичным деревом поиска, и сбалансированным двоичным деревом, то есть одновременно удовлетворяет всем свойствам обеих этих структур. Поэтому AVL-дерево является разновидностью сбалансированного двоичного дерева поиска (balanced binary search tree).

        ","path":["Глава 7. Деревья","7.5   AVL-дерево *"],"tags":[]},{"location":"chapter_tree/avl_tree/#1","level":3,"title":"1.   Высота узла","text":"

        Поскольку операции AVL-дерева требуют получать высоту узла, нам нужно добавить в класс узла переменную height :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
        class TreeNode:\n    \"\"\"Класс узла AVL-дерева\"\"\"\n    def __init__(self, val: int):\n        self.val: int = val                 # Значение узла\n        self.height: int = 0                # Высота узла\n        self.left: TreeNode | None = None   # Ссылка на левого дочернего узла\n        self.right: TreeNode | None = None  # Ссылка на правого дочернего узла\n
        /* Класс узла AVL-дерева */\nstruct TreeNode {\n    int val{};          // Значение узла\n    int height = 0;     // Высота узла\n    TreeNode *left{};   // Левый дочерний узел\n    TreeNode *right{};  // Правый дочерний узел\n    TreeNode() = default;\n    explicit TreeNode(int x) : val(x){}\n};\n
        /* Класс узла AVL-дерева */\nclass TreeNode {\n    public int val;        // Значение узла\n    public int height;     // Высота узла\n    public TreeNode left;  // Левый дочерний узел\n    public TreeNode right; // Правый дочерний узел\n    public TreeNode(int x) { val = x; }\n}\n
        /* Класс узла AVL-дерева */\nclass TreeNode(int? x) {\n    public int? val = x;    // Значение узла\n    public int height;      // Высота узла\n    public TreeNode? left;  // Ссылка на левого дочернего узла\n    public TreeNode? right; // Ссылка на правого дочернего узла\n}\n
        /* Структура узла AVL-дерева */\ntype TreeNode struct {\n    Val    int       // Значение узла\n    Height int       // Высота узла\n    Left   *TreeNode // Ссылка на левого дочернего узла\n    Right  *TreeNode // Ссылка на правого дочернего узла\n}\n
        /* Класс узла AVL-дерева */\nclass TreeNode {\n    var val: Int // Значение узла\n    var height: Int // Высота узла\n    var left: TreeNode? // Левый дочерний узел\n    var right: TreeNode? // Правый дочерний узел\n\n    init(x: Int) {\n        val = x\n        height = 0\n    }\n}\n
        /* Класс узла AVL-дерева */\nclass TreeNode {\n    val; // Значение узла\n    height; // Высота узла\n    left; // Указатель на левого дочернего узла\n    right; // Указатель на правого дочернего узла\n    constructor(val, left, right, height) {\n        this.val = val === undefined ? 0 : val;\n        this.height = height === undefined ? 0 : height;\n        this.left = left === undefined ? null : left;\n        this.right = right === undefined ? null : right;\n    }\n}\n
        /* Класс узла AVL-дерева */\nclass TreeNode {\n    val: number;            // Значение узла\n    height: number;         // Высота узла\n    left: TreeNode | null;  // Указатель на левого дочернего узла\n    right: TreeNode | null; // Указатель на правого дочернего узла\n    constructor(val?: number, height?: number, left?: TreeNode | null, right?: TreeNode | null) {\n        this.val = val === undefined ? 0 : val;\n        this.height = height === undefined ? 0 : height;\n        this.left = left === undefined ? null : left;\n        this.right = right === undefined ? null : right;\n    }\n}\n
        /* Класс узла AVL-дерева */\nclass TreeNode {\n  int val;         // Значение узла\n  int height;      // Высота узла\n  TreeNode? left;  // Левый дочерний узел\n  TreeNode? right; // Правый дочерний узел\n  TreeNode(this.val, [this.height = 0, this.left, this.right]);\n}\n
        use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* Структура узла AVL-дерева */\nstruct TreeNode {\n    val: i32,                               // Значение узла\n    height: i32,                            // Высота узла\n    left: Option<Rc<RefCell<TreeNode>>>,    // Левый дочерний узел\n    right: Option<Rc<RefCell<TreeNode>>>,   // Правый дочерний узел\n}\n\nimpl TreeNode {\n    /* Конструктор */\n    fn new(val: i32) -> Rc<RefCell<Self>> {\n        Rc::new(RefCell::new(Self {\n            val,\n            height: 0,\n            left: None,\n            right: None\n        }))\n    }\n}\n
        /* Структура узла AVL-дерева */\ntypedef struct TreeNode {\n    int val;\n    int height;\n    struct TreeNode *left;\n    struct TreeNode *right;\n} TreeNode;\n\n/* Конструктор */\nTreeNode *newTreeNode(int val) {\n    TreeNode *node;\n\n    node = (TreeNode *)malloc(sizeof(TreeNode));\n    node->val = val;\n    node->height = 0;\n    node->left = NULL;\n    node->right = NULL;\n    return node;\n}\n
        /* Класс узла AVL-дерева */\nclass TreeNode(val _val: Int) {  // Значение узла\n    val height: Int = 0          // Высота узла\n    val left: TreeNode? = null   // Левый дочерний узел\n    val right: TreeNode? = null  // Правый дочерний узел\n}\n
        ### Класс узла AVL-дерева ###\nclass TreeNode\n  attr_accessor :val    # Значение узла\n  attr_accessor :height # Высота узла\n  attr_accessor :left   # Ссылка на левого дочернего узла\n  attr_accessor :right  # Ссылка на правого дочернего узла\n\n  def initialize(val)\n    @val = val\n    @height = 0\n  end\nend\n

        \"Высота узла\" означает расстояние от этого узла до самого удаленного листового узла, то есть число пройденных \"ребер\". Особенно важно помнить, что высота листового узла равна \\(0\\) , а высота пустого узла равна \\(-1\\) . Мы создадим две вспомогательные функции: одну для получения высоты узла, другую для ее обновления:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
        def height(self, node: TreeNode | None) -> int:\n    \"\"\"Получить высоту узла\"\"\"\n    # Высота пустого узла равна -1, высота листового узла равна 0\n    if node is not None:\n        return node.height\n    return -1\n\ndef update_height(self, node: TreeNode | None):\n    \"\"\"Обновить высоту узла\"\"\"\n    # Высота узла равна высоте более высокого поддерева + 1\n    node.height = max([self.height(node.left), self.height(node.right)]) + 1\n
        avl_tree.cpp
        /* Получить высоту узла */\nint height(TreeNode *node) {\n    // Высота пустого узла равна -1, высота листового узла равна 0\n    return node == nullptr ? -1 : node->height;\n}\n\n/* Обновить высоту узла */\nvoid updateHeight(TreeNode *node) {\n    // Высота узла равна высоте более высокого поддерева + 1\n    node->height = max(height(node->left), height(node->right)) + 1;\n}\n
        avl_tree.java
        /* Получить высоту узла */\nint height(TreeNode node) {\n    // Высота пустого узла равна -1, высота листового узла равна 0\n    return node == null ? -1 : node.height;\n}\n\n/* Обновить высоту узла */\nvoid updateHeight(TreeNode node) {\n    // Высота узла равна высоте более высокого поддерева + 1\n    node.height = Math.max(height(node.left), height(node.right)) + 1;\n}\n
        avl_tree.cs
        /* Получить высоту узла */\nint Height(TreeNode? node) {\n    // Высота пустого узла равна -1, высота листового узла равна 0\n    return node == null ? -1 : node.height;\n}\n\n/* Обновить высоту узла */\nvoid UpdateHeight(TreeNode node) {\n    // Высота узла равна высоте более высокого поддерева + 1\n    node.height = Math.Max(Height(node.left), Height(node.right)) + 1;\n}\n
        avl_tree.go
        /* Получить высоту узла */\nfunc (t *aVLTree) height(node *TreeNode) int {\n    // Высота пустого узла равна -1, высота листового узла равна 0\n    if node != nil {\n        return node.Height\n    }\n    return -1\n}\n\n/* Обновить высоту узла */\nfunc (t *aVLTree) updateHeight(node *TreeNode) {\n    lh := t.height(node.Left)\n    rh := t.height(node.Right)\n    // Высота узла равна высоте более высокого поддерева + 1\n    if lh > rh {\n        node.Height = lh + 1\n    } else {\n        node.Height = rh + 1\n    }\n}\n
        avl_tree.swift
        /* Получить высоту узла */\nfunc height(node: TreeNode?) -> Int {\n    // Высота пустого узла равна -1, высота листового узла равна 0\n    node?.height ?? -1\n}\n\n/* Обновить высоту узла */\nfunc updateHeight(node: TreeNode?) {\n    // Высота узла равна высоте более высокого поддерева + 1\n    node?.height = max(height(node: node?.left), height(node: node?.right)) + 1\n}\n
        avl_tree.js
        /* Получить высоту узла */\nheight(node) {\n    // Высота пустого узла равна -1, высота листового узла равна 0\n    return node === null ? -1 : node.height;\n}\n\n/* Обновить высоту узла */\n#updateHeight(node) {\n    // Высота узла равна высоте более высокого поддерева + 1\n    node.height =\n        Math.max(this.height(node.left), this.height(node.right)) + 1;\n}\n
        avl_tree.ts
        /* Получить высоту узла */\nheight(node: TreeNode): number {\n    // Высота пустого узла равна -1, высота листового узла равна 0\n    return node === null ? -1 : node.height;\n}\n\n/* Обновить высоту узла */\nupdateHeight(node: TreeNode): void {\n    // Высота узла равна высоте более высокого поддерева + 1\n    node.height =\n        Math.max(this.height(node.left), this.height(node.right)) + 1;\n}\n
        avl_tree.dart
        /* Получить высоту узла */\nint height(TreeNode? node) {\n  // Высота пустого узла равна -1, высота листового узла равна 0\n  return node == null ? -1 : node.height;\n}\n\n/* Обновить высоту узла */\nvoid updateHeight(TreeNode? node) {\n  // Высота узла равна высоте более высокого поддерева + 1\n  node!.height = max(height(node.left), height(node.right)) + 1;\n}\n
        avl_tree.rs
        /* Получить высоту узла */\nfn height(node: OptionTreeNodeRc) -> i32 {\n    // Высота пустого узла равна -1, высота листового узла равна 0\n    match node {\n        Some(node) => node.borrow().height,\n        None => -1,\n    }\n}\n\n/* Обновить высоту узла */\nfn update_height(node: OptionTreeNodeRc) {\n    if let Some(node) = node {\n        let left = node.borrow().left.clone();\n        let right = node.borrow().right.clone();\n        // Высота узла равна высоте более высокого поддерева + 1\n        node.borrow_mut().height = std::cmp::max(Self::height(left), Self::height(right)) + 1;\n    }\n}\n
        avl_tree.c
        /* Получить высоту узла */\nint height(TreeNode *node) {\n    // Высота пустого узла равна -1, высота листового узла равна 0\n    if (node != NULL) {\n        return node->height;\n    }\n    return -1;\n}\n\n/* Обновить высоту узла */\nvoid updateHeight(TreeNode *node) {\n    int lh = height(node->left);\n    int rh = height(node->right);\n    // Высота узла равна высоте более высокого поддерева + 1\n    if (lh > rh) {\n        node->height = lh + 1;\n    } else {\n        node->height = rh + 1;\n    }\n}\n
        avl_tree.kt
        /* Получить высоту узла */\nfun height(node: TreeNode?): Int {\n    // Высота пустого узла равна -1, высота листового узла равна 0\n    return node?.height ?: -1\n}\n\n/* Обновить высоту узла */\nfun updateHeight(node: TreeNode?) {\n    // Высота узла равна высоте более высокого поддерева + 1\n    node?.height = max(height(node?.left), height(node?.right)) + 1\n}\n
        avl_tree.rb
        ### Получить высоту узла ###\ndef height(node)\n  # Высота пустого узла равна -1, высота листового узла равна 0\n  return node.height unless node.nil?\n\n  -1\nend\n\n### Обновить высоту узла ###\ndef update_height(node)\n  # Высота узла равна высоте более высокого поддерева + 1\n  node.height = [height(node.left), height(node.right)].max + 1\nend\n
        ","path":["Глава 7. Деревья","7.5   AVL-дерево *"],"tags":[]},{"location":"chapter_tree/avl_tree/#2-","level":3,"title":"2.   Баланс-фактор узла","text":"

        Баланс-фактор (balance factor) узла определяется как высота левого поддерева минус высота правого поддерева; при этом баланс-фактор пустого узла считается равным \\(0\\) . Мы также инкапсулируем получение баланс-фактора в отдельную функцию, чтобы потом было удобнее ее использовать:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
        def balance_factor(self, node: TreeNode | None) -> int:\n    \"\"\"Получить коэффициент баланса\"\"\"\n    # Коэффициент баланса пустого узла равен 0\n    if node is None:\n        return 0\n    # Коэффициент баланса узла = высота левого поддерева - высота правого поддерева\n    return self.height(node.left) - self.height(node.right)\n
        avl_tree.cpp
        /* Получить коэффициент баланса */\nint balanceFactor(TreeNode *node) {\n    // Коэффициент баланса пустого узла равен 0\n    if (node == nullptr)\n        return 0;\n    // Коэффициент баланса узла = высота левого поддерева - высота правого поддерева\n    return height(node->left) - height(node->right);\n}\n
        avl_tree.java
        /* Получить коэффициент баланса */\nint balanceFactor(TreeNode node) {\n    // Коэффициент баланса пустого узла равен 0\n    if (node == null)\n        return 0;\n    // Коэффициент баланса узла = высота левого поддерева - высота правого поддерева\n    return height(node.left) - height(node.right);\n}\n
        avl_tree.cs
        /* Получить коэффициент баланса */\nint BalanceFactor(TreeNode? node) {\n    // Коэффициент баланса пустого узла равен 0\n    if (node == null) return 0;\n    // Коэффициент баланса узла = высота левого поддерева - высота правого поддерева\n    return Height(node.left) - Height(node.right);\n}\n
        avl_tree.go
        /* Получить коэффициент баланса */\nfunc (t *aVLTree) balanceFactor(node *TreeNode) int {\n    // Коэффициент баланса пустого узла равен 0\n    if node == nil {\n        return 0\n    }\n    // Коэффициент баланса узла = высота левого поддерева - высота правого поддерева\n    return t.height(node.Left) - t.height(node.Right)\n}\n
        avl_tree.swift
        /* Получить коэффициент баланса */\nfunc balanceFactor(node: TreeNode?) -> Int {\n    // Коэффициент баланса пустого узла равен 0\n    guard let node = node else { return 0 }\n    // Коэффициент баланса узла = высота левого поддерева - высота правого поддерева\n    return height(node: node.left) - height(node: node.right)\n}\n
        avl_tree.js
        /* Получить коэффициент баланса */\nbalanceFactor(node) {\n    // Коэффициент баланса пустого узла равен 0\n    if (node === null) return 0;\n    // Коэффициент баланса узла = высота левого поддерева - высота правого поддерева\n    return this.height(node.left) - this.height(node.right);\n}\n
        avl_tree.ts
        /* Получить коэффициент баланса */\nbalanceFactor(node: TreeNode): number {\n    // Коэффициент баланса пустого узла равен 0\n    if (node === null) return 0;\n    // Коэффициент баланса узла = высота левого поддерева - высота правого поддерева\n    return this.height(node.left) - this.height(node.right);\n}\n
        avl_tree.dart
        /* Получить коэффициент баланса */\nint balanceFactor(TreeNode? node) {\n  // Коэффициент баланса пустого узла равен 0\n  if (node == null) return 0;\n  // Коэффициент баланса узла = высота левого поддерева - высота правого поддерева\n  return height(node.left) - height(node.right);\n}\n
        avl_tree.rs
        /* Получить коэффициент баланса */\nfn balance_factor(node: OptionTreeNodeRc) -> i32 {\n    match node {\n        // Коэффициент баланса пустого узла равен 0\n        None => 0,\n        // Коэффициент баланса узла = высота левого поддерева - высота правого поддерева\n        Some(node) => {\n            Self::height(node.borrow().left.clone()) - Self::height(node.borrow().right.clone())\n        }\n    }\n}\n
        avl_tree.c
        /* Получить коэффициент баланса */\nint balanceFactor(TreeNode *node) {\n    // Коэффициент баланса пустого узла равен 0\n    if (node == NULL) {\n        return 0;\n    }\n    // Коэффициент баланса узла = высота левого поддерева - высота правого поддерева\n    return height(node->left) - height(node->right);\n}\n
        avl_tree.kt
        /* Получить коэффициент баланса */\nfun balanceFactor(node: TreeNode?): Int {\n    // Коэффициент баланса пустого узла равен 0\n    if (node == null) return 0\n    // Коэффициент баланса узла = высота левого поддерева - высота правого поддерева\n    return height(node.left) - height(node.right)\n}\n
        avl_tree.rb
        ### Получить коэффициент баланса ###\ndef balance_factor(node)\n  # Коэффициент баланса пустого узла равен 0\n  return 0 if node.nil?\n\n  # Коэффициент баланса узла = высота левого поддерева - высота правого поддерева\n  height(node.left) - height(node.right)\nend\n

        Tip

        Пусть баланс-фактор равен \\(f\\) ; тогда для любого узла AVL-дерева выполняется \\(-1 \\le f \\le 1\\) .

        ","path":["Глава 7. Деревья","7.5   AVL-дерево *"],"tags":[]},{"location":"chapter_tree/avl_tree/#752-avl-","level":2,"title":"7.5.2   Вращения AVL-дерева","text":"

        Особенность AVL-дерева заключается в операции \"вращения\", которая позволяет заново сбалансировать разбалансированный узел, не нарушая последовательность симметричного обхода двоичного дерева. Иначе говоря, операция вращения одновременно сохраняет свойство \"двоичного дерева поиска\" и возвращает дерево в состояние \"сбалансированного двоичного дерева\".

        Узлы, для которых абсолютное значение баланс-фактора больше \\(1\\) , мы называем \"разбалансированными узлами\". В зависимости от вида разбаланса вращения делятся на четыре типа: правое вращение, левое вращение, сначала левое затем правое, и сначала правое затем левое. Ниже разберем их подробно.

        ","path":["Глава 7. Деревья","7.5   AVL-дерево *"],"tags":[]},{"location":"chapter_tree/avl_tree/#1_1","level":3,"title":"1.   Правое вращение","text":"

        Как показано на рисунках ниже, под узлом указан его баланс-фактор. Если двигаться снизу вверх, то первым разбалансированным узлом в двоичном дереве будет \"узел 3\". Рассмотрим поддерево с этим узлом в качестве корня, обозначим данный узел как node , его левого дочернего узла как child и выполним \"правое вращение\". После завершения правого вращения поддерево снова станет сбалансированным и при этом сохранит свойство двоичного дерева поиска.

        <1><2><3><4>

        Рисунок 7-26   Шаги правого вращения

        Как показано на рисунке 7-27, когда у узла child есть правый дочерний узел, который мы обозначим как grand_child , в правое вращение нужно добавить еще один шаг: сделать grand_child левым дочерним узлом node .

        Рисунок 7-27   Правое вращение при наличии grand_child

        \"Поворот вправо\" - это лишь образное описание; в реальности он реализуется через изменение указателей узлов. Код приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
        def right_rotate(self, node: TreeNode | None) -> TreeNode | None:\n    \"\"\"Операция правого вращения\"\"\"\n    child = node.left\n    grand_child = child.right\n    # Выполнить правое вращение узла node вокруг child\n    child.right = node\n    node.left = grand_child\n    # Обновить высоту узла\n    self.update_height(node)\n    self.update_height(child)\n    # Вернуть корневой узел поддерева после вращения\n    return child\n
        avl_tree.cpp
        /* Операция правого вращения */\nTreeNode *rightRotate(TreeNode *node) {\n    TreeNode *child = node->left;\n    TreeNode *grandChild = child->right;\n    // Выполнить правое вращение узла node вокруг child\n    child->right = node;\n    node->left = grandChild;\n    // Обновить высоту узла\n    updateHeight(node);\n    updateHeight(child);\n    // Вернуть корневой узел поддерева после вращения\n    return child;\n}\n
        avl_tree.java
        /* Операция правого вращения */\nTreeNode rightRotate(TreeNode node) {\n    TreeNode child = node.left;\n    TreeNode grandChild = child.right;\n    // Выполнить правое вращение узла node вокруг child\n    child.right = node;\n    node.left = grandChild;\n    // Обновить высоту узла\n    updateHeight(node);\n    updateHeight(child);\n    // Вернуть корневой узел поддерева после вращения\n    return child;\n}\n
        avl_tree.cs
        /* Операция правого вращения */\nTreeNode? RightRotate(TreeNode? node) {\n    TreeNode? child = node?.left;\n    TreeNode? grandChild = child?.right;\n    // Выполнить правое вращение узла node вокруг child\n    child.right = node;\n    node.left = grandChild;\n    // Обновить высоту узла\n    UpdateHeight(node);\n    UpdateHeight(child);\n    // Вернуть корневой узел поддерева после вращения\n    return child;\n}\n
        avl_tree.go
        /* Операция правого вращения */\nfunc (t *aVLTree) rightRotate(node *TreeNode) *TreeNode {\n    child := node.Left\n    grandChild := child.Right\n    // Выполнить правое вращение узла node вокруг child\n    child.Right = node\n    node.Left = grandChild\n    // Обновить высоту узла\n    t.updateHeight(node)\n    t.updateHeight(child)\n    // Вернуть корневой узел поддерева после вращения\n    return child\n}\n
        avl_tree.swift
        /* Операция правого вращения */\nfunc rightRotate(node: TreeNode?) -> TreeNode? {\n    let child = node?.left\n    let grandChild = child?.right\n    // Выполнить правое вращение узла node вокруг child\n    child?.right = node\n    node?.left = grandChild\n    // Обновить высоту узла\n    updateHeight(node: node)\n    updateHeight(node: child)\n    // Вернуть корневой узел поддерева после вращения\n    return child\n}\n
        avl_tree.js
        /* Операция правого вращения */\n#rightRotate(node) {\n    const child = node.left;\n    const grandChild = child.right;\n    // Выполнить правое вращение узла node вокруг child\n    child.right = node;\n    node.left = grandChild;\n    // Обновить высоту узла\n    this.#updateHeight(node);\n    this.#updateHeight(child);\n    // Вернуть корневой узел поддерева после вращения\n    return child;\n}\n
        avl_tree.ts
        /* Операция правого вращения */\nrightRotate(node: TreeNode): TreeNode {\n    const child = node.left;\n    const grandChild = child.right;\n    // Выполнить правое вращение узла node вокруг child\n    child.right = node;\n    node.left = grandChild;\n    // Обновить высоту узла\n    this.updateHeight(node);\n    this.updateHeight(child);\n    // Вернуть корневой узел поддерева после вращения\n    return child;\n}\n
        avl_tree.dart
        /* Операция правого вращения */\nTreeNode? rightRotate(TreeNode? node) {\n  TreeNode? child = node!.left;\n  TreeNode? grandChild = child!.right;\n  // Выполнить правое вращение узла node вокруг child\n  child.right = node;\n  node.left = grandChild;\n  // Обновить высоту узла\n  updateHeight(node);\n  updateHeight(child);\n  // Вернуть корневой узел поддерева после вращения\n  return child;\n}\n
        avl_tree.rs
        /* Операция правого вращения */\nfn right_rotate(node: OptionTreeNodeRc) -> OptionTreeNodeRc {\n    match node {\n        Some(node) => {\n            let child = node.borrow().left.clone().unwrap();\n            let grand_child = child.borrow().right.clone();\n            // Выполнить правое вращение узла node вокруг child\n            child.borrow_mut().right = Some(node.clone());\n            node.borrow_mut().left = grand_child;\n            // Обновить высоту узла\n            Self::update_height(Some(node));\n            Self::update_height(Some(child.clone()));\n            // Вернуть корневой узел поддерева после вращения\n            Some(child)\n        }\n        None => None,\n    }\n}\n
        avl_tree.c
        /* Операция правого вращения */\nTreeNode *rightRotate(TreeNode *node) {\n    TreeNode *child, *grandChild;\n    child = node->left;\n    grandChild = child->right;\n    // Выполнить правое вращение узла node вокруг child\n    child->right = node;\n    node->left = grandChild;\n    // Обновить высоту узла\n    updateHeight(node);\n    updateHeight(child);\n    // Вернуть корневой узел поддерева после вращения\n    return child;\n}\n
        avl_tree.kt
        /* Операция правого вращения */\nfun rightRotate(node: TreeNode?): TreeNode {\n    val child = node!!.left\n    val grandChild = child!!.right\n    // Выполнить правое вращение узла node вокруг child\n    child.right = node\n    node.left = grandChild\n    // Обновить высоту узла\n    updateHeight(node)\n    updateHeight(child)\n    // Вернуть корневой узел поддерева после вращения\n    return child\n}\n
        avl_tree.rb
        ### Операция правого вращения ###\ndef right_rotate(node)\n  child = node.left\n  grand_child = child.right\n  # Выполнить правое вращение узла node вокруг child\n  child.right = node\n  node.left = grand_child\n  # Обновить высоту узла\n  update_height(node)\n  update_height(child)\n  # Вернуть корневой узел поддерева после вращения\n  child\nend\n
        ","path":["Глава 7. Деревья","7.5   AVL-дерево *"],"tags":[]},{"location":"chapter_tree/avl_tree/#2","level":3,"title":"2.   Левое вращение","text":"

        Соответственно, если рассмотреть \"зеркальную\" версию приведенного выше разбалансированного двоичного дерева, то понадобится выполнить \"левое вращение\", показанное на рисунке 7-28.

        Рисунок 7-28   Левое вращение

        По той же причине, когда у узла child есть левый дочерний узел, который обозначим как grand_child , в левое вращение также требуется добавить шаг: сделать grand_child правым дочерним узлом node .

        Рисунок 7-29   Левое вращение при наличии grand_child

        Можно заметить, что операции правого и левого вращения логически зеркально симметричны, и два вида разбаланса, которые они исправляют, тоже симметричны. Поэтому, опираясь на эту симметрию, достаточно заменить в коде правого вращения все left на right , а все right на left , чтобы получить реализацию левого вращения:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
        def left_rotate(self, node: TreeNode | None) -> TreeNode | None:\n    \"\"\"Операция левого вращения\"\"\"\n    child = node.right\n    grand_child = child.left\n    # Выполнить левое вращение узла node вокруг child\n    child.left = node\n    node.right = grand_child\n    # Обновить высоту узла\n    self.update_height(node)\n    self.update_height(child)\n    # Вернуть корневой узел поддерева после вращения\n    return child\n
        avl_tree.cpp
        /* Операция левого вращения */\nTreeNode *leftRotate(TreeNode *node) {\n    TreeNode *child = node->right;\n    TreeNode *grandChild = child->left;\n    // Выполнить левое вращение узла node вокруг child\n    child->left = node;\n    node->right = grandChild;\n    // Обновить высоту узла\n    updateHeight(node);\n    updateHeight(child);\n    // Вернуть корневой узел поддерева после вращения\n    return child;\n}\n
        avl_tree.java
        /* Операция левого вращения */\nTreeNode leftRotate(TreeNode node) {\n    TreeNode child = node.right;\n    TreeNode grandChild = child.left;\n    // Выполнить левое вращение узла node вокруг child\n    child.left = node;\n    node.right = grandChild;\n    // Обновить высоту узла\n    updateHeight(node);\n    updateHeight(child);\n    // Вернуть корневой узел поддерева после вращения\n    return child;\n}\n
        avl_tree.cs
        /* Операция левого вращения */\nTreeNode? LeftRotate(TreeNode? node) {\n    TreeNode? child = node?.right;\n    TreeNode? grandChild = child?.left;\n    // Выполнить левое вращение узла node вокруг child\n    child.left = node;\n    node.right = grandChild;\n    // Обновить высоту узла\n    UpdateHeight(node);\n    UpdateHeight(child);\n    // Вернуть корневой узел поддерева после вращения\n    return child;\n}\n
        avl_tree.go
        /* Операция левого вращения */\nfunc (t *aVLTree) leftRotate(node *TreeNode) *TreeNode {\n    child := node.Right\n    grandChild := child.Left\n    // Выполнить левое вращение узла node вокруг child\n    child.Left = node\n    node.Right = grandChild\n    // Обновить высоту узла\n    t.updateHeight(node)\n    t.updateHeight(child)\n    // Вернуть корневой узел поддерева после вращения\n    return child\n}\n
        avl_tree.swift
        /* Операция левого вращения */\nfunc leftRotate(node: TreeNode?) -> TreeNode? {\n    let child = node?.right\n    let grandChild = child?.left\n    // Выполнить левое вращение узла node вокруг child\n    child?.left = node\n    node?.right = grandChild\n    // Обновить высоту узла\n    updateHeight(node: node)\n    updateHeight(node: child)\n    // Вернуть корневой узел поддерева после вращения\n    return child\n}\n
        avl_tree.js
        /* Операция левого вращения */\n#leftRotate(node) {\n    const child = node.right;\n    const grandChild = child.left;\n    // Выполнить левое вращение узла node вокруг child\n    child.left = node;\n    node.right = grandChild;\n    // Обновить высоту узла\n    this.#updateHeight(node);\n    this.#updateHeight(child);\n    // Вернуть корневой узел поддерева после вращения\n    return child;\n}\n
        avl_tree.ts
        /* Операция левого вращения */\nleftRotate(node: TreeNode): TreeNode {\n    const child = node.right;\n    const grandChild = child.left;\n    // Выполнить левое вращение узла node вокруг child\n    child.left = node;\n    node.right = grandChild;\n    // Обновить высоту узла\n    this.updateHeight(node);\n    this.updateHeight(child);\n    // Вернуть корневой узел поддерева после вращения\n    return child;\n}\n
        avl_tree.dart
        /* Операция левого вращения */\nTreeNode? leftRotate(TreeNode? node) {\n  TreeNode? child = node!.right;\n  TreeNode? grandChild = child!.left;\n  // Выполнить левое вращение узла node вокруг child\n  child.left = node;\n  node.right = grandChild;\n  // Обновить высоту узла\n  updateHeight(node);\n  updateHeight(child);\n  // Вернуть корневой узел поддерева после вращения\n  return child;\n}\n
        avl_tree.rs
        /* Операция левого вращения */\nfn left_rotate(node: OptionTreeNodeRc) -> OptionTreeNodeRc {\n    match node {\n        Some(node) => {\n            let child = node.borrow().right.clone().unwrap();\n            let grand_child = child.borrow().left.clone();\n            // Выполнить левое вращение узла node вокруг child\n            child.borrow_mut().left = Some(node.clone());\n            node.borrow_mut().right = grand_child;\n            // Обновить высоту узла\n            Self::update_height(Some(node));\n            Self::update_height(Some(child.clone()));\n            // Вернуть корневой узел поддерева после вращения\n            Some(child)\n        }\n        None => None,\n    }\n}\n
        avl_tree.c
        /* Операция левого вращения */\nTreeNode *leftRotate(TreeNode *node) {\n    TreeNode *child, *grandChild;\n    child = node->right;\n    grandChild = child->left;\n    // Выполнить левое вращение узла node вокруг child\n    child->left = node;\n    node->right = grandChild;\n    // Обновить высоту узла\n    updateHeight(node);\n    updateHeight(child);\n    // Вернуть корневой узел поддерева после вращения\n    return child;\n}\n
        avl_tree.kt
        /* Операция левого вращения */\nfun leftRotate(node: TreeNode?): TreeNode {\n    val child = node!!.right\n    val grandChild = child!!.left\n    // Выполнить левое вращение узла node вокруг child\n    child.left = node\n    node.right = grandChild\n    // Обновить высоту узла\n    updateHeight(node)\n    updateHeight(child)\n    // Вернуть корневой узел поддерева после вращения\n    return child\n}\n
        avl_tree.rb
        ### Операция левого вращения ###\ndef left_rotate(node)\n  child = node.right\n  grand_child = child.left\n  # Выполнить левое вращение узла node вокруг child\n  child.left = node\n  node.right = grand_child\n  # Обновить высоту узла\n  update_height(node)\n  update_height(child)\n  # Вернуть корневой узел поддерева после вращения\n  child\nend\n
        ","path":["Глава 7. Деревья","7.5   AVL-дерево *"],"tags":[]},{"location":"chapter_tree/avl_tree/#3","level":3,"title":"3.   Сначала левое, затем правое вращение","text":"

        Для разбалансированного узла 3 на рисунке 7-30 ни одно лишь левое вращение, ни одно лишь правое вращение не способны вернуть поддерево в баланс. В этом случае нужно сначала выполнить \"левое вращение\" для child , а затем выполнить \"правое вращение\" для node .

        Рисунок 7-30   Сначала левое, затем правое вращение

        ","path":["Глава 7. Деревья","7.5   AVL-дерево *"],"tags":[]},{"location":"chapter_tree/avl_tree/#4","level":3,"title":"4.   Сначала правое, затем левое вращение","text":"

        Как показано на рисунке 7-31, для зеркальной ситуации предыдущего разбалансированного двоичного дерева нужно сначала выполнить \"правое вращение\" для child , а затем \"левое вращение\" для node .

        Рисунок 7-31   Сначала правое, затем левое вращение

        ","path":["Глава 7. Деревья","7.5   AVL-дерево *"],"tags":[]},{"location":"chapter_tree/avl_tree/#5","level":3,"title":"5.   Выбор вращения","text":"

        Четыре вида разбаланса, показанные на рисунке 7-32, по одному соответствуют рассмотренным выше случаям; для них соответственно требуются правое вращение, сначала левое затем правое, сначала правое затем левое и левое вращение.

        Рисунок 7-32   Четыре случая вращений AVL-дерева

        Как показано в таблице 7-3, мы определяем, какому из этих четырех случаев соответствует разбалансированный узел, по знаку баланс-фактора самого разбалансированного узла и по знаку баланс-фактора дочернего узла на более высокой стороне.

        Таблица 7-3   Условия выбора для четырех случаев вращений

        Баланс-фактор разбалансированного узла Баланс-фактор дочернего узла Какое вращение использовать \\(> 1\\) (левостороннее дерево) \\(\\geq 0\\) Правое вращение \\(> 1\\) (левостороннее дерево) \\(<0\\) Сначала левое, затем правое \\(< -1\\) (правостороннее дерево) \\(\\leq 0\\) Левое вращение \\(< -1\\) (правостороннее дерево) \\(>0\\) Сначала правое, затем левое

        Для удобства мы инкапсулируем операцию вращения в отдельную функцию. С помощью этой функции можно выполнить корректное вращение для любой ситуации разбаланса и снова привести узел в сбалансированное состояние. Код приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
        def rotate(self, node: TreeNode | None) -> TreeNode | None:\n    \"\"\"Выполнить вращение, чтобы снова сбалансировать поддерево\"\"\"\n    # Получить коэффициент баланса узла node\n    balance_factor = self.balance_factor(node)\n    # Левосторонне перекошенное дерево\n    if balance_factor > 1:\n        if self.balance_factor(node.left) >= 0:\n            # Правое вращение\n            return self.right_rotate(node)\n        else:\n            # Сначала левое вращение, затем правое\n            node.left = self.left_rotate(node.left)\n            return self.right_rotate(node)\n    # Правосторонне перекошенное дерево\n    elif balance_factor < -1:\n        if self.balance_factor(node.right) <= 0:\n            # Левое вращение\n            return self.left_rotate(node)\n        else:\n            # Сначала правое вращение, затем левое\n            node.right = self.right_rotate(node.right)\n            return self.left_rotate(node)\n    # Дерево сбалансировано, вращение не требуется, вернуть сразу\n    return node\n
        avl_tree.cpp
        /* Выполнить вращение, чтобы снова сбалансировать поддерево */\nTreeNode *rotate(TreeNode *node) {\n    // Получить коэффициент баланса узла node\n    int _balanceFactor = balanceFactor(node);\n    // Левосторонне перекошенное дерево\n    if (_balanceFactor > 1) {\n        if (balanceFactor(node->left) >= 0) {\n            // Правое вращение\n            return rightRotate(node);\n        } else {\n            // Сначала левое вращение, затем правое\n            node->left = leftRotate(node->left);\n            return rightRotate(node);\n        }\n    }\n    // Правосторонне перекошенное дерево\n    if (_balanceFactor < -1) {\n        if (balanceFactor(node->right) <= 0) {\n            // Левое вращение\n            return leftRotate(node);\n        } else {\n            // Сначала правое вращение, затем левое\n            node->right = rightRotate(node->right);\n            return leftRotate(node);\n        }\n    }\n    // Дерево сбалансировано, вращение не требуется, вернуть сразу\n    return node;\n}\n
        avl_tree.java
        /* Выполнить вращение, чтобы снова сбалансировать поддерево */\nTreeNode rotate(TreeNode node) {\n    // Получить коэффициент баланса узла node\n    int balanceFactor = balanceFactor(node);\n    // Левосторонне перекошенное дерево\n    if (balanceFactor > 1) {\n        if (balanceFactor(node.left) >= 0) {\n            // Правое вращение\n            return rightRotate(node);\n        } else {\n            // Сначала левое вращение, затем правое\n            node.left = leftRotate(node.left);\n            return rightRotate(node);\n        }\n    }\n    // Правосторонне перекошенное дерево\n    if (balanceFactor < -1) {\n        if (balanceFactor(node.right) <= 0) {\n            // Левое вращение\n            return leftRotate(node);\n        } else {\n            // Сначала правое вращение, затем левое\n            node.right = rightRotate(node.right);\n            return leftRotate(node);\n        }\n    }\n    // Дерево сбалансировано, вращение не требуется, вернуть сразу\n    return node;\n}\n
        avl_tree.cs
        /* Выполнить вращение, чтобы снова сбалансировать поддерево */\nTreeNode? Rotate(TreeNode? node) {\n    // Получить коэффициент баланса узла node\n    int balanceFactorInt = BalanceFactor(node);\n    // Левосторонне перекошенное дерево\n    if (balanceFactorInt > 1) {\n        if (BalanceFactor(node?.left) >= 0) {\n            // Правое вращение\n            return RightRotate(node);\n        } else {\n            // Сначала левое вращение, затем правое\n            node!.left = LeftRotate(node!.left);\n            return RightRotate(node);\n        }\n    }\n    // Правосторонне перекошенное дерево\n    if (balanceFactorInt < -1) {\n        if (BalanceFactor(node?.right) <= 0) {\n            // Левое вращение\n            return LeftRotate(node);\n        } else {\n            // Сначала правое вращение, затем левое\n            node!.right = RightRotate(node!.right);\n            return LeftRotate(node);\n        }\n    }\n    // Дерево сбалансировано, вращение не требуется, вернуть сразу\n    return node;\n}\n
        avl_tree.go
        /* Выполнить вращение, чтобы снова сбалансировать поддерево */\nfunc (t *aVLTree) rotate(node *TreeNode) *TreeNode {\n    // Получить коэффициент баланса узла node\n    // В Go рекомендуется использовать короткие имена переменных, здесь bf обозначает t.balanceFactor\n    bf := t.balanceFactor(node)\n    // Левосторонне перекошенное дерево\n    if bf > 1 {\n        if t.balanceFactor(node.Left) >= 0 {\n            // Правое вращение\n            return t.rightRotate(node)\n        } else {\n            // Сначала левое вращение, затем правое\n            node.Left = t.leftRotate(node.Left)\n            return t.rightRotate(node)\n        }\n    }\n    // Правосторонне перекошенное дерево\n    if bf < -1 {\n        if t.balanceFactor(node.Right) <= 0 {\n            // Левое вращение\n            return t.leftRotate(node)\n        } else {\n            // Сначала правое вращение, затем левое\n            node.Right = t.rightRotate(node.Right)\n            return t.leftRotate(node)\n        }\n    }\n    // Дерево сбалансировано, вращение не требуется, вернуть сразу\n    return node\n}\n
        avl_tree.swift
        /* Выполнить вращение, чтобы снова сбалансировать поддерево */\nfunc rotate(node: TreeNode?) -> TreeNode? {\n    // Получить коэффициент баланса узла node\n    let balanceFactor = balanceFactor(node: node)\n    // Левосторонне перекошенное дерево\n    if balanceFactor > 1 {\n        if self.balanceFactor(node: node?.left) >= 0 {\n            // Правое вращение\n            return rightRotate(node: node)\n        } else {\n            // Сначала левое вращение, затем правое\n            node?.left = leftRotate(node: node?.left)\n            return rightRotate(node: node)\n        }\n    }\n    // Правосторонне перекошенное дерево\n    if balanceFactor < -1 {\n        if self.balanceFactor(node: node?.right) <= 0 {\n            // Левое вращение\n            return leftRotate(node: node)\n        } else {\n            // Сначала правое вращение, затем левое\n            node?.right = rightRotate(node: node?.right)\n            return leftRotate(node: node)\n        }\n    }\n    // Дерево сбалансировано, вращение не требуется, вернуть сразу\n    return node\n}\n
        avl_tree.js
        /* Выполнить вращение, чтобы снова сбалансировать поддерево */\n#rotate(node) {\n    // Получить коэффициент баланса узла node\n    const balanceFactor = this.balanceFactor(node);\n    // Левосторонне перекошенное дерево\n    if (balanceFactor > 1) {\n        if (this.balanceFactor(node.left) >= 0) {\n            // Правое вращение\n            return this.#rightRotate(node);\n        } else {\n            // Сначала левое вращение, затем правое\n            node.left = this.#leftRotate(node.left);\n            return this.#rightRotate(node);\n        }\n    }\n    // Правосторонне перекошенное дерево\n    if (balanceFactor < -1) {\n        if (this.balanceFactor(node.right) <= 0) {\n            // Левое вращение\n            return this.#leftRotate(node);\n        } else {\n            // Сначала правое вращение, затем левое\n            node.right = this.#rightRotate(node.right);\n            return this.#leftRotate(node);\n        }\n    }\n    // Дерево сбалансировано, вращение не требуется, вернуть сразу\n    return node;\n}\n
        avl_tree.ts
        /* Выполнить вращение, чтобы снова сбалансировать поддерево */\nrotate(node: TreeNode): TreeNode {\n    // Получить коэффициент баланса узла node\n    const balanceFactor = this.balanceFactor(node);\n    // Левосторонне перекошенное дерево\n    if (balanceFactor > 1) {\n        if (this.balanceFactor(node.left) >= 0) {\n            // Правое вращение\n            return this.rightRotate(node);\n        } else {\n            // Сначала левое вращение, затем правое\n            node.left = this.leftRotate(node.left);\n            return this.rightRotate(node);\n        }\n    }\n    // Правосторонне перекошенное дерево\n    if (balanceFactor < -1) {\n        if (this.balanceFactor(node.right) <= 0) {\n            // Левое вращение\n            return this.leftRotate(node);\n        } else {\n            // Сначала правое вращение, затем левое\n            node.right = this.rightRotate(node.right);\n            return this.leftRotate(node);\n        }\n    }\n    // Дерево сбалансировано, вращение не требуется, вернуть сразу\n    return node;\n}\n
        avl_tree.dart
        /* Выполнить вращение, чтобы снова сбалансировать поддерево */\nTreeNode? rotate(TreeNode? node) {\n  // Получить коэффициент баланса узла node\n  int factor = balanceFactor(node);\n  // Левосторонне перекошенное дерево\n  if (factor > 1) {\n    if (balanceFactor(node!.left) >= 0) {\n      // Правое вращение\n      return rightRotate(node);\n    } else {\n      // Сначала левое вращение, затем правое\n      node.left = leftRotate(node.left);\n      return rightRotate(node);\n    }\n  }\n  // Правосторонне перекошенное дерево\n  if (factor < -1) {\n    if (balanceFactor(node!.right) <= 0) {\n      // Левое вращение\n      return leftRotate(node);\n    } else {\n      // Сначала правое вращение, затем левое\n      node.right = rightRotate(node.right);\n      return leftRotate(node);\n    }\n  }\n  // Дерево сбалансировано, вращение не требуется, вернуть сразу\n  return node;\n}\n
        avl_tree.rs
        /* Выполнить вращение, чтобы снова сбалансировать поддерево */\nfn rotate(node: OptionTreeNodeRc) -> OptionTreeNodeRc {\n    // Получить коэффициент баланса узла node\n    let balance_factor = Self::balance_factor(node.clone());\n    // Левосторонне перекошенное дерево\n    if balance_factor > 1 {\n        let node = node.unwrap();\n        if Self::balance_factor(node.borrow().left.clone()) >= 0 {\n            // Правое вращение\n            Self::right_rotate(Some(node))\n        } else {\n            // Сначала левое вращение, затем правое\n            let left = node.borrow().left.clone();\n            node.borrow_mut().left = Self::left_rotate(left);\n            Self::right_rotate(Some(node))\n        }\n    }\n    // Правосторонне перекошенное дерево\n    else if balance_factor < -1 {\n        let node = node.unwrap();\n        if Self::balance_factor(node.borrow().right.clone()) <= 0 {\n            // Левое вращение\n            Self::left_rotate(Some(node))\n        } else {\n            // Сначала правое вращение, затем левое\n            let right = node.borrow().right.clone();\n            node.borrow_mut().right = Self::right_rotate(right);\n            Self::left_rotate(Some(node))\n        }\n    } else {\n        // Дерево сбалансировано, вращение не требуется, вернуть сразу\n        node\n    }\n}\n
        avl_tree.c
        /* Выполнить вращение, чтобы снова сбалансировать поддерево */\nTreeNode *rotate(TreeNode *node) {\n    // Получить коэффициент баланса узла node\n    int bf = balanceFactor(node);\n    // Левосторонне перекошенное дерево\n    if (bf > 1) {\n        if (balanceFactor(node->left) >= 0) {\n            // Правое вращение\n            return rightRotate(node);\n        } else {\n            // Сначала левое вращение, затем правое\n            node->left = leftRotate(node->left);\n            return rightRotate(node);\n        }\n    }\n    // Правосторонне перекошенное дерево\n    if (bf < -1) {\n        if (balanceFactor(node->right) <= 0) {\n            // Левое вращение\n            return leftRotate(node);\n        } else {\n            // Сначала правое вращение, затем левое\n            node->right = rightRotate(node->right);\n            return leftRotate(node);\n        }\n    }\n    // Дерево сбалансировано, вращение не требуется, вернуть сразу\n    return node;\n}\n
        avl_tree.kt
        /* Выполнить вращение, чтобы снова сбалансировать поддерево */\nfun rotate(node: TreeNode): TreeNode {\n    // Получить коэффициент баланса узла node\n    val balanceFactor = balanceFactor(node)\n    // Левосторонне перекошенное дерево\n    if (balanceFactor > 1) {\n        if (balanceFactor(node.left) >= 0) {\n            // Правое вращение\n            return rightRotate(node)\n        } else {\n            // Сначала левое вращение, затем правое\n            node.left = leftRotate(node.left)\n            return rightRotate(node)\n        }\n    }\n    // Правосторонне перекошенное дерево\n    if (balanceFactor < -1) {\n        if (balanceFactor(node.right) <= 0) {\n            // Левое вращение\n            return leftRotate(node)\n        } else {\n            // Сначала правое вращение, затем левое\n            node.right = rightRotate(node.right)\n            return leftRotate(node)\n        }\n    }\n    // Дерево сбалансировано, вращение не требуется, вернуть сразу\n    return node\n}\n
        avl_tree.rb
        ### Выполнить вращение, чтобы снова сбалансировать поддерево ###\ndef rotate(node)\n  # Получить коэффициент баланса узла node\n  balance_factor = balance_factor(node)\n  # Обойти левое поддерево\n  if balance_factor > 1\n    if balance_factor(node.left) >= 0\n      # Правое вращение\n      return right_rotate(node)\n    else\n      # Сначала левое вращение, затем правое\n      node.left = left_rotate(node.left)\n      return right_rotate(node)\n    end\n  # Правостороннее дерево обхода\n  elsif balance_factor < -1\n    if balance_factor(node.right) <= 0\n      # Левое вращение\n      return left_rotate(node)\n    else\n      # Сначала правое вращение, затем левое\n      node.right = right_rotate(node.right)\n      return left_rotate(node)\n    end\n  end\n  # Дерево сбалансировано, вращение не требуется, вернуть сразу\n  node\nend\n
        ","path":["Глава 7. Деревья","7.5   AVL-дерево *"],"tags":[]},{"location":"chapter_tree/avl_tree/#753-avl-","level":2,"title":"7.5.3   Распространенные операции AVL-дерева","text":"","path":["Глава 7. Деревья","7.5   AVL-дерево *"],"tags":[]},{"location":"chapter_tree/avl_tree/#1_2","level":3,"title":"1.   Вставка узла","text":"

        Операция вставки узла в AVL-дерево по основному процессу похожа на вставку в двоичное дерево поиска. Единственная разница состоит в том, что после вставки в AVL-дерево на пути от вставленного узла к корню может появиться цепочка разбалансированных узлов. Поэтому начиная от этого узла, мы должны выполнять вращения снизу вверх, чтобы вернуть в баланс все разбалансированные узлы. Код приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
        def insert(self, val):\n    \"\"\"Вставка узла\"\"\"\n    self._root = self.insert_helper(self._root, val)\n\ndef insert_helper(self, node: TreeNode | None, val: int) -> TreeNode:\n    \"\"\"Рекурсивная вставка узла (вспомогательный метод)\"\"\"\n    if node is None:\n        return TreeNode(val)\n    # 1. Найти позицию вставки и вставить узел\n    if val < node.val:\n        node.left = self.insert_helper(node.left, val)\n    elif val > node.val:\n        node.right = self.insert_helper(node.right, val)\n    else:\n        # Повторяющийся узел не вставлять, сразу вернуть\n        return node\n    # Обновить высоту узла\n    self.update_height(node)\n    # 2. Выполнить вращение, чтобы снова сбалансировать поддерево\n    return self.rotate(node)\n
        avl_tree.cpp
        /* Вставка узла */\nvoid insert(int val) {\n    root = insertHelper(root, val);\n}\n\n/* Рекурсивная вставка узла (вспомогательный метод) */\nTreeNode *insertHelper(TreeNode *node, int val) {\n    if (node == nullptr)\n        return new TreeNode(val);\n    /* 1. Найти позицию вставки и вставить узел */\n    if (val < node->val)\n        node->left = insertHelper(node->left, val);\n    else if (val > node->val)\n        node->right = insertHelper(node->right, val);\n    else\n        return node;    // Повторяющийся узел не вставлять, сразу вернуть\n    updateHeight(node); // Обновить высоту узла\n    /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n    node = rotate(node);\n    // Вернуть корневой узел поддерева\n    return node;\n}\n
        avl_tree.java
        /* Вставка узла */\nvoid insert(int val) {\n    root = insertHelper(root, val);\n}\n\n/* Рекурсивная вставка узла (вспомогательный метод) */\nTreeNode insertHelper(TreeNode node, int val) {\n    if (node == null)\n        return new TreeNode(val);\n    /* 1. Найти позицию вставки и вставить узел */\n    if (val < node.val)\n        node.left = insertHelper(node.left, val);\n    else if (val > node.val)\n        node.right = insertHelper(node.right, val);\n    else\n        return node; // Повторяющийся узел не вставлять, сразу вернуть\n    updateHeight(node); // Обновить высоту узла\n    /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n    node = rotate(node);\n    // Вернуть корневой узел поддерева\n    return node;\n}\n
        avl_tree.cs
        /* Вставка узла */\nvoid Insert(int val) {\n    root = InsertHelper(root, val);\n}\n\n/* Рекурсивная вставка узла (вспомогательный метод) */\nTreeNode? InsertHelper(TreeNode? node, int val) {\n    if (node == null) return new TreeNode(val);\n    /* 1. Найти позицию вставки и вставить узел */\n    if (val < node.val)\n        node.left = InsertHelper(node.left, val);\n    else if (val > node.val)\n        node.right = InsertHelper(node.right, val);\n    else\n        return node;     // Повторяющийся узел не вставлять, сразу вернуть\n    UpdateHeight(node);  // Обновить высоту узла\n    /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n    node = Rotate(node);\n    // Вернуть корневой узел поддерева\n    return node;\n}\n
        avl_tree.go
        /* Вставка узла */\nfunc (t *aVLTree) insert(val int) {\n    t.root = t.insertHelper(t.root, val)\n}\n\n/* Рекурсивная вставка узла (вспомогательная функция) */\nfunc (t *aVLTree) insertHelper(node *TreeNode, val int) *TreeNode {\n    if node == nil {\n        return NewTreeNode(val)\n    }\n    /* 1. Найти позицию вставки и вставить узел */\n    if val < node.Val.(int) {\n        node.Left = t.insertHelper(node.Left, val)\n    } else if val > node.Val.(int) {\n        node.Right = t.insertHelper(node.Right, val)\n    } else {\n        // Повторяющийся узел не вставлять, сразу вернуть\n        return node\n    }\n    // Обновить высоту узла\n    t.updateHeight(node)\n    /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n    node = t.rotate(node)\n    // Вернуть корневой узел поддерева\n    return node\n}\n
        avl_tree.swift
        /* Вставка узла */\nfunc insert(val: Int) {\n    root = insertHelper(node: root, val: val)\n}\n\n/* Рекурсивная вставка узла (вспомогательный метод) */\nfunc insertHelper(node: TreeNode?, val: Int) -> TreeNode? {\n    var node = node\n    if node == nil {\n        return TreeNode(x: val)\n    }\n    /* 1. Найти позицию вставки и вставить узел */\n    if val < node!.val {\n        node?.left = insertHelper(node: node?.left, val: val)\n    } else if val > node!.val {\n        node?.right = insertHelper(node: node?.right, val: val)\n    } else {\n        return node // Повторяющийся узел не вставлять, сразу вернуть\n    }\n    updateHeight(node: node) // Обновить высоту узла\n    /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n    node = rotate(node: node)\n    // Вернуть корневой узел поддерева\n    return node\n}\n
        avl_tree.js
        /* Вставка узла */\ninsert(val) {\n    this.root = this.#insertHelper(this.root, val);\n}\n\n/* Рекурсивная вставка узла (вспомогательный метод) */\n#insertHelper(node, val) {\n    if (node === null) return new TreeNode(val);\n    /* 1. Найти позицию вставки и вставить узел */\n    if (val < node.val) node.left = this.#insertHelper(node.left, val);\n    else if (val > node.val)\n        node.right = this.#insertHelper(node.right, val);\n    else return node; // Повторяющийся узел не вставлять, сразу вернуть\n    this.#updateHeight(node); // Обновить высоту узла\n    /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n    node = this.#rotate(node);\n    // Вернуть корневой узел поддерева\n    return node;\n}\n
        avl_tree.ts
        /* Вставка узла */\ninsert(val: number): void {\n    this.root = this.insertHelper(this.root, val);\n}\n\n/* Рекурсивная вставка узла (вспомогательный метод) */\ninsertHelper(node: TreeNode, val: number): TreeNode {\n    if (node === null) return new TreeNode(val);\n    /* 1. Найти позицию вставки и вставить узел */\n    if (val < node.val) {\n        node.left = this.insertHelper(node.left, val);\n    } else if (val > node.val) {\n        node.right = this.insertHelper(node.right, val);\n    } else {\n        return node; // Повторяющийся узел не вставлять, сразу вернуть\n    }\n    this.updateHeight(node); // Обновить высоту узла\n    /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n    node = this.rotate(node);\n    // Вернуть корневой узел поддерева\n    return node;\n}\n
        avl_tree.dart
        /* Вставка узла */\nvoid insert(int val) {\n  root = insertHelper(root, val);\n}\n\n/* Рекурсивная вставка узла (вспомогательный метод) */\nTreeNode? insertHelper(TreeNode? node, int val) {\n  if (node == null) return TreeNode(val);\n  /* 1. Найти позицию вставки и вставить узел */\n  if (val < node.val)\n    node.left = insertHelper(node.left, val);\n  else if (val > node.val)\n    node.right = insertHelper(node.right, val);\n  else\n    return node; // Повторяющийся узел не вставлять, сразу вернуть\n  updateHeight(node); // Обновить высоту узла\n  /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n  node = rotate(node);\n  // Вернуть корневой узел поддерева\n  return node;\n}\n
        avl_tree.rs
        /* Вставка узла */\nfn insert(&mut self, val: i32) {\n    self.root = Self::insert_helper(self.root.clone(), val);\n}\n\n/* Рекурсивная вставка узла (вспомогательный метод) */\nfn insert_helper(node: OptionTreeNodeRc, val: i32) -> OptionTreeNodeRc {\n    match node {\n        Some(mut node) => {\n            /* 1. Найти позицию вставки и вставить узел */\n            match {\n                let node_val = node.borrow().val;\n                node_val\n            }\n            .cmp(&val)\n            {\n                Ordering::Greater => {\n                    let left = node.borrow().left.clone();\n                    node.borrow_mut().left = Self::insert_helper(left, val);\n                }\n                Ordering::Less => {\n                    let right = node.borrow().right.clone();\n                    node.borrow_mut().right = Self::insert_helper(right, val);\n                }\n                Ordering::Equal => {\n                    return Some(node); // Повторяющийся узел не вставлять, сразу вернуть\n                }\n            }\n            Self::update_height(Some(node.clone())); // Обновить высоту узла\n\n            /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n            node = Self::rotate(Some(node)).unwrap();\n            // Вернуть корневой узел поддерева\n            Some(node)\n        }\n        None => Some(TreeNode::new(val)),\n    }\n}\n
        avl_tree.c
        /* Вставка узла */\nvoid insert(AVLTree *tree, int val) {\n    tree->root = insertHelper(tree->root, val);\n}\n\n/* Рекурсивная вставка узла (вспомогательная функция) */\nTreeNode *insertHelper(TreeNode *node, int val) {\n    if (node == NULL) {\n        return newTreeNode(val);\n    }\n    /* 1. Найти позицию вставки и вставить узел */\n    if (val < node->val) {\n        node->left = insertHelper(node->left, val);\n    } else if (val > node->val) {\n        node->right = insertHelper(node->right, val);\n    } else {\n        // Повторяющийся узел не вставлять, сразу вернуть\n        return node;\n    }\n    // Обновить высоту узла\n    updateHeight(node);\n    /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n    node = rotate(node);\n    // Вернуть корневой узел поддерева\n    return node;\n}\n
        avl_tree.kt
        /* Вставка узла */\nfun insert(_val: Int) {\n    root = insertHelper(root, _val)\n}\n\n/* Рекурсивная вставка узла (вспомогательный метод) */\nfun insertHelper(n: TreeNode?, _val: Int): TreeNode {\n    if (n == null)\n        return TreeNode(_val)\n    var node = n\n    /* 1. Найти позицию вставки и вставить узел */\n    if (_val < node._val)\n        node.left = insertHelper(node.left, _val)\n    else if (_val > node._val)\n        node.right = insertHelper(node.right, _val)\n    else\n        return node // Повторяющийся узел не вставлять, сразу вернуть\n    updateHeight(node) // Обновить высоту узла\n    /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n    node = rotate(node)\n    // Вернуть корневой узел поддерева\n    return node\n}\n
        avl_tree.rb
        ### Вставка узла ###\ndef insert(val)\n  @root = insert_helper(@root, val)\nend\n\n### Вставка узла ###\ndef insert(val)\n  @root = insert_helper(@root, val)\nend\n\n# ## Рекурсивная вставка узла (вспомогательный метод) ###\ndef insert_helper(node, val)\n  return TreeNode.new(val) if node.nil?\n  # 1. Найти позицию вставки и вставить узел\n  if val < node.val\n    node.left = insert_helper(node.left, val)\n  elsif val > node.val\n    node.right = insert_helper(node.right, val)\n  else\n    # Повторяющийся узел не вставлять, сразу вернуть\n    return node\n  end\n  # Обновить высоту узла\n  update_height(node)\n  # 2. Выполнить вращение, чтобы снова сбалансировать поддерево\n  rotate(node)\nend\n
        ","path":["Глава 7. Деревья","7.5   AVL-дерево *"],"tags":[]},{"location":"chapter_tree/avl_tree/#2_1","level":3,"title":"2.   Удаление узла","text":"

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

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
        def remove(self, val: int):\n    \"\"\"Удаление узла\"\"\"\n    self._root = self.remove_helper(self._root, val)\n\ndef remove_helper(self, node: TreeNode | None, val: int) -> TreeNode | None:\n    \"\"\"Рекурсивное удаление узла (вспомогательный метод)\"\"\"\n    if node is None:\n        return None\n    # 1. Найти узел и удалить его\n    if val < node.val:\n        node.left = self.remove_helper(node.left, val)\n    elif val > node.val:\n        node.right = self.remove_helper(node.right, val)\n    else:\n        if node.left is None or node.right is None:\n            child = node.left or node.right\n            # Число дочерних узлов = 0, удалить node и сразу вернуть\n            if child is None:\n                return None\n            # Число дочерних узлов = 1, удалить node напрямую\n            else:\n                node = child\n        else:\n            # Число дочерних узлов = 2, удалить следующий по симметричному обходу узел и заменить им текущий узел\n            temp = node.right\n            while temp.left is not None:\n                temp = temp.left\n            node.right = self.remove_helper(node.right, temp.val)\n            node.val = temp.val\n    # Обновить высоту узла\n    self.update_height(node)\n    # 2. Выполнить вращение, чтобы снова сбалансировать поддерево\n    return self.rotate(node)\n
        avl_tree.cpp
        /* Удаление узла */\nvoid remove(int val) {\n    root = removeHelper(root, val);\n}\n\n/* Рекурсивное удаление узла (вспомогательный метод) */\nTreeNode *removeHelper(TreeNode *node, int val) {\n    if (node == nullptr)\n        return nullptr;\n    /* 1. Найти узел и удалить его */\n    if (val < node->val)\n        node->left = removeHelper(node->left, val);\n    else if (val > node->val)\n        node->right = removeHelper(node->right, val);\n    else {\n        if (node->left == nullptr || node->right == nullptr) {\n            TreeNode *child = node->left != nullptr ? node->left : node->right;\n            // Число дочерних узлов = 0, удалить node и сразу вернуть\n            if (child == nullptr) {\n                delete node;\n                return nullptr;\n            }\n            // Число дочерних узлов = 1, удалить node напрямую\n            else {\n                delete node;\n                node = child;\n            }\n        } else {\n            // Число дочерних узлов = 2, удалить следующий по симметричному обходу узел и заменить им текущий узел\n            TreeNode *temp = node->right;\n            while (temp->left != nullptr) {\n                temp = temp->left;\n            }\n            int tempVal = temp->val;\n            node->right = removeHelper(node->right, temp->val);\n            node->val = tempVal;\n        }\n    }\n    updateHeight(node); // Обновить высоту узла\n    /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n    node = rotate(node);\n    // Вернуть корневой узел поддерева\n    return node;\n}\n
        avl_tree.java
        /* Удаление узла */\nvoid remove(int val) {\n    root = removeHelper(root, val);\n}\n\n/* Рекурсивное удаление узла (вспомогательный метод) */\nTreeNode removeHelper(TreeNode node, int val) {\n    if (node == null)\n        return null;\n    /* 1. Найти узел и удалить его */\n    if (val < node.val)\n        node.left = removeHelper(node.left, val);\n    else if (val > node.val)\n        node.right = removeHelper(node.right, val);\n    else {\n        if (node.left == null || node.right == null) {\n            TreeNode child = node.left != null ? node.left : node.right;\n            // Число дочерних узлов = 0, удалить node и сразу вернуть\n            if (child == null)\n                return null;\n            // Число дочерних узлов = 1, удалить node напрямую\n            else\n                node = child;\n        } else {\n            // Число дочерних узлов = 2, удалить следующий по симметричному обходу узел и заменить им текущий узел\n            TreeNode temp = node.right;\n            while (temp.left != null) {\n                temp = temp.left;\n            }\n            node.right = removeHelper(node.right, temp.val);\n            node.val = temp.val;\n        }\n    }\n    updateHeight(node); // Обновить высоту узла\n    /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n    node = rotate(node);\n    // Вернуть корневой узел поддерева\n    return node;\n}\n
        avl_tree.cs
        /* Удаление узла */\nvoid Remove(int val) {\n    root = RemoveHelper(root, val);\n}\n\n/* Рекурсивное удаление узла (вспомогательный метод) */\nTreeNode? RemoveHelper(TreeNode? node, int val) {\n    if (node == null) return null;\n    /* 1. Найти узел и удалить его */\n    if (val < node.val)\n        node.left = RemoveHelper(node.left, val);\n    else if (val > node.val)\n        node.right = RemoveHelper(node.right, val);\n    else {\n        if (node.left == null || node.right == null) {\n            TreeNode? child = node.left ?? node.right;\n            // Число дочерних узлов = 0, удалить node и сразу вернуть\n            if (child == null)\n                return null;\n            // Число дочерних узлов = 1, удалить node напрямую\n            else\n                node = child;\n        } else {\n            // Число дочерних узлов = 2, удалить следующий по симметричному обходу узел и заменить им текущий узел\n            TreeNode? temp = node.right;\n            while (temp.left != null) {\n                temp = temp.left;\n            }\n            node.right = RemoveHelper(node.right, temp.val!.Value);\n            node.val = temp.val;\n        }\n    }\n    UpdateHeight(node);  // Обновить высоту узла\n    /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n    node = Rotate(node);\n    // Вернуть корневой узел поддерева\n    return node;\n}\n
        avl_tree.go
        /* Удаление узла */\nfunc (t *aVLTree) remove(val int) {\n    t.root = t.removeHelper(t.root, val)\n}\n\n/* Рекурсивное удаление узла (вспомогательная функция) */\nfunc (t *aVLTree) removeHelper(node *TreeNode, val int) *TreeNode {\n    if node == nil {\n        return nil\n    }\n    /* 1. Найти узел и удалить его */\n    if val < node.Val.(int) {\n        node.Left = t.removeHelper(node.Left, val)\n    } else if val > node.Val.(int) {\n        node.Right = t.removeHelper(node.Right, val)\n    } else {\n        if node.Left == nil || node.Right == nil {\n            child := node.Left\n            if node.Right != nil {\n                child = node.Right\n            }\n            if child == nil {\n                // Число дочерних узлов = 0, удалить node и сразу вернуть\n                return nil\n            } else {\n                // Число дочерних узлов = 1, удалить node напрямую\n                node = child\n            }\n        } else {\n            // Число дочерних узлов = 2, удалить следующий по симметричному обходу узел и заменить им текущий узел\n            temp := node.Right\n            for temp.Left != nil {\n                temp = temp.Left\n            }\n            node.Right = t.removeHelper(node.Right, temp.Val.(int))\n            node.Val = temp.Val\n        }\n    }\n    // Обновить высоту узла\n    t.updateHeight(node)\n    /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n    node = t.rotate(node)\n    // Вернуть корневой узел поддерева\n    return node\n}\n
        avl_tree.swift
        /* Удаление узла */\nfunc remove(val: Int) {\n    root = removeHelper(node: root, val: val)\n}\n\n/* Рекурсивное удаление узла (вспомогательный метод) */\nfunc removeHelper(node: TreeNode?, val: Int) -> TreeNode? {\n    var node = node\n    if node == nil {\n        return nil\n    }\n    /* 1. Найти узел и удалить его */\n    if val < node!.val {\n        node?.left = removeHelper(node: node?.left, val: val)\n    } else if val > node!.val {\n        node?.right = removeHelper(node: node?.right, val: val)\n    } else {\n        if node?.left == nil || node?.right == nil {\n            let child = node?.left ?? node?.right\n            // Число дочерних узлов = 0, удалить node и сразу вернуть\n            if child == nil {\n                return nil\n            }\n            // Число дочерних узлов = 1, удалить node напрямую\n            else {\n                node = child\n            }\n        } else {\n            // Число дочерних узлов = 2, удалить следующий по симметричному обходу узел и заменить им текущий узел\n            var temp = node?.right\n            while temp?.left != nil {\n                temp = temp?.left\n            }\n            node?.right = removeHelper(node: node?.right, val: temp!.val)\n            node?.val = temp!.val\n        }\n    }\n    updateHeight(node: node) // Обновить высоту узла\n    /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n    node = rotate(node: node)\n    // Вернуть корневой узел поддерева\n    return node\n}\n
        avl_tree.js
        /* Удаление узла */\nremove(val) {\n    this.root = this.#removeHelper(this.root, val);\n}\n\n/* Рекурсивное удаление узла (вспомогательный метод) */\n#removeHelper(node, val) {\n    if (node === null) return null;\n    /* 1. Найти узел и удалить его */\n    if (val < node.val) node.left = this.#removeHelper(node.left, val);\n    else if (val > node.val)\n        node.right = this.#removeHelper(node.right, val);\n    else {\n        if (node.left === null || node.right === null) {\n            const child = node.left !== null ? node.left : node.right;\n            // Число дочерних узлов = 0, удалить node и сразу вернуть\n            if (child === null) return null;\n            // Число дочерних узлов = 1, удалить node напрямую\n            else node = child;\n        } else {\n            // Число дочерних узлов = 2, удалить следующий по симметричному обходу узел и заменить им текущий узел\n            let temp = node.right;\n            while (temp.left !== null) {\n                temp = temp.left;\n            }\n            node.right = this.#removeHelper(node.right, temp.val);\n            node.val = temp.val;\n        }\n    }\n    this.#updateHeight(node); // Обновить высоту узла\n    /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n    node = this.#rotate(node);\n    // Вернуть корневой узел поддерева\n    return node;\n}\n
        avl_tree.ts
        /* Удаление узла */\nremove(val: number): void {\n    this.root = this.removeHelper(this.root, val);\n}\n\n/* Рекурсивное удаление узла (вспомогательный метод) */\nremoveHelper(node: TreeNode, val: number): TreeNode {\n    if (node === null) return null;\n    /* 1. Найти узел и удалить его */\n    if (val < node.val) {\n        node.left = this.removeHelper(node.left, val);\n    } else if (val > node.val) {\n        node.right = this.removeHelper(node.right, val);\n    } else {\n        if (node.left === null || node.right === null) {\n            const child = node.left !== null ? node.left : node.right;\n            // Число дочерних узлов = 0, удалить node и сразу вернуть\n            if (child === null) {\n                return null;\n            } else {\n                // Число дочерних узлов = 1, удалить node напрямую\n                node = child;\n            }\n        } else {\n            // Число дочерних узлов = 2, удалить следующий по симметричному обходу узел и заменить им текущий узел\n            let temp = node.right;\n            while (temp.left !== null) {\n                temp = temp.left;\n            }\n            node.right = this.removeHelper(node.right, temp.val);\n            node.val = temp.val;\n        }\n    }\n    this.updateHeight(node); // Обновить высоту узла\n    /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n    node = this.rotate(node);\n    // Вернуть корневой узел поддерева\n    return node;\n}\n
        avl_tree.dart
        /* Удаление узла */\nvoid remove(int val) {\n  root = removeHelper(root, val);\n}\n\n/* Рекурсивное удаление узла (вспомогательный метод) */\nTreeNode? removeHelper(TreeNode? node, int val) {\n  if (node == null) return null;\n  /* 1. Найти узел и удалить его */\n  if (val < node.val)\n    node.left = removeHelper(node.left, val);\n  else if (val > node.val)\n    node.right = removeHelper(node.right, val);\n  else {\n    if (node.left == null || node.right == null) {\n      TreeNode? child = node.left ?? node.right;\n      // Число дочерних узлов = 0, удалить node и сразу вернуть\n      if (child == null)\n        return null;\n      // Число дочерних узлов = 1, удалить node напрямую\n      else\n        node = child;\n    } else {\n      // Число дочерних узлов = 2, удалить следующий по симметричному обходу узел и заменить им текущий узел\n      TreeNode? temp = node.right;\n      while (temp!.left != null) {\n        temp = temp.left;\n      }\n      node.right = removeHelper(node.right, temp.val);\n      node.val = temp.val;\n    }\n  }\n  updateHeight(node); // Обновить высоту узла\n  /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n  node = rotate(node);\n  // Вернуть корневой узел поддерева\n  return node;\n}\n
        avl_tree.rs
        /* Удаление узла */\nfn remove(&self, val: i32) {\n    Self::remove_helper(self.root.clone(), val);\n}\n\n/* Рекурсивное удаление узла (вспомогательный метод) */\nfn remove_helper(node: OptionTreeNodeRc, val: i32) -> OptionTreeNodeRc {\n    match node {\n        Some(mut node) => {\n            /* 1. Найти узел и удалить его */\n            if val < node.borrow().val {\n                let left = node.borrow().left.clone();\n                node.borrow_mut().left = Self::remove_helper(left, val);\n            } else if val > node.borrow().val {\n                let right = node.borrow().right.clone();\n                node.borrow_mut().right = Self::remove_helper(right, val);\n            } else if node.borrow().left.is_none() || node.borrow().right.is_none() {\n                let child = if node.borrow().left.is_some() {\n                    node.borrow().left.clone()\n                } else {\n                    node.borrow().right.clone()\n                };\n                match child {\n                    // Число дочерних узлов = 0, удалить node и сразу вернуть\n                    None => {\n                        return None;\n                    }\n                    // Число дочерних узлов = 1, удалить node напрямую\n                    Some(child) => node = child,\n                }\n            } else {\n                // Число дочерних узлов = 2, удалить следующий по симметричному обходу узел и заменить им текущий узел\n                let mut temp = node.borrow().right.clone().unwrap();\n                loop {\n                    let temp_left = temp.borrow().left.clone();\n                    if temp_left.is_none() {\n                        break;\n                    }\n                    temp = temp_left.unwrap();\n                }\n                let right = node.borrow().right.clone();\n                node.borrow_mut().right = Self::remove_helper(right, temp.borrow().val);\n                node.borrow_mut().val = temp.borrow().val;\n            }\n            Self::update_height(Some(node.clone())); // Обновить высоту узла\n\n            /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n            node = Self::rotate(Some(node)).unwrap();\n            // Вернуть корневой узел поддерева\n            Some(node)\n        }\n        None => None,\n    }\n}\n
        avl_tree.c
        /* Удаление узла */\n// Из-за подключения stdio.h здесь нельзя использовать ключевое слово remove\nvoid removeItem(AVLTree *tree, int val) {\n    TreeNode *root = removeHelper(tree->root, val);\n}\n\n/* Рекурсивное удаление узла (вспомогательная функция) */\nTreeNode *removeHelper(TreeNode *node, int val) {\n    TreeNode *child, *grandChild;\n    if (node == NULL) {\n        return NULL;\n    }\n    /* 1. Найти узел и удалить его */\n    if (val < node->val) {\n        node->left = removeHelper(node->left, val);\n    } else if (val > node->val) {\n        node->right = removeHelper(node->right, val);\n    } else {\n        if (node->left == NULL || node->right == NULL) {\n            child = node->left;\n            if (node->right != NULL) {\n                child = node->right;\n            }\n            // Число дочерних узлов = 0, удалить node и сразу вернуть\n            if (child == NULL) {\n                return NULL;\n            } else {\n                // Число дочерних узлов = 1, удалить node напрямую\n                node = child;\n            }\n        } else {\n            // Число дочерних узлов = 2, удалить следующий по симметричному обходу узел и заменить им текущий узел\n            TreeNode *temp = node->right;\n            while (temp->left != NULL) {\n                temp = temp->left;\n            }\n            int tempVal = temp->val;\n            node->right = removeHelper(node->right, temp->val);\n            node->val = tempVal;\n        }\n    }\n    // Обновить высоту узла\n    updateHeight(node);\n    /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n    node = rotate(node);\n    // Вернуть корневой узел поддерева\n    return node;\n}\n
        avl_tree.kt
        /* Удаление узла */\nfun remove(_val: Int) {\n    root = removeHelper(root, _val)\n}\n\n/* Рекурсивное удаление узла (вспомогательный метод) */\nfun removeHelper(n: TreeNode?, _val: Int): TreeNode? {\n    var node = n ?: return null\n    /* 1. Найти узел и удалить его */\n    if (_val < node._val)\n        node.left = removeHelper(node.left, _val)\n    else if (_val > node._val)\n        node.right = removeHelper(node.right, _val)\n    else {\n        if (node.left == null || node.right == null) {\n            val child = if (node.left != null)\n                node.left\n            else\n                node.right\n            // Число дочерних узлов = 0, удалить node и сразу вернуть\n            if (child == null)\n                return null\n            // Число дочерних узлов = 1, удалить node напрямую\n            else\n                node = child\n        } else {\n            // Число дочерних узлов = 2, удалить следующий по симметричному обходу узел и заменить им текущий узел\n            var temp = node.right\n            while (temp!!.left != null) {\n                temp = temp.left\n            }\n            node.right = removeHelper(node.right, temp._val)\n            node._val = temp._val\n        }\n    }\n    updateHeight(node) // Обновить высоту узла\n    /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n    node = rotate(node)\n    // Вернуть корневой узел поддерева\n    return node\n}\n
        avl_tree.rb
        ### Удаление узла ###\ndef remove(val)\n  @root = remove_helper(@root, val)\nend\n\n### Удаление узла ###\ndef remove(val)\n  @root = remove_helper(@root, val)\nend\n\n# ## Рекурсивное удаление узла (вспомогательный метод) ###\ndef remove_helper(node, val)\n  return if node.nil?\n  # 1. Найти узел и удалить его\n  if val < node.val\n    node.left = remove_helper(node.left, val)\n  elsif val > node.val\n    node.right = remove_helper(node.right, val)\n  else\n    if node.left.nil? || node.right.nil?\n      child = node.left || node.right\n      # Число дочерних узлов = 0, удалить node и сразу вернуть\n      return if child.nil?\n      # Число дочерних узлов = 1, удалить node напрямую\n      node = child\n    else\n      # Число дочерних узлов = 2, удалить следующий по симметричному обходу узел и заменить им текущий узел\n      temp = node.right\n      while !temp.left.nil?\n        temp = temp.left\n      end\n      node.right = remove_helper(node.right, temp.val)\n      node.val = temp.val\n    end\n  end\n  # Обновить высоту узла\n  update_height(node)\n  # 2. Выполнить вращение, чтобы снова сбалансировать поддерево\n  rotate(node)\nend\n
        ","path":["Глава 7. Деревья","7.5   AVL-дерево *"],"tags":[]},{"location":"chapter_tree/avl_tree/#3_1","level":3,"title":"3.   Поиск узла","text":"

        Операция поиска узла в AVL-дереве совпадает с поиском в двоичном дереве поиска, поэтому здесь она повторно не рассматривается.

        ","path":["Глава 7. Деревья","7.5   AVL-дерево *"],"tags":[]},{"location":"chapter_tree/avl_tree/#754-avl-","level":2,"title":"7.5.4   Типичные применения AVL-дерева","text":"
        • Организация и хранение больших массивов данных, особенно в сценариях с частым поиском и относительно редкими вставками и удалениями.
        • Использование при построении индексных систем в базах данных.
        • Красно-черное дерево тоже является распространенным видом сбалансированного двоичного дерева поиска. По сравнению с AVL-деревом условия баланса у красно-черного дерева мягче, поэтому при вставке и удалении требуется меньше вращений, а средняя эффективность операций добавления и удаления выше.
        ","path":["Глава 7. Деревья","7.5   AVL-дерево *"],"tags":[]},{"location":"chapter_tree/binary_search_tree/","level":1,"title":"7.4   Двоичное дерево поиска","text":"

        Как показано на рисунке 7-16, двоичное дерево поиска (binary search tree) удовлетворяет следующим условиям.

        1. Для корневого узла все значения в левом поддереве меньше значения корневого узла, а все значения в правом поддереве больше значения корневого узла.
        2. Левое и правое поддеревья любого узла также являются двоичными деревьями поиска, то есть тоже удовлетворяют условию 1. .

        Рисунок 7-16   Двоичное дерево поиска

        ","path":["Глава 7. Деревья","7.4   Двоичное дерево поиска"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#741","level":2,"title":"7.4.1   Операции с двоичным деревом поиска","text":"

        Мы инкапсулируем двоичное дерево поиска в класс BinarySearchTree и объявляем переменную-член root , которая указывает на корневой узел дерева.

        ","path":["Глава 7. Деревья","7.4   Двоичное дерево поиска"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#1","level":3,"title":"1.   Поиск узла","text":"

        Для заданного целевого значения узла num можно выполнить поиск, опираясь на свойства двоичного дерева поиска. Как показано на рисунках ниже, мы объявляем узел cur , стартуем от корня дерева root и циклически сравниваем значения cur.val и num .

        • Если cur.val < num , это означает, что целевой узел находится в правом поддереве cur , поэтому выполняем cur = cur.right .
        • Если cur.val > num , это означает, что целевой узел находится в левом поддереве cur , поэтому выполняем cur = cur.left .
        • Если cur.val = num , это означает, что целевой узел найден, и мы выходим из цикла, возвращая этот узел.
        <1><2><3><4>

        Рисунок 7-17   Пример поиска узла в двоичном дереве поиска

        Операция поиска в двоичном дереве поиска работает по тому же принципу, что и двоичный поиск: на каждом шаге она отбрасывает половину вариантов. Число итераций не превосходит высоты двоичного дерева, а когда дерево сбалансировано, требуется \\(O(\\log n)\\) времени. Пример кода приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_tree.py
        def search(self, num: int) -> TreeNode | None:\n    \"\"\"Поиск узла\"\"\"\n    cur = self._root\n    # Искать в цикле и выйти после прохода за листовой узел\n    while cur is not None:\n        # Целевой узел находится в правом поддереве cur\n        if cur.val < num:\n            cur = cur.right\n        # Целевой узел находится в левом поддереве cur\n        elif cur.val > num:\n            cur = cur.left\n        # Найти целевой узел и выйти из цикла\n        else:\n            break\n    return cur\n
        binary_search_tree.cpp
        /* Поиск узла */\nTreeNode *search(int num) {\n    TreeNode *cur = root;\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur != nullptr) {\n        // Целевой узел находится в правом поддереве cur\n        if (cur->val < num)\n            cur = cur->right;\n        // Целевой узел находится в левом поддереве cur\n        else if (cur->val > num)\n            cur = cur->left;\n        // Найти целевой узел и выйти из цикла\n        else\n            break;\n    }\n    // Вернуть целевой узел\n    return cur;\n}\n
        binary_search_tree.java
        /* Поиск узла */\nTreeNode search(int num) {\n    TreeNode cur = root;\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur != null) {\n        // Целевой узел находится в правом поддереве cur\n        if (cur.val < num)\n            cur = cur.right;\n        // Целевой узел находится в левом поддереве cur\n        else if (cur.val > num)\n            cur = cur.left;\n        // Найти целевой узел и выйти из цикла\n        else\n            break;\n    }\n    // Вернуть целевой узел\n    return cur;\n}\n
        binary_search_tree.cs
        /* Поиск узла */\nTreeNode? Search(int num) {\n    TreeNode? cur = root;\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur != null) {\n        // Целевой узел находится в правом поддереве cur\n        if (cur.val < num) cur =\n            cur.right;\n        // Целевой узел находится в левом поддереве cur\n        else if (cur.val > num)\n            cur = cur.left;\n        // Найти целевой узел и выйти из цикла\n        else\n            break;\n    }\n    // Вернуть целевой узел\n    return cur;\n}\n
        binary_search_tree.go
        /* Поиск узла */\nfunc (bst *binarySearchTree) search(num int) *TreeNode {\n    node := bst.root\n    // Искать в цикле и выйти после прохода за листовой узел\n    for node != nil {\n        if node.Val.(int) < num {\n            // Целевой узел находится в правом поддереве cur\n            node = node.Right\n        } else if node.Val.(int) > num {\n            // Целевой узел находится в левом поддереве cur\n            node = node.Left\n        } else {\n            // Найти целевой узел и выйти из цикла\n            break\n        }\n    }\n    // Вернуть целевой узел\n    return node\n}\n
        binary_search_tree.swift
        /* Поиск узла */\nfunc search(num: Int) -> TreeNode? {\n    var cur = root\n    // Искать в цикле и выйти после прохода за листовой узел\n    while cur != nil {\n        // Целевой узел находится в правом поддереве cur\n        if cur!.val < num {\n            cur = cur?.right\n        }\n        // Целевой узел находится в левом поддереве cur\n        else if cur!.val > num {\n            cur = cur?.left\n        }\n        // Найти целевой узел и выйти из цикла\n        else {\n            break\n        }\n    }\n    // Вернуть целевой узел\n    return cur\n}\n
        binary_search_tree.js
        /* Поиск узла */\nsearch(num) {\n    let cur = this.root;\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur !== null) {\n        // Целевой узел находится в правом поддереве cur\n        if (cur.val < num) cur = cur.right;\n        // Целевой узел находится в левом поддереве cur\n        else if (cur.val > num) cur = cur.left;\n        // Найти целевой узел и выйти из цикла\n        else break;\n    }\n    // Вернуть целевой узел\n    return cur;\n}\n
        binary_search_tree.ts
        /* Поиск узла */\nsearch(num: number): TreeNode | null {\n    let cur = this.root;\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur !== null) {\n        // Целевой узел находится в правом поддереве cur\n        if (cur.val < num) cur = cur.right;\n        // Целевой узел находится в левом поддереве cur\n        else if (cur.val > num) cur = cur.left;\n        // Найти целевой узел и выйти из цикла\n        else break;\n    }\n    // Вернуть целевой узел\n    return cur;\n}\n
        binary_search_tree.dart
        /* Поиск узла */\nTreeNode? search(int _num) {\n  TreeNode? cur = _root;\n  // Искать в цикле и выйти после прохода за листовой узел\n  while (cur != null) {\n    // Целевой узел находится в правом поддереве cur\n    if (cur.val < _num)\n      cur = cur.right;\n    // Целевой узел находится в левом поддереве cur\n    else if (cur.val > _num)\n      cur = cur.left;\n    // Найти целевой узел и выйти из цикла\n    else\n      break;\n  }\n  // Вернуть целевой узел\n  return cur;\n}\n
        binary_search_tree.rs
        /* Поиск узла */\npub fn search(&self, num: i32) -> OptionTreeNodeRc {\n    let mut cur = self.root.clone();\n    // Искать в цикле и выйти после прохода за листовой узел\n    while let Some(node) = cur.clone() {\n        match num.cmp(&node.borrow().val) {\n            // Целевой узел находится в правом поддереве cur\n            Ordering::Greater => cur = node.borrow().right.clone(),\n            // Целевой узел находится в левом поддереве cur\n            Ordering::Less => cur = node.borrow().left.clone(),\n            // Найти целевой узел и выйти из цикла\n            Ordering::Equal => break,\n        }\n    }\n\n    // Вернуть целевой узел\n    cur\n}\n
        binary_search_tree.c
        /* Поиск узла */\nTreeNode *search(BinarySearchTree *bst, int num) {\n    TreeNode *cur = bst->root;\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur != NULL) {\n        if (cur->val < num) {\n            // Целевой узел находится в правом поддереве cur\n            cur = cur->right;\n        } else if (cur->val > num) {\n            // Целевой узел находится в левом поддереве cur\n            cur = cur->left;\n        } else {\n            // Найти целевой узел и выйти из цикла\n            break;\n        }\n    }\n    // Вернуть целевой узел\n    return cur;\n}\n
        binary_search_tree.kt
        /* Поиск узла */\nfun search(num: Int): TreeNode? {\n    var cur = root\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur != null) {\n        // Целевой узел находится в правом поддереве cur\n        cur = if (cur._val < num)\n            cur.right\n        // Целевой узел находится в левом поддереве cur\n        else if (cur._val > num)\n            cur.left\n        // Найти целевой узел и выйти из цикла\n        else\n            break\n    }\n    // Вернуть целевой узел\n    return cur\n}\n
        binary_search_tree.rb
        ### Поиск узла ###\ndef search(num)\n  cur = @root\n\n  # Искать в цикле и выйти после прохода за листовой узел\n  while !cur.nil?\n    # Целевой узел находится в правом поддереве cur\n    if cur.val < num\n      cur = cur.right\n    # Целевой узел находится в левом поддереве cur\n    elsif cur.val > num\n      cur = cur.left\n    # Найти целевой узел и выйти из цикла\n    else\n      break\n    end\n  end\n\n  cur\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 7. Деревья","7.4   Двоичное дерево поиска"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#2","level":3,"title":"2.   Вставка узла","text":"

        Пусть дан элемент num , который нужно вставить. Чтобы сохранить свойство двоичного дерева поиска \"левое поддерево < корень < правое поддерево\", процесс вставки выглядит следующим образом.

        1. Найти позицию для вставки: как и в операции поиска, начиная от корня, мы циклически спускаемся вниз в зависимости от соотношения между текущим значением узла и num , пока не выйдем за листовой узел (то есть не дойдем до None ).
        2. Вставить узел в найденную позицию: инициализировать узел num и поставить его на место этого None .

        Рисунок 7-18   Вставка узла в двоичное дерево поиска

        В реализации кода нужно обратить внимание на следующие два момента.

        • Двоичное дерево поиска не допускает дублирующихся узлов, иначе его определение будет нарушено. Поэтому если вставляемый узел уже существует в дереве, вставка не выполняется и функция сразу возвращается.
        • Чтобы реализовать вставку, нам нужно использовать узел pre для сохранения узла предыдущей итерации цикла. Тогда, когда обход дойдет до None , мы сможем получить его родителя и завершить вставку.
        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_tree.py
        def insert(self, num: int):\n    \"\"\"Вставка узла\"\"\"\n    # Если дерево пусто, инициализировать корневой узел\n    if self._root is None:\n        self._root = TreeNode(num)\n        return\n    # Искать в цикле и выйти после прохода за листовой узел\n    cur, pre = self._root, None\n    while cur is not None:\n        # Найти повторяющийся узел и сразу вернуть\n        if cur.val == num:\n            return\n        pre = cur\n        # Позиция вставки находится в правом поддереве cur\n        if cur.val < num:\n            cur = cur.right\n        # Позиция вставки находится в левом поддереве cur\n        else:\n            cur = cur.left\n    # Вставка узла\n    node = TreeNode(num)\n    if pre.val < num:\n        pre.right = node\n    else:\n        pre.left = node\n
        binary_search_tree.cpp
        /* Вставка узла */\nvoid insert(int num) {\n    // Если дерево пусто, инициализировать корневой узел\n    if (root == nullptr) {\n        root = new TreeNode(num);\n        return;\n    }\n    TreeNode *cur = root, *pre = nullptr;\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur != nullptr) {\n        // Найти повторяющийся узел и сразу вернуть\n        if (cur->val == num)\n            return;\n        pre = cur;\n        // Позиция вставки находится в правом поддереве cur\n        if (cur->val < num)\n            cur = cur->right;\n        // Позиция вставки находится в левом поддереве cur\n        else\n            cur = cur->left;\n    }\n    // Вставка узла\n    TreeNode *node = new TreeNode(num);\n    if (pre->val < num)\n        pre->right = node;\n    else\n        pre->left = node;\n}\n
        binary_search_tree.java
        /* Вставка узла */\nvoid insert(int num) {\n    // Если дерево пусто, инициализировать корневой узел\n    if (root == null) {\n        root = new TreeNode(num);\n        return;\n    }\n    TreeNode cur = root, pre = null;\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur != null) {\n        // Найти повторяющийся узел и сразу вернуть\n        if (cur.val == num)\n            return;\n        pre = cur;\n        // Позиция вставки находится в правом поддереве cur\n        if (cur.val < num)\n            cur = cur.right;\n        // Позиция вставки находится в левом поддереве cur\n        else\n            cur = cur.left;\n    }\n    // Вставка узла\n    TreeNode node = new TreeNode(num);\n    if (pre.val < num)\n        pre.right = node;\n    else\n        pre.left = node;\n}\n
        binary_search_tree.cs
        /* Вставка узла */\nvoid Insert(int num) {\n    // Если дерево пусто, инициализировать корневой узел\n    if (root == null) {\n        root = new TreeNode(num);\n        return;\n    }\n    TreeNode? cur = root, pre = null;\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur != null) {\n        // Найти повторяющийся узел и сразу вернуть\n        if (cur.val == num)\n            return;\n        pre = cur;\n        // Позиция вставки находится в правом поддереве cur\n        if (cur.val < num)\n            cur = cur.right;\n        // Позиция вставки находится в левом поддереве cur\n        else\n            cur = cur.left;\n    }\n\n    // Вставка узла\n    TreeNode node = new(num);\n    if (pre != null) {\n        if (pre.val < num)\n            pre.right = node;\n        else\n            pre.left = node;\n    }\n}\n
        binary_search_tree.go
        /* Вставка узла */\nfunc (bst *binarySearchTree) insert(num int) {\n    cur := bst.root\n    // Если дерево пусто, инициализировать корневой узел\n    if cur == nil {\n        bst.root = NewTreeNode(num)\n        return\n    }\n    // Позиция узла, предшествующего вставляемому\n    var pre *TreeNode = nil\n    // Искать в цикле и выйти после прохода за листовой узел\n    for cur != nil {\n        if cur.Val == num {\n            return\n        }\n        pre = cur\n        if cur.Val.(int) < num {\n            cur = cur.Right\n        } else {\n            cur = cur.Left\n        }\n    }\n    // Вставка узла\n    node := NewTreeNode(num)\n    if pre.Val.(int) < num {\n        pre.Right = node\n    } else {\n        pre.Left = node\n    }\n}\n
        binary_search_tree.swift
        /* Вставка узла */\nfunc insert(num: Int) {\n    // Если дерево пусто, инициализировать корневой узел\n    if root == nil {\n        root = TreeNode(x: num)\n        return\n    }\n    var cur = root\n    var pre: TreeNode?\n    // Искать в цикле и выйти после прохода за листовой узел\n    while cur != nil {\n        // Найти повторяющийся узел и сразу вернуть\n        if cur!.val == num {\n            return\n        }\n        pre = cur\n        // Позиция вставки находится в правом поддереве cur\n        if cur!.val < num {\n            cur = cur?.right\n        }\n        // Позиция вставки находится в левом поддереве cur\n        else {\n            cur = cur?.left\n        }\n    }\n    // Вставка узла\n    let node = TreeNode(x: num)\n    if pre!.val < num {\n        pre?.right = node\n    } else {\n        pre?.left = node\n    }\n}\n
        binary_search_tree.js
        /* Вставка узла */\ninsert(num) {\n    // Если дерево пусто, инициализировать корневой узел\n    if (this.root === null) {\n        this.root = new TreeNode(num);\n        return;\n    }\n    let cur = this.root,\n        pre = null;\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur !== null) {\n        // Найти повторяющийся узел и сразу вернуть\n        if (cur.val === num) return;\n        pre = cur;\n        // Позиция вставки находится в правом поддереве cur\n        if (cur.val < num) cur = cur.right;\n        // Позиция вставки находится в левом поддереве cur\n        else cur = cur.left;\n    }\n    // Вставка узла\n    const node = new TreeNode(num);\n    if (pre.val < num) pre.right = node;\n    else pre.left = node;\n}\n
        binary_search_tree.ts
        /* Вставка узла */\ninsert(num: number): void {\n    // Если дерево пусто, инициализировать корневой узел\n    if (this.root === null) {\n        this.root = new TreeNode(num);\n        return;\n    }\n    let cur: TreeNode | null = this.root,\n        pre: TreeNode | null = null;\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur !== null) {\n        // Найти повторяющийся узел и сразу вернуть\n        if (cur.val === num) return;\n        pre = cur;\n        // Позиция вставки находится в правом поддереве cur\n        if (cur.val < num) cur = cur.right;\n        // Позиция вставки находится в левом поддереве cur\n        else cur = cur.left;\n    }\n    // Вставка узла\n    const node = new TreeNode(num);\n    if (pre!.val < num) pre!.right = node;\n    else pre!.left = node;\n}\n
        binary_search_tree.dart
        /* Вставка узла */\nvoid insert(int _num) {\n  // Если дерево пусто, инициализировать корневой узел\n  if (_root == null) {\n    _root = TreeNode(_num);\n    return;\n  }\n  TreeNode? cur = _root;\n  TreeNode? pre = null;\n  // Искать в цикле и выйти после прохода за листовой узел\n  while (cur != null) {\n    // Найти повторяющийся узел и сразу вернуть\n    if (cur.val == _num) return;\n    pre = cur;\n    // Позиция вставки находится в правом поддереве cur\n    if (cur.val < _num)\n      cur = cur.right;\n    // Позиция вставки находится в левом поддереве cur\n    else\n      cur = cur.left;\n  }\n  // Вставка узла\n  TreeNode? node = TreeNode(_num);\n  if (pre!.val < _num)\n    pre.right = node;\n  else\n    pre.left = node;\n}\n
        binary_search_tree.rs
        /* Вставка узла */\npub fn insert(&mut self, num: i32) {\n    // Если дерево пусто, инициализировать корневой узел\n    if self.root.is_none() {\n        self.root = Some(TreeNode::new(num));\n        return;\n    }\n    let mut cur = self.root.clone();\n    let mut pre = None;\n    // Искать в цикле и выйти после прохода за листовой узел\n    while let Some(node) = cur.clone() {\n        match num.cmp(&node.borrow().val) {\n            // Найти повторяющийся узел и сразу вернуть\n            Ordering::Equal => return,\n            // Позиция вставки находится в правом поддереве cur\n            Ordering::Greater => {\n                pre = cur.clone();\n                cur = node.borrow().right.clone();\n            }\n            // Позиция вставки находится в левом поддереве cur\n            Ordering::Less => {\n                pre = cur.clone();\n                cur = node.borrow().left.clone();\n            }\n        }\n    }\n    // Вставка узла\n    let pre = pre.unwrap();\n    let node = Some(TreeNode::new(num));\n    if num > pre.borrow().val {\n        pre.borrow_mut().right = node;\n    } else {\n        pre.borrow_mut().left = node;\n    }\n}\n
        binary_search_tree.c
        /* Вставка узла */\nvoid insert(BinarySearchTree *bst, int num) {\n    // Если дерево пусто, инициализировать корневой узел\n    if (bst->root == NULL) {\n        bst->root = newTreeNode(num);\n        return;\n    }\n    TreeNode *cur = bst->root, *pre = NULL;\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur != NULL) {\n        // Найти повторяющийся узел и сразу вернуть\n        if (cur->val == num) {\n            return;\n        }\n        pre = cur;\n        if (cur->val < num) {\n            // Позиция вставки находится в правом поддереве cur\n            cur = cur->right;\n        } else {\n            // Позиция вставки находится в левом поддереве cur\n            cur = cur->left;\n        }\n    }\n    // Вставка узла\n    TreeNode *node = newTreeNode(num);\n    if (pre->val < num) {\n        pre->right = node;\n    } else {\n        pre->left = node;\n    }\n}\n
        binary_search_tree.kt
        /* Вставка узла */\nfun insert(num: Int) {\n    // Если дерево пусто, инициализировать корневой узел\n    if (root == null) {\n        root = TreeNode(num)\n        return\n    }\n    var cur = root\n    var pre: TreeNode? = null\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur != null) {\n        // Найти повторяющийся узел и сразу вернуть\n        if (cur._val == num)\n            return\n        pre = cur\n        // Позиция вставки находится в правом поддереве cur\n        cur = if (cur._val < num)\n            cur.right\n        // Позиция вставки находится в левом поддереве cur\n        else\n            cur.left\n    }\n    // Вставка узла\n    val node = TreeNode(num)\n    if (pre?._val!! < num)\n        pre.right = node\n    else\n        pre.left = node\n}\n
        binary_search_tree.rb
        ### Вставка узла ###\ndef insert(num)\n  # Если дерево пусто, инициализировать корневой узел\n  if @root.nil?\n    @root = TreeNode.new(num)\n    return\n  end\n\n  # Искать в цикле и выйти после прохода за листовой узел\n  cur, pre = @root, nil\n  while !cur.nil?\n    # Найти повторяющийся узел и сразу вернуть\n    return if cur.val == num\n\n    pre = cur\n    # Позиция вставки находится в правом поддереве cur\n    if cur.val < num\n      cur = cur.right\n    # Позиция вставки находится в левом поддереве cur\n    else\n      cur = cur.left\n    end\n  end\n\n  # Вставка узла\n  node = TreeNode.new(num)\n  if pre.val < num\n    pre.right = node\n  else\n    pre.left = node\n  end\nend\n
        Визуализация кода

        Во весь экран >

        Как и поиск узла, вставка узла требует \\(O(\\log n)\\) времени.

        ","path":["Глава 7. Деревья","7.4   Двоичное дерево поиска"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#3","level":3,"title":"3.   Удаление узла","text":"

        Сначала нужно найти в двоичном дереве целевой узел, а затем удалить его. Как и при вставке, после удаления необходимо сохранить свойство двоичного дерева поиска: \"левое поддерево < корень < правое поддерево\". Поэтому в зависимости от числа дочерних узлов у удаляемого узла, то есть для случаев со степенью 0, 1 и 2, выполняются разные операции удаления.

        Как показано на рисунке 7-19, когда степень удаляемого узла равна \\(0\\) , это значит, что узел является листом и может быть удален напрямую.

        Рисунок 7-19   Удаление узла в двоичном дереве поиска (степень 0)

        Как показано на рисунке 7-20, когда степень удаляемого узла равна \\(1\\) , достаточно заменить удаляемый узел его дочерним узлом.

        Рисунок 7-20   Удаление узла в двоичном дереве поиска (степень 1)

        Когда степень удаляемого узла равна \\(2\\) , мы уже не можем удалить его напрямую и должны использовать для замены другой узел. Чтобы сохранить свойство двоичного дерева поиска \"левое поддерево \\(<\\) корень \\(<\\) правое поддерево\", этим узлом может быть минимальный узел правого поддерева или максимальный узел левого поддерева.

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

        1. Найти следующий узел в \"последовательности симметричного обхода\" для удаляемого узла и обозначить его как tmp .
        2. Значением tmp перезаписать значение удаляемого узла, а затем рекурсивно удалить узел tmp из дерева.
        <1><2><3><4>

        Рисунок 7-21   Удаление узла в двоичном дереве поиска (степень 2)

        Операция удаления узла также требует \\(O(\\log n)\\) времени, где поиск удаляемого узла стоит \\(O(\\log n)\\) , а получение следующего узла симметричного обхода также требует \\(O(\\log n)\\) . Пример кода приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_tree.py
        def remove(self, num: int):\n    \"\"\"Удаление узла\"\"\"\n    # Если дерево пусто, сразу вернуть\n    if self._root is None:\n        return\n    # Искать в цикле и выйти после прохода за листовой узел\n    cur, pre = self._root, None\n    while cur is not None:\n        # Найти узел для удаления и выйти из цикла\n        if cur.val == num:\n            break\n        pre = cur\n        # Узел для удаления находится в правом поддереве cur\n        if cur.val < num:\n            cur = cur.right\n        # Узел для удаления находится в левом поддереве cur\n        else:\n            cur = cur.left\n    # Если узел для удаления отсутствует, сразу вернуть\n    if cur is None:\n        return\n\n    # Число дочерних узлов = 0 или 1\n    if cur.left is None or cur.right is None:\n        # Когда число дочерних узлов = 0 / 1, child = null / этот дочерний узел\n        child = cur.left or cur.right\n        # Удалить узел cur\n        if cur != self._root:\n            if pre.left == cur:\n                pre.left = child\n            else:\n                pre.right = child\n        else:\n            # Если удаляемый узел является корнем, заново назначить корневой узел\n            self._root = child\n    # Число дочерних узлов = 2\n    else:\n        # Получить следующий узел после cur в симметричном обходе\n        tmp: TreeNode = cur.right\n        while tmp.left is not None:\n            tmp = tmp.left\n        # Рекурсивно удалить узел tmp\n        self.remove(tmp.val)\n        # Перезаписать cur значением tmp\n        cur.val = tmp.val\n
        binary_search_tree.cpp
        /* Удаление узла */\nvoid remove(int num) {\n    // Если дерево пусто, сразу вернуть\n    if (root == nullptr)\n        return;\n    TreeNode *cur = root, *pre = nullptr;\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur != nullptr) {\n        // Найти узел для удаления и выйти из цикла\n        if (cur->val == num)\n            break;\n        pre = cur;\n        // Узел для удаления находится в правом поддереве cur\n        if (cur->val < num)\n            cur = cur->right;\n        // Узел для удаления находится в левом поддереве cur\n        else\n            cur = cur->left;\n    }\n    // Если узел для удаления отсутствует, сразу вернуть\n    if (cur == nullptr)\n        return;\n    // Число дочерних узлов = 0 или 1\n    if (cur->left == nullptr || cur->right == nullptr) {\n        // Когда число дочерних узлов = 0 / 1, child = nullptr / этот дочерний узел\n        TreeNode *child = cur->left != nullptr ? cur->left : cur->right;\n        // Удалить узел cur\n        if (cur != root) {\n            if (pre->left == cur)\n                pre->left = child;\n            else\n                pre->right = child;\n        } else {\n            // Если удаляемый узел является корнем, заново назначить корневой узел\n            root = child;\n        }\n        // Освободить память\n        delete cur;\n    }\n    // Число дочерних узлов = 2\n    else {\n        // Получить следующий узел после cur в симметричном обходе\n        TreeNode *tmp = cur->right;\n        while (tmp->left != nullptr) {\n            tmp = tmp->left;\n        }\n        int tmpVal = tmp->val;\n        // Рекурсивно удалить узел tmp\n        remove(tmp->val);\n        // Перезаписать cur значением tmp\n        cur->val = tmpVal;\n    }\n}\n
        binary_search_tree.java
        /* Удаление узла */\nvoid remove(int num) {\n    // Если дерево пусто, сразу вернуть\n    if (root == null)\n        return;\n    TreeNode cur = root, pre = null;\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur != null) {\n        // Найти узел для удаления и выйти из цикла\n        if (cur.val == num)\n            break;\n        pre = cur;\n        // Узел для удаления находится в правом поддереве cur\n        if (cur.val < num)\n            cur = cur.right;\n        // Узел для удаления находится в левом поддереве cur\n        else\n            cur = cur.left;\n    }\n    // Если узел для удаления отсутствует, сразу вернуть\n    if (cur == null)\n        return;\n    // Число дочерних узлов = 0 или 1\n    if (cur.left == null || cur.right == null) {\n        // Когда число дочерних узлов = 0 / 1, child = null / этот дочерний узел\n        TreeNode child = cur.left != null ? cur.left : cur.right;\n        // Удалить узел cur\n        if (cur != root) {\n            if (pre.left == cur)\n                pre.left = child;\n            else\n                pre.right = child;\n        } else {\n            // Если удаляемый узел является корнем, заново назначить корневой узел\n            root = child;\n        }\n    }\n    // Число дочерних узлов = 2\n    else {\n        // Получить следующий узел после cur в симметричном обходе\n        TreeNode tmp = cur.right;\n        while (tmp.left != null) {\n            tmp = tmp.left;\n        }\n        // Рекурсивно удалить узел tmp\n        remove(tmp.val);\n        // Перезаписать cur значением tmp\n        cur.val = tmp.val;\n    }\n}\n
        binary_search_tree.cs
        /* Удаление узла */\nvoid Remove(int num) {\n    // Если дерево пусто, сразу вернуть\n    if (root == null)\n        return;\n    TreeNode? cur = root, pre = null;\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur != null) {\n        // Найти узел для удаления и выйти из цикла\n        if (cur.val == num)\n            break;\n        pre = cur;\n        // Узел для удаления находится в правом поддереве cur\n        if (cur.val < num)\n            cur = cur.right;\n        // Узел для удаления находится в левом поддереве cur\n        else\n            cur = cur.left;\n    }\n    // Если узел для удаления отсутствует, сразу вернуть\n    if (cur == null)\n        return;\n    // Число дочерних узлов = 0 или 1\n    if (cur.left == null || cur.right == null) {\n        // Когда число дочерних узлов = 0 / 1, child = null / этот дочерний узел\n        TreeNode? child = cur.left ?? cur.right;\n        // Удалить узел cur\n        if (cur != root) {\n            if (pre!.left == cur)\n                pre.left = child;\n            else\n                pre.right = child;\n        } else {\n            // Если удаляемый узел является корнем, заново назначить корневой узел\n            root = child;\n        }\n    }\n    // Число дочерних узлов = 2\n    else {\n        // Получить следующий узел после cur в симметричном обходе\n        TreeNode? tmp = cur.right;\n        while (tmp.left != null) {\n            tmp = tmp.left;\n        }\n        // Рекурсивно удалить узел tmp\n        Remove(tmp.val!.Value);\n        // Перезаписать cur значением tmp\n        cur.val = tmp.val;\n    }\n}\n
        binary_search_tree.go
        /* Удаление узла */\nfunc (bst *binarySearchTree) remove(num int) {\n    cur := bst.root\n    // Если дерево пусто, сразу вернуть\n    if cur == nil {\n        return\n    }\n    // Позиция узла, предшествующего удаляемому\n    var pre *TreeNode = nil\n    // Искать в цикле и выйти после прохода за листовой узел\n    for cur != nil {\n        if cur.Val == num {\n            break\n        }\n        pre = cur\n        if cur.Val.(int) < num {\n            // Удаляемый узел находится в правом поддереве\n            cur = cur.Right\n        } else {\n            // Удаляемый узел находится в левом поддереве\n            cur = cur.Left\n        }\n    }\n    // Если узел для удаления отсутствует, сразу вернуть\n    if cur == nil {\n        return\n    }\n    // Число дочерних узлов равно 0 или 1\n    if cur.Left == nil || cur.Right == nil {\n        var child *TreeNode = nil\n        // Извлечь дочерний узел удаляемого узла\n        if cur.Left != nil {\n            child = cur.Left\n        } else {\n            child = cur.Right\n        }\n        // Удалить узел cur\n        if cur != bst.root {\n            if pre.Left == cur {\n                pre.Left = child\n            } else {\n                pre.Right = child\n            }\n        } else {\n            // Если удаляемый узел является корнем, заново назначить корневой узел\n            bst.root = child\n        }\n        // Число дочерних узлов равно 2\n    } else {\n        // Получить следующий после cur узел в симметричном обходе для удаляемого узла\n        tmp := cur.Right\n        for tmp.Left != nil {\n            tmp = tmp.Left\n        }\n        // Рекурсивно удалить узел tmp\n        bst.remove(tmp.Val.(int))\n        // Перезаписать cur значением tmp\n        cur.Val = tmp.Val\n    }\n}\n
        binary_search_tree.swift
        /* Удаление узла */\nfunc remove(num: Int) {\n    // Если дерево пусто, сразу вернуть\n    if root == nil {\n        return\n    }\n    var cur = root\n    var pre: TreeNode?\n    // Искать в цикле и выйти после прохода за листовой узел\n    while cur != nil {\n        // Найти узел для удаления и выйти из цикла\n        if cur!.val == num {\n            break\n        }\n        pre = cur\n        // Узел для удаления находится в правом поддереве cur\n        if cur!.val < num {\n            cur = cur?.right\n        }\n        // Узел для удаления находится в левом поддереве cur\n        else {\n            cur = cur?.left\n        }\n    }\n    // Если узел для удаления отсутствует, сразу вернуть\n    if cur == nil {\n        return\n    }\n    // Число дочерних узлов = 0 или 1\n    if cur?.left == nil || cur?.right == nil {\n        // Когда число дочерних узлов = 0 / 1, child = null / этот дочерний узел\n        let child = cur?.left ?? cur?.right\n        // Удалить узел cur\n        if cur !== root {\n            if pre?.left === cur {\n                pre?.left = child\n            } else {\n                pre?.right = child\n            }\n        } else {\n            // Если удаляемый узел является корнем, заново назначить корневой узел\n            root = child\n        }\n    }\n    // Число дочерних узлов = 2\n    else {\n        // Получить следующий узел после cur в симметричном обходе\n        var tmp = cur?.right\n        while tmp?.left != nil {\n            tmp = tmp?.left\n        }\n        // Рекурсивно удалить узел tmp\n        remove(num: tmp!.val)\n        // Перезаписать cur значением tmp\n        cur?.val = tmp!.val\n    }\n}\n
        binary_search_tree.js
        /* Удаление узла */\nremove(num) {\n    // Если дерево пусто, сразу вернуть\n    if (this.root === null) return;\n    let cur = this.root,\n        pre = null;\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur !== null) {\n        // Найти узел для удаления и выйти из цикла\n        if (cur.val === num) break;\n        pre = cur;\n        // Узел для удаления находится в правом поддереве cur\n        if (cur.val < num) cur = cur.right;\n        // Узел для удаления находится в левом поддереве cur\n        else cur = cur.left;\n    }\n    // Если узел для удаления отсутствует, сразу вернуть\n    if (cur === null) return;\n    // Число дочерних узлов = 0 или 1\n    if (cur.left === null || cur.right === null) {\n        // Когда число дочерних узлов = 0 / 1, child = null / этот дочерний узел\n        const child = cur.left !== null ? cur.left : cur.right;\n        // Удалить узел cur\n        if (cur !== this.root) {\n            if (pre.left === cur) pre.left = child;\n            else pre.right = child;\n        } else {\n            // Если удаляемый узел является корнем, заново назначить корневой узел\n            this.root = child;\n        }\n    }\n    // Число дочерних узлов = 2\n    else {\n        // Получить следующий узел после cur в симметричном обходе\n        let tmp = cur.right;\n        while (tmp.left !== null) {\n            tmp = tmp.left;\n        }\n        // Рекурсивно удалить узел tmp\n        this.remove(tmp.val);\n        // Перезаписать cur значением tmp\n        cur.val = tmp.val;\n    }\n}\n
        binary_search_tree.ts
        /* Удаление узла */\nremove(num: number): void {\n    // Если дерево пусто, сразу вернуть\n    if (this.root === null) return;\n    let cur: TreeNode | null = this.root,\n        pre: TreeNode | null = null;\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur !== null) {\n        // Найти узел для удаления и выйти из цикла\n        if (cur.val === num) break;\n        pre = cur;\n        // Узел для удаления находится в правом поддереве cur\n        if (cur.val < num) cur = cur.right;\n        // Узел для удаления находится в левом поддереве cur\n        else cur = cur.left;\n    }\n    // Если узел для удаления отсутствует, сразу вернуть\n    if (cur === null) return;\n    // Число дочерних узлов = 0 или 1\n    if (cur.left === null || cur.right === null) {\n        // Когда число дочерних узлов = 0 / 1, child = null / этот дочерний узел\n        const child: TreeNode | null =\n            cur.left !== null ? cur.left : cur.right;\n        // Удалить узел cur\n        if (cur !== this.root) {\n            if (pre!.left === cur) pre!.left = child;\n            else pre!.right = child;\n        } else {\n            // Если удаляемый узел является корнем, заново назначить корневой узел\n            this.root = child;\n        }\n    }\n    // Число дочерних узлов = 2\n    else {\n        // Получить следующий узел после cur в симметричном обходе\n        let tmp: TreeNode | null = cur.right;\n        while (tmp!.left !== null) {\n            tmp = tmp!.left;\n        }\n        // Рекурсивно удалить узел tmp\n        this.remove(tmp!.val);\n        // Перезаписать cur значением tmp\n        cur.val = tmp!.val;\n    }\n}\n
        binary_search_tree.dart
        /* Удаление узла */\nvoid remove(int _num) {\n  // Если дерево пусто, сразу вернуть\n  if (_root == null) return;\n  TreeNode? cur = _root;\n  TreeNode? pre = null;\n  // Искать в цикле и выйти после прохода за листовой узел\n  while (cur != null) {\n    // Найти узел для удаления и выйти из цикла\n    if (cur.val == _num) break;\n    pre = cur;\n    // Узел для удаления находится в правом поддереве cur\n    if (cur.val < _num)\n      cur = cur.right;\n    // Узел для удаления находится в левом поддереве cur\n    else\n      cur = cur.left;\n  }\n  // Если удаляемого узла нет, сразу вернуть\n  if (cur == null) return;\n  // Число дочерних узлов = 0 или 1\n  if (cur.left == null || cur.right == null) {\n    // Когда число дочерних узлов = 0 / 1, child = null / этот дочерний узел\n    TreeNode? child = cur.left ?? cur.right;\n    // Удалить узел cur\n    if (cur != _root) {\n      if (pre!.left == cur)\n        pre.left = child;\n      else\n        pre.right = child;\n    } else {\n      // Если удаляемый узел является корнем, заново назначить корневой узел\n      _root = child;\n    }\n  } else {\n    // Число дочерних узлов = 2\n    // Получить следующий узел после cur в симметричном обходе\n    TreeNode? tmp = cur.right;\n    while (tmp!.left != null) {\n      tmp = tmp.left;\n    }\n    // Рекурсивно удалить узел tmp\n    remove(tmp.val);\n    // Перезаписать cur значением tmp\n    cur.val = tmp.val;\n  }\n}\n
        binary_search_tree.rs
        /* Удаление узла */\npub fn remove(&mut self, num: i32) {\n    // Если дерево пусто, сразу вернуть\n    if self.root.is_none() {\n        return;\n    }\n    let mut cur = self.root.clone();\n    let mut pre = None;\n    // Искать в цикле и выйти после прохода за листовой узел\n    while let Some(node) = cur.clone() {\n        match num.cmp(&node.borrow().val) {\n            // Найти узел для удаления и выйти из цикла\n            Ordering::Equal => break,\n            // Узел для удаления находится в правом поддереве cur\n            Ordering::Greater => {\n                pre = cur.clone();\n                cur = node.borrow().right.clone();\n            }\n            // Узел для удаления находится в левом поддереве cur\n            Ordering::Less => {\n                pre = cur.clone();\n                cur = node.borrow().left.clone();\n            }\n        }\n    }\n    // Если узел для удаления отсутствует, сразу вернуть\n    if cur.is_none() {\n        return;\n    }\n    let cur = cur.unwrap();\n    let (left_child, right_child) = (cur.borrow().left.clone(), cur.borrow().right.clone());\n    match (left_child.clone(), right_child.clone()) {\n        // Число дочерних узлов = 0 или 1\n        (None, None) | (Some(_), None) | (None, Some(_)) => {\n            // Когда число дочерних узлов = 0 / 1, child = nullptr / этот дочерний узел\n            let child = left_child.or(right_child);\n            let pre = pre.unwrap();\n            // Удалить узел cur\n            if !Rc::ptr_eq(&cur, self.root.as_ref().unwrap()) {\n                let left = pre.borrow().left.clone();\n                if left.is_some() && Rc::ptr_eq(left.as_ref().unwrap(), &cur) {\n                    pre.borrow_mut().left = child;\n                } else {\n                    pre.borrow_mut().right = child;\n                }\n            } else {\n                // Если удаляемый узел является корнем, заново назначить корневой узел\n                self.root = child;\n            }\n        }\n        // Число дочерних узлов = 2\n        (Some(_), Some(_)) => {\n            // Получить следующий узел после cur в симметричном обходе\n            let mut tmp = cur.borrow().right.clone();\n            while let Some(node) = tmp.clone() {\n                if node.borrow().left.is_some() {\n                    tmp = node.borrow().left.clone();\n                } else {\n                    break;\n                }\n            }\n            let tmp_val = tmp.unwrap().borrow().val;\n            // Рекурсивно удалить узел tmp\n            self.remove(tmp_val);\n            // Перезаписать cur значением tmp\n            cur.borrow_mut().val = tmp_val;\n        }\n    }\n}\n
        binary_search_tree.c
        /* Удаление узла */\n// Из-за подключения stdio.h здесь нельзя использовать ключевое слово remove\nvoid removeItem(BinarySearchTree *bst, int num) {\n    // Если дерево пусто, сразу вернуть\n    if (bst->root == NULL)\n        return;\n    TreeNode *cur = bst->root, *pre = NULL;\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur != NULL) {\n        // Найти узел для удаления и выйти из цикла\n        if (cur->val == num)\n            break;\n        pre = cur;\n        if (cur->val < num) {\n            // Удаляемый узел находится в правом поддереве root\n            cur = cur->right;\n        } else {\n            // Удаляемый узел находится в левом поддереве root\n            cur = cur->left;\n        }\n    }\n    // Если узел для удаления отсутствует, сразу вернуть\n    if (cur == NULL)\n        return;\n    // Проверить, есть ли дочерние узлы у удаляемого узла\n    if (cur->left == NULL || cur->right == NULL) {\n        /* Число дочерних узлов = 0 или 1 */\n        // Когда число дочерних узлов = 0 / 1, child = nullptr / этот дочерний узел\n        TreeNode *child = cur->left != NULL ? cur->left : cur->right;\n        // Удалить узел cur\n        if (pre->left == cur) {\n            pre->left = child;\n        } else {\n            pre->right = child;\n        }\n        // Освободить память\n        free(cur);\n    } else {\n        /* Число дочерних узлов = 2 */\n        // Получить следующий узел после cur в симметричном обходе\n        TreeNode *tmp = cur->right;\n        while (tmp->left != NULL) {\n            tmp = tmp->left;\n        }\n        int tmpVal = tmp->val;\n        // Рекурсивно удалить узел tmp\n        removeItem(bst, tmp->val);\n        // Перезаписать cur значением tmp\n        cur->val = tmpVal;\n    }\n}\n
        binary_search_tree.kt
        /* Удаление узла */\nfun remove(num: Int) {\n    // Если дерево пусто, сразу вернуть\n    if (root == null)\n        return\n    var cur = root\n    var pre: TreeNode? = null\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur != null) {\n        // Найти узел для удаления и выйти из цикла\n        if (cur._val == num)\n            break\n        pre = cur\n        // Узел для удаления находится в правом поддереве cur\n        cur = if (cur._val < num)\n            cur.right\n        // Узел для удаления находится в левом поддереве cur\n        else\n            cur.left\n    }\n    // Если узел для удаления отсутствует, сразу вернуть\n    if (cur == null)\n        return\n    // Число дочерних узлов = 0 или 1\n    if (cur.left == null || cur.right == null) {\n        // Когда число дочерних узлов = 0 / 1, child = null / этот дочерний узел\n        val child = if (cur.left != null)\n            cur.left\n        else\n            cur.right\n        // Удалить узел cur\n        if (cur != root) {\n            if (pre!!.left == cur)\n                pre.left = child\n            else\n                pre.right = child\n        } else {\n            // Если удаляемый узел является корнем, заново назначить корневой узел\n            root = child\n        }\n        // Число дочерних узлов = 2\n    } else {\n        // Получить следующий узел после cur в симметричном обходе\n        var tmp = cur.right\n        while (tmp!!.left != null) {\n            tmp = tmp.left\n        }\n        // Рекурсивно удалить узел tmp\n        remove(tmp._val)\n        // Перезаписать cur значением tmp\n        cur._val = tmp._val\n    }\n}\n
        binary_search_tree.rb
        ### Удаление узла ###\ndef remove(num)\n  # Если дерево пусто, сразу вернуть\n  return if @root.nil?\n\n  # Искать в цикле и выйти после прохода за листовой узел\n  cur, pre = @root, nil\n  while !cur.nil?\n    # Найти узел для удаления и выйти из цикла\n    break if cur.val == num\n\n    pre = cur\n    # Узел для удаления находится в правом поддереве cur\n    if cur.val < num\n      cur = cur.right\n    # Узел для удаления находится в левом поддереве cur\n    else\n      cur = cur.left\n    end\n  end\n  # Если узел для удаления отсутствует, сразу вернуть\n  return if cur.nil?\n\n  # Число дочерних узлов = 0 или 1\n  if cur.left.nil? || cur.right.nil?\n    # Когда число дочерних узлов = 0 / 1, child = null / этот дочерний узел\n    child = cur.left || cur.right\n    # Удалить узел cur\n    if cur != @root\n      if pre.left == cur\n        pre.left = child\n      else\n        pre.right = child\n      end\n    else\n      # Если удаляемый узел является корнем, заново назначить корневой узел\n      @root = child\n    end\n  # Число дочерних узлов = 2\n  else\n    # Получить следующий узел после cur в симметричном обходе\n    tmp = cur.right\n    while !tmp.left.nil?\n      tmp = tmp.left\n    end\n    # Рекурсивно удалить узел tmp\n    remove(tmp.val)\n    # Перезаписать cur значением tmp\n    cur.val = tmp.val\n  end\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 7. Деревья","7.4   Двоичное дерево поиска"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#4","level":3,"title":"4.   Упорядоченность симметричного обхода","text":"

        Как показано на рисунке 7-22, симметричный обход двоичного дерева следует порядку \"лево \\(\\rightarrow\\) корень \\(\\rightarrow\\) право\", а двоичное дерево поиска удовлетворяет соотношению \"левый дочерний узел \\(<\\) корень \\(<\\) правый дочерний узел\".

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

        Используя это свойство возрастающей последовательности симметричного обхода, мы можем получить отсортированные данные из двоичного дерева поиска всего за \\(O(n)\\) времени, без дополнительной сортировки, что очень эффективно.

        Рисунок 7-22   Последовательность симметричного обхода двоичного дерева поиска

        ","path":["Глава 7. Деревья","7.4   Двоичное дерево поиска"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#742","level":2,"title":"7.4.2   Эффективность двоичного дерева поиска","text":"

        Для заданного набора данных можно рассмотреть хранение либо в массиве, либо в двоичном дереве поиска. Из таблицы ниже видно, что временная сложность операций двоичного дерева поиска имеет логарифмический порядок и обеспечивает стабильную высокую производительность. Только в сценариях с очень частыми вставками и редкими поисками и удалениями массив может быть эффективнее, чем двоичное дерево поиска.

        Таблица 7-2   Сравнение эффективности массива и дерева поиска

        Неупорядоченный массив Двоичное дерево поиска Поиск элемента \\(O(n)\\) \\(O(\\log n)\\) Вставка элемента \\(O(1)\\) \\(O(\\log n)\\) Удаление элемента \\(O(n)\\) \\(O(\\log n)\\)

        В идеальном случае двоичное дерево поиска является \"сбалансированным\", и тогда любой узел можно найти за \\(\\log n\\) итераций.

        Однако если в двоичное дерево поиска непрерывно вставлять и удалять узлы, оно может выродиться в связный список, как показано на рисунке 7-23. Тогда временная сложность различных операций тоже вырождается до \\(O(n)\\) .

        Рисунок 7-23   Деградация двоичного дерева поиска

        ","path":["Глава 7. Деревья","7.4   Двоичное дерево поиска"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#743","level":2,"title":"7.4.3   Типичные применения двоичного дерева поиска","text":"
        • Используется как многоуровневый индекс в системах, обеспечивая эффективный поиск, вставку и удаление.
        • Служит базовой структурой данных для некоторых поисковых алгоритмов.
        • Применяется для хранения потока данных в отсортированном состоянии.
        ","path":["Глава 7. Деревья","7.4   Двоичное дерево поиска"],"tags":[]},{"location":"chapter_tree/binary_tree/","level":1,"title":"7.1   Двоичное дерево","text":"

        Двоичное дерево (binary tree) - это нелинейная структура данных, представляющая отношения между \"предками\" и \"потомками\" и отражающая логику \"разделяй и властвуй\". Подобно связному списку, базовой единицей двоичного дерева является узел; каждый узел содержит значение, ссылку на левого дочернего узла и ссылку на правого дочернего узла.

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
        class TreeNode:\n    \"\"\"Класс узла двоичного дерева\"\"\"\n    def __init__(self, val: int):\n        self.val: int = val                # Значение узла\n        self.left: TreeNode | None = None  # Ссылка на левого дочернего узла\n        self.right: TreeNode | None = None # Ссылка на правого дочернего узла\n
        /* Структура узла двоичного дерева */\nstruct TreeNode {\n    int val;          // Значение узла\n    TreeNode *left;   // Указатель на левого дочернего узла\n    TreeNode *right;  // Указатель на правого дочернего узла\n    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}\n};\n
        /* Класс узла двоичного дерева */\nclass TreeNode {\n    int val;         // Значение узла\n    TreeNode left;   // Ссылка на левого дочернего узла\n    TreeNode right;  // Ссылка на правого дочернего узла\n    TreeNode(int x) { val = x; }\n}\n
        /* Класс узла двоичного дерева */\nclass TreeNode(int? x) {\n    public int? val = x;    // Значение узла\n    public TreeNode? left;  // Ссылка на левого дочернего узла\n    public TreeNode? right; // Ссылка на правого дочернего узла\n}\n
        /* Структура узла двоичного дерева */\ntype TreeNode struct {\n    Val   int\n    Left  *TreeNode\n    Right *TreeNode\n}\n/* Конструктор */\nfunc NewTreeNode(v int) *TreeNode {\n    return &TreeNode{\n        Left:  nil, // Указатель на левого дочернего узла\n        Right: nil, // Указатель на правого дочернего узла\n        Val:   v,   // Значение узла\n    }\n}\n
        /* Класс узла двоичного дерева */\nclass TreeNode {\n    var val: Int // Значение узла\n    var left: TreeNode? // Ссылка на левого дочернего узла\n    var right: TreeNode? // Ссылка на правого дочернего узла\n\n    init(x: Int) {\n        val = x\n    }\n}\n
        /* Класс узла двоичного дерева */\nclass TreeNode {\n    val; // Значение узла\n    left; // Указатель на левого дочернего узла\n    right; // Указатель на правого дочернего узла\n    constructor(val, left, right) {\n        this.val = val === undefined ? 0 : val;\n        this.left = left === undefined ? null : left;\n        this.right = right === undefined ? null : right;\n    }\n}\n
        /* Класс узла двоичного дерева */\nclass TreeNode {\n    val: number;\n    left: TreeNode | null;\n    right: TreeNode | null;\n\n    constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {\n        this.val = val === undefined ? 0 : val; // Значение узла\n        this.left = left === undefined ? null : left; // Ссылка на левого дочернего узла\n        this.right = right === undefined ? null : right; // Ссылка на правого дочернего узла\n    }\n}\n
        /* Класс узла двоичного дерева */\nclass TreeNode {\n  int val;         // Значение узла\n  TreeNode? left;  // Ссылка на левого дочернего узла\n  TreeNode? right; // Ссылка на правого дочернего узла\n  TreeNode(this.val, [this.left, this.right]);\n}\n
        use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* Структура узла двоичного дерева */\nstruct TreeNode {\n    val: i32,                               // Значение узла\n    left: Option<Rc<RefCell<TreeNode>>>,    // Ссылка на левого дочернего узла\n    right: Option<Rc<RefCell<TreeNode>>>,   // Ссылка на правого дочернего узла\n}\n\nimpl TreeNode {\n    /* Конструктор */\n    fn new(val: i32) -> Rc<RefCell<Self>> {\n        Rc::new(RefCell::new(Self {\n            val,\n            left: None,\n            right: None\n        }))\n    }\n}\n
        /* Структура узла двоичного дерева */\ntypedef struct TreeNode {\n    int val;                // Значение узла\n    int height;             // Высота узла\n    struct TreeNode *left;  // Указатель на левого дочернего узла\n    struct TreeNode *right; // Указатель на правого дочернего узла\n} TreeNode;\n\n/* Конструктор */\nTreeNode *newTreeNode(int val) {\n    TreeNode *node;\n\n    node = (TreeNode *)malloc(sizeof(TreeNode));\n    node->val = val;\n    node->height = 0;\n    node->left = NULL;\n    node->right = NULL;\n    return node;\n}\n
        /* Класс узла двоичного дерева */\nclass TreeNode(val _val: Int) {  // Значение узла\n    val left: TreeNode? = null   // Ссылка на левого дочернего узла\n    val right: TreeNode? = null  // Ссылка на правого дочернего узла\n}\n
        ### Класс узла двоичного дерева ###\nclass TreeNode\n  attr_accessor :val    # Значение узла\n  attr_accessor :left   # Ссылка на левого дочернего узла\n  attr_accessor :right  # Ссылка на правого дочернего узла\n\n  def initialize(val)\n    @val = val\n  end\nend\n

        Каждый узел имеет две ссылки (указателя), которые соответственно указывают на левого дочернего узла (left-child node) и правого дочернего узла (right-child node); данный узел называется родительским узлом (parent node) для этих двух дочерних узлов. Если задан некоторый узел двоичного дерева, то дерево, образованное его левым дочерним узлом и всеми узлами ниже него, называется левым поддеревом (left subtree) этого узла; аналогично определяется правое поддерево (right subtree).

        Узлы, не имеющие дочерних узлов, называют листьями, а все остальные узлы содержат дочерние узлы и непустые поддеревья. Как показано на рисунке 7-1, если рассматривать \"узел 2\" как родительский, то его левым и правым дочерними узлами будут \"узел 4\" и \"узел 5\"; левое поддерево - это \"узел 4 и дерево ниже него\", а правое поддерево - это \"узел 5 и дерево ниже него\".

        Рисунок 7-1   Родительский узел, дочерние узлы и поддеревья

        ","path":["Глава 7. Деревья","7.1   Двоичное дерево"],"tags":[]},{"location":"chapter_tree/binary_tree/#711","level":2,"title":"7.1.1   Распространенные термины двоичного дерева","text":"

        Распространенные термины двоичного дерева показаны на рисунке 7-2.

        • Корневой узел (root node): узел, расположенный на верхнем уровне двоичного дерева и не имеющий родительского узла.
        • Листовой узел (leaf node): узел без дочерних узлов; оба его указателя направлены на None .
        • Ребро (edge): отрезок, соединяющий два узла, то есть ссылка (указатель) между узлами.
        • Уровень (level) узла: увеличивается сверху вниз; уровень корневого узла равен 1 .
        • Степень (degree) узла: число дочерних узлов данного узла. В двоичном дереве возможны степени 0, 1, 2 .
        • Высота (height) двоичного дерева: число ребер от корневого узла до самого удаленного листового узла.
        • Глубина (depth) узла: число ребер от корневого узла до данного узла.
        • Высота (height) узла: число ребер от самого удаленного листового узла до данного узла.

        Рисунок 7-2   Распространенные термины двоичного дерева

        Tip

        Обычно под \"высотой\" и \"глубиной\" понимают \"число пройденных ребер\", но в некоторых задачах или учебниках их могут определять как \"число пройденных узлов\". В таком случае и высоту, и глубину нужно увеличить на 1 .

        ","path":["Глава 7. Деревья","7.1   Двоичное дерево"],"tags":[]},{"location":"chapter_tree/binary_tree/#712","level":2,"title":"7.1.2   Базовые операции двоичного дерева","text":"","path":["Глава 7. Деревья","7.1   Двоичное дерево"],"tags":[]},{"location":"chapter_tree/binary_tree/#1","level":3,"title":"1.   Инициализация двоичного дерева","text":"

        Как и в связном списке, сначала инициализируются узлы, а затем между ними строятся ссылки (указатели).

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree.py
        # Инициализация двоичного дерева\n# Инициализация узлов\nn1 = TreeNode(val=1)\nn2 = TreeNode(val=2)\nn3 = TreeNode(val=3)\nn4 = TreeNode(val=4)\nn5 = TreeNode(val=5)\n# Построение ссылок (указателей) между узлами\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n
        binary_tree.cpp
        /* Инициализация двоичного дерева */\n// Инициализация узлов\nTreeNode* n1 = new TreeNode(1);\nTreeNode* n2 = new TreeNode(2);\nTreeNode* n3 = new TreeNode(3);\nTreeNode* n4 = new TreeNode(4);\nTreeNode* n5 = new TreeNode(5);\n// Построение ссылок (указателей) между узлами\nn1->left = n2;\nn1->right = n3;\nn2->left = n4;\nn2->right = n5;\n
        binary_tree.java
        // Инициализация узлов\nTreeNode n1 = new TreeNode(1);\nTreeNode n2 = new TreeNode(2);\nTreeNode n3 = new TreeNode(3);\nTreeNode n4 = new TreeNode(4);\nTreeNode n5 = new TreeNode(5);\n// Построение ссылок (указателей) между узлами\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
        binary_tree.cs
        /* Инициализация двоичного дерева */\n// Инициализация узлов\nTreeNode n1 = new(1);\nTreeNode n2 = new(2);\nTreeNode n3 = new(3);\nTreeNode n4 = new(4);\nTreeNode n5 = new(5);\n// Построение ссылок (указателей) между узлами\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
        binary_tree.go
        /* Инициализация двоичного дерева */\n// Инициализация узлов\nn1 := NewTreeNode(1)\nn2 := NewTreeNode(2)\nn3 := NewTreeNode(3)\nn4 := NewTreeNode(4)\nn5 := NewTreeNode(5)\n// Построение ссылок (указателей) между узлами\nn1.Left = n2\nn1.Right = n3\nn2.Left = n4\nn2.Right = n5\n
        binary_tree.swift
        // Инициализация узлов\nlet n1 = TreeNode(x: 1)\nlet n2 = TreeNode(x: 2)\nlet n3 = TreeNode(x: 3)\nlet n4 = TreeNode(x: 4)\nlet n5 = TreeNode(x: 5)\n// Построение ссылок (указателей) между узлами\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n
        binary_tree.js
        /* Инициализация двоичного дерева */\n// Инициализация узлов\nlet n1 = new TreeNode(1),\n    n2 = new TreeNode(2),\n    n3 = new TreeNode(3),\n    n4 = new TreeNode(4),\n    n5 = new TreeNode(5);\n// Построение ссылок (указателей) между узлами\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
        binary_tree.ts
        /* Инициализация двоичного дерева */\n// Инициализация узлов\nlet n1 = new TreeNode(1),\n    n2 = new TreeNode(2),\n    n3 = new TreeNode(3),\n    n4 = new TreeNode(4),\n    n5 = new TreeNode(5);\n// Построение ссылок (указателей) между узлами\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
        binary_tree.dart
        /* Инициализация двоичного дерева */\n// Инициализация узлов\nTreeNode n1 = new TreeNode(1);\nTreeNode n2 = new TreeNode(2);\nTreeNode n3 = new TreeNode(3);\nTreeNode n4 = new TreeNode(4);\nTreeNode n5 = new TreeNode(5);\n// Построение ссылок (указателей) между узлами\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
        binary_tree.rs
        // Инициализация узлов\nlet n1 = TreeNode::new(1);\nlet n2 = TreeNode::new(2);\nlet n3 = TreeNode::new(3);\nlet n4 = TreeNode::new(4);\nlet n5 = TreeNode::new(5);\n// Построение ссылок (указателей) между узлами\nn1.borrow_mut().left = Some(n2.clone());\nn1.borrow_mut().right = Some(n3);\nn2.borrow_mut().left = Some(n4);\nn2.borrow_mut().right = Some(n5);\n
        binary_tree.c
        /* Инициализация двоичного дерева */\n// Инициализация узлов\nTreeNode *n1 = newTreeNode(1);\nTreeNode *n2 = newTreeNode(2);\nTreeNode *n3 = newTreeNode(3);\nTreeNode *n4 = newTreeNode(4);\nTreeNode *n5 = newTreeNode(5);\n// Построение ссылок (указателей) между узлами\nn1->left = n2;\nn1->right = n3;\nn2->left = n4;\nn2->right = n5;\n
        binary_tree.kt
        // Инициализация узлов\nval n1 = TreeNode(1)\nval n2 = TreeNode(2)\nval n3 = TreeNode(3)\nval n4 = TreeNode(4)\nval n5 = TreeNode(5)\n// Построение ссылок (указателей) между узлами\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n
        binary_tree.rb
        # Инициализация двоичного дерева\n# Инициализация узлов\nn1 = TreeNode.new(1)\nn2 = TreeNode.new(2)\nn3 = TreeNode.new(3)\nn4 = TreeNode.new(4)\nn5 = TreeNode.new(5)\n# Построение ссылок (указателей) между узлами\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n
        Визуализация выполнения

        https://pythontutor.com/render.html#code=class%20TreeNode%3A%0A%20%20%20%20%22%22%22%D0%9A%D0%BB%D0%B0%D1%81%D1%81%20%D1%83%D0%B7%D0%BB%D0%B0%20%D0%B4%D0%B2%D0%BE%D0%B8%D1%87%D0%BD%D0%BE%D0%B3%D0%BE%20%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%B0%22%22%22%0A%20%20%20%20def%20__init__%28self%2C%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%D0%97%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%D1%83%D0%B7%D0%BB%D0%B0%0A%20%20%20%20%20%20%20%20self.left%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%D0%A1%D1%81%D1%8B%D0%BB%D0%BA%D0%B0%20%D0%BD%D0%B0%20%D0%BB%D0%B5%D0%B2%D1%8B%D0%B9%20%D0%B4%D0%BE%D1%87%D0%B5%D1%80%D0%BD%D0%B8%D0%B9%20%D1%83%D0%B7%D0%B5%D0%BB%0A%20%20%20%20%20%20%20%20self.right%3A%20TreeNode%20%7C%20None%20%3D%20None%20%23%20%D0%A1%D1%81%D1%8B%D0%BB%D0%BA%D0%B0%20%D0%BD%D0%B0%20%D0%BF%D1%80%D0%B0%D0%B2%D1%8B%D0%B9%20%D0%B4%D0%BE%D1%87%D0%B5%D1%80%D0%BD%D0%B8%D0%B9%20%D1%83%D0%B7%D0%B5%D0%BB%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D0%B4%D0%B2%D0%BE%D0%B8%D1%87%D0%BD%D0%BE%D0%B5%20%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%BE%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%83%D0%B7%D0%B5%D0%BB%0A%20%20%20%20n1%20%3D%20TreeNode%28val%3D1%29%0A%20%20%20%20n2%20%3D%20TreeNode%28val%3D2%29%0A%20%20%20%20n3%20%3D%20TreeNode%28val%3D3%29%0A%20%20%20%20n4%20%3D%20TreeNode%28val%3D4%29%0A%20%20%20%20n5%20%3D%20TreeNode%28val%3D5%29%0A%20%20%20%20%23%20%D0%9F%D0%BE%D1%81%D1%82%D1%80%D0%BE%D0%B8%D1%82%D1%8C%20%D1%81%D1%81%D1%8B%D0%BB%D0%BA%D0%B8%20%D0%BC%D0%B5%D0%B6%D0%B4%D1%83%20%D1%83%D0%B7%D0%BB%D0%B0%D0%BC%D0%B8%20%28%D1%83%D0%BA%D0%B0%D0%B7%D0%B0%D1%82%D0%B5%D0%BB%D0%B8%29%0A%20%20%20%20n1.left%20%3D%20n2%0A%20%20%20%20n1.right%20%3D%20n3%0A%20%20%20%20n2.left%20%3D%20n4%0A%20%20%20%20n2.right%20%3D%20n5&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

        ","path":["Глава 7. Деревья","7.1   Двоичное дерево"],"tags":[]},{"location":"chapter_tree/binary_tree/#2","level":3,"title":"2.   Вставка и удаление узлов","text":"

        Как и в связном списке, вставка и удаление узлов в двоичном дереве могут выполняться через изменение указателей. На рисунке 7-3 приведен пример.

        Рисунок 7-3   Вставка и удаление узлов в двоичном дереве

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree.py
        # Вставка и удаление узлов\np = TreeNode(0)\n# Вставить узел P между n1 -> n2\nn1.left = p\np.left = n2\n# Удалить узел P\nn1.left = n2\n
        binary_tree.cpp
        /* Вставка и удаление узлов */\nTreeNode* P = new TreeNode(0);\n// Вставить узел P между n1 -> n2\nn1->left = P;\nP->left = n2;\n// Удалить узел P\nn1->left = n2;\n// Освободить память\ndelete P;\n
        binary_tree.java
        TreeNode P = new TreeNode(0);\n// Вставить узел P между n1 -> n2\nn1.left = P;\nP.left = n2;\n// Удалить узел P\nn1.left = n2;\n
        binary_tree.cs
        /* Вставка и удаление узлов */\nTreeNode P = new(0);\n// Вставить узел P между n1 -> n2\nn1.left = P;\nP.left = n2;\n// Удалить узел P\nn1.left = n2;\n
        binary_tree.go
        /* Вставка и удаление узлов */\n// Вставить узел P между n1 -> n2\np := NewTreeNode(0)\nn1.Left = p\np.Left = n2\n// Удалить узел P\nn1.Left = n2\n
        binary_tree.swift
        let P = TreeNode(x: 0)\n// Вставить узел P между n1 -> n2\nn1.left = P\nP.left = n2\n// Удалить узел P\nn1.left = n2\n
        binary_tree.js
        /* Вставка и удаление узлов */\nlet P = new TreeNode(0);\n// Вставить узел P между n1 -> n2\nn1.left = P;\nP.left = n2;\n// Удалить узел P\nn1.left = n2;\n
        binary_tree.ts
        /* Вставка и удаление узлов */\nconst P = new TreeNode(0);\n// Вставить узел P между n1 -> n2\nn1.left = P;\nP.left = n2;\n// Удалить узел P\nn1.left = n2;\n
        binary_tree.dart
        /* Вставка и удаление узлов */\nTreeNode P = new TreeNode(0);\n// Вставить узел P между n1 -> n2\nn1.left = P;\nP.left = n2;\n// Удалить узел P\nn1.left = n2;\n
        binary_tree.rs
        let p = TreeNode::new(0);\n// Вставить узел P между n1 -> n2\nn1.borrow_mut().left = Some(p.clone());\np.borrow_mut().left = Some(n2.clone());\n// Удалить узел p\nn1.borrow_mut().left = Some(n2);\n
        binary_tree.c
        /* Вставка и удаление узлов */\nTreeNode *P = newTreeNode(0);\n// Вставить узел P между n1 -> n2\nn1->left = P;\nP->left = n2;\n// Удалить узел P\nn1->left = n2;\n// Освободить память\nfree(P);\n
        binary_tree.kt
        val P = TreeNode(0)\n// Вставить узел P между n1 -> n2\nn1.left = P\nP.left = n2\n// Удалить узел P\nn1.left = n2\n
        binary_tree.rb
        # Вставка и удаление узлов\n_p = TreeNode.new(0)\n# Вставить узел _p между n1 -> n2\nn1.left = _p\n_p.left = n2\n# Удалить узел\nn1.left = n2\n
        Визуализация выполнения

        https://pythontutor.com/render.html#code=class%20TreeNode%3A%0A%20%20%20%20%22%22%22%D0%9A%D0%BB%D0%B0%D1%81%D1%81%20%D1%83%D0%B7%D0%BB%D0%B0%20%D0%B4%D0%B2%D0%BE%D0%B8%D1%87%D0%BD%D0%BE%D0%B3%D0%BE%20%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%B0%22%22%22%0A%20%20%20%20def%20__init__%28self%2C%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%D0%97%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%D1%83%D0%B7%D0%BB%D0%B0%0A%20%20%20%20%20%20%20%20self.left%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%D0%A1%D1%81%D1%8B%D0%BB%D0%BA%D0%B0%20%D0%BD%D0%B0%20%D0%BB%D0%B5%D0%B2%D1%8B%D0%B9%20%D0%B4%D0%BE%D1%87%D0%B5%D1%80%D0%BD%D0%B8%D0%B9%20%D1%83%D0%B7%D0%B5%D0%BB%0A%20%20%20%20%20%20%20%20self.right%3A%20TreeNode%20%7C%20None%20%3D%20None%20%23%20%D0%A1%D1%81%D1%8B%D0%BB%D0%BA%D0%B0%20%D0%BD%D0%B0%20%D0%BF%D1%80%D0%B0%D0%B2%D1%8B%D0%B9%20%D0%B4%D0%BE%D1%87%D0%B5%D1%80%D0%BD%D0%B8%D0%B9%20%D1%83%D0%B7%D0%B5%D0%BB%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D0%B4%D0%B2%D0%BE%D0%B8%D1%87%D0%BD%D0%BE%D0%B5%20%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%BE%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%83%D0%B7%D0%B5%D0%BB%0A%20%20%20%20n1%20%3D%20TreeNode%28val%3D1%29%0A%20%20%20%20n2%20%3D%20TreeNode%28val%3D2%29%0A%20%20%20%20n3%20%3D%20TreeNode%28val%3D3%29%0A%20%20%20%20n4%20%3D%20TreeNode%28val%3D4%29%0A%20%20%20%20n5%20%3D%20TreeNode%28val%3D5%29%0A%20%20%20%20%23%20%D0%9F%D0%BE%D1%81%D1%82%D1%80%D0%BE%D0%B8%D1%82%D1%8C%20%D1%81%D1%81%D1%8B%D0%BB%D0%BA%D0%B8%20%D0%BC%D0%B5%D0%B6%D0%B4%D1%83%20%D1%83%D0%B7%D0%BB%D0%B0%D0%BC%D0%B8%20%28%D1%83%D0%BA%D0%B0%D0%B7%D0%B0%D1%82%D0%B5%D0%BB%D0%B8%29%0A%20%20%20%20n1.left%20%3D%20n2%0A%20%20%20%20n1.right%20%3D%20n3%0A%20%20%20%20n2.left%20%3D%20n4%0A%20%20%20%20n2.right%20%3D%20n5%0A%0A%20%20%20%20%23%20%D0%92%D1%81%D1%82%D0%B0%D0%B2%D0%BA%D0%B0%20%D0%B8%20%D1%83%D0%B4%D0%B0%D0%BB%D0%B5%D0%BD%D0%B8%D0%B5%20%D1%83%D0%B7%D0%BB%D0%BE%D0%B2%0A%20%20%20%20p%20%3D%20TreeNode%280%29%0A%20%20%20%20%23%20%D0%92%D1%81%D1%82%D0%B0%D0%B2%D0%B8%D1%82%D1%8C%20%D1%83%D0%B7%D0%B5%D0%BB%20P%20%D0%BC%D0%B5%D0%B6%D0%B4%D1%83%20n1%20-%3E%20n2%0A%20%20%20%20n1.left%20%3D%20p%0A%20%20%20%20p.left%20%3D%20n2%0A%20%20%20%20%23%20%D0%A3%D0%B4%D0%B0%D0%BB%D0%B8%D1%82%D1%8C%20%D1%83%D0%B7%D0%B5%D0%BB%20P%0A%20%20%20%20n1.left%20%3D%20n2&cumulative=false&curInstr=37&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

        Tip

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

        ","path":["Глава 7. Деревья","7.1   Двоичное дерево"],"tags":[]},{"location":"chapter_tree/binary_tree/#713","level":2,"title":"7.1.3   Распространенные типы двоичных деревьев","text":"","path":["Глава 7. Деревья","7.1   Двоичное дерево"],"tags":[]},{"location":"chapter_tree/binary_tree/#1_1","level":3,"title":"1.   Идеальное двоичное дерево","text":"

        Как показано на рисунке 7-4, идеальное двоичное дерево (perfect binary tree) полностью заполнено на всех уровнях. В идеальном двоичном дереве степень листовых узлов равна \\(0\\) , а у всех остальных узлов степень равна \\(2\\) ; если высота дерева равна \\(h\\) , то общее число узлов равно \\(2^{h+1} - 1\\) , что образует стандартную экспоненциальную зависимость и отражает часто встречающееся в природе явление клеточного деления.

        Tip

        В китайскоязычном сообществе идеальное двоичное дерево часто называют полностью заполненным двоичным деревом.

        Рисунок 7-4   Идеальное двоичное дерево

        ","path":["Глава 7. Деревья","7.1   Двоичное дерево"],"tags":[]},{"location":"chapter_tree/binary_tree/#2_1","level":3,"title":"2.   Полное двоичное дерево","text":"

        Как показано на рисунке 7-5, полное двоичное дерево (complete binary tree) допускает неполное заполнение только на самом нижнем уровне, причем узлы этого уровня должны непрерывно заполняться слева направо. Стоит отметить, что идеальное двоичное дерево тоже является полным двоичным деревом.

        Рисунок 7-5   Полное двоичное дерево

        ","path":["Глава 7. Деревья","7.1   Двоичное дерево"],"tags":[]},{"location":"chapter_tree/binary_tree/#3","level":3,"title":"3.   Строгое двоичное дерево","text":"

        Как показано на рисунке 7-6, строгое двоичное дерево (full binary tree) имеет у всех нелистовых узлов ровно двух дочерних узлов.

        Рисунок 7-6   Строгое двоичное дерево

        ","path":["Глава 7. Деревья","7.1   Двоичное дерево"],"tags":[]},{"location":"chapter_tree/binary_tree/#4","level":3,"title":"4.   Сбалансированное двоичное дерево","text":"

        Как показано на рисунке 7-7, в сбалансированном двоичном дереве (balanced binary tree) для любого узла абсолютное значение разности высот левого и правого поддеревьев не превышает 1 .

        Рисунок 7-7   Сбалансированное двоичное дерево

        ","path":["Глава 7. Деревья","7.1   Двоичное дерево"],"tags":[]},{"location":"chapter_tree/binary_tree/#714","level":2,"title":"7.1.4   Вырождение двоичного дерева","text":"

        На рисунке 7-8 показаны идеальная структура двоичного дерева и вырожденная структура. Когда каждый уровень двоичного дерева полностью заполнен узлами, мы получаем \"идеальное двоичное дерево\"; когда же все узлы смещаются к одной стороне, двоичное дерево вырождается в \"связный список\".

        • Идеальное двоичное дерево соответствует лучшему случаю и позволяет в полной мере раскрыть преимущества подхода \"разделяй и властвуй\".
        • Связный список представляет противоположную крайность: все операции становятся линейными, а временная сложность деградирует до \\(O(n)\\) .

        Рисунок 7-8   Лучший и худший случаи структуры двоичного дерева

        Как показано в таблице 7-1, в лучшем и худшем случаях число листовых узлов, общее число узлов, высота и другие характеристики двоичного дерева достигают максимума или минимума.

        Таблица 7-1   Лучший и худший случаи структуры двоичного дерева

        Идеальное двоичное дерево Связный список Число узлов на уровне \\(i\\) \\(2^{i-1}\\) \\(1\\) Число листьев у дерева высоты \\(h\\) \\(2^h\\) \\(1\\) Общее число узлов у дерева высоты \\(h\\) \\(2^{h+1} - 1\\) \\(h + 1\\) Высота дерева с \\(n\\) узлами \\(\\log_2 (n+1) - 1\\) \\(n - 1\\)","path":["Глава 7. Деревья","7.1   Двоичное дерево"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/","level":1,"title":"7.2   Обход двоичного дерева","text":"

        С точки зрения физической структуры дерево представляет собой разновидность структуры данных на основе связей, поэтому его обход выполняется через последовательный доступ к узлам по указателям. Однако дерево является нелинейной структурой данных, а значит, его обход сложнее, чем обход связного списка, и для него требуется использовать поисковые алгоритмы.

        К распространенным способам обхода двоичного дерева относятся обход по уровням, прямой обход, симметричный обход и обратный обход.

        ","path":["Глава 7. Деревья","7.2   Обход двоичного дерева"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#721","level":2,"title":"7.2.1   Обход по уровням","text":"

        Как показано на рисунке 7-9, обход по уровням (level-order traversal) проходит двоичное дерево сверху вниз по уровням и на каждом уровне посещает узлы слева направо.

        По своей сути обход по уровням относится к обходу в ширину (breadth-first traversal), также называемому поиском в ширину (breadth-first search, BFS); он отражает идею \"расширяться от центра к периферии слой за слоем\".

        Рисунок 7-9   Обход двоичного дерева по уровням

        ","path":["Глава 7. Деревья","7.2   Обход двоичного дерева"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#1","level":3,"title":"1.   Код реализации","text":"

        Обход в ширину обычно реализуется с помощью \"очереди\". Очередь подчиняется правилу \"первым пришел - первым вышел\", а обход в ширину подчиняется правилу \"продвигаться по уровням\", поэтому стоящая за ними идея согласована. Код реализации приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree_bfs.py
        def level_order(root: TreeNode | None) -> list[int]:\n    \"\"\"Обход в ширину\"\"\"\n    # Инициализировать очередь и добавить корневой узел\n    queue: deque[TreeNode] = deque()\n    queue.append(root)\n    # Инициализировать список для хранения последовательности обхода\n    res = []\n    while queue:\n        node: TreeNode = queue.popleft()  # Извлечение из очереди\n        res.append(node.val)  # Сохранить значение узла\n        if node.left is not None:\n            queue.append(node.left)  # Поместить левый дочерний узел в очередь\n        if node.right is not None:\n            queue.append(node.right)  # Поместить правый дочерний узел в очередь\n    return res\n
        binary_tree_bfs.cpp
        /* Обход в ширину */\nvector<int> levelOrder(TreeNode *root) {\n    // Инициализировать очередь и добавить корневой узел\n    queue<TreeNode *> queue;\n    queue.push(root);\n    // Инициализировать список для хранения последовательности обхода\n    vector<int> vec;\n    while (!queue.empty()) {\n        TreeNode *node = queue.front();\n        queue.pop();              // Извлечение из очереди\n        vec.push_back(node->val); // Сохранить значение узла\n        if (node->left != nullptr)\n            queue.push(node->left); // Поместить левый дочерний узел в очередь\n        if (node->right != nullptr)\n            queue.push(node->right); // Поместить правый дочерний узел в очередь\n    }\n    return vec;\n}\n
        binary_tree_bfs.java
        /* Обход в ширину */\nList<Integer> levelOrder(TreeNode root) {\n    // Инициализировать очередь и добавить корневой узел\n    Queue<TreeNode> queue = new LinkedList<>();\n    queue.add(root);\n    // Инициализировать список для хранения последовательности обхода\n    List<Integer> list = new ArrayList<>();\n    while (!queue.isEmpty()) {\n        TreeNode node = queue.poll(); // Извлечение из очереди\n        list.add(node.val);           // Сохранить значение узла\n        if (node.left != null)\n            queue.offer(node.left);   // Поместить левый дочерний узел в очередь\n        if (node.right != null)\n            queue.offer(node.right);  // Поместить правый дочерний узел в очередь\n    }\n    return list;\n}\n
        binary_tree_bfs.cs
        /* Обход в ширину */\nList<int> LevelOrder(TreeNode root) {\n    // Инициализировать очередь и добавить корневой узел\n    Queue<TreeNode> queue = new();\n    queue.Enqueue(root);\n    // Инициализировать список для хранения последовательности обхода\n    List<int> list = [];\n    while (queue.Count != 0) {\n        TreeNode node = queue.Dequeue(); // Извлечение из очереди\n        list.Add(node.val!.Value);       // Сохранить значение узла\n        if (node.left != null)\n            queue.Enqueue(node.left);    // Поместить левый дочерний узел в очередь\n        if (node.right != null)\n            queue.Enqueue(node.right);   // Поместить правый дочерний узел в очередь\n    }\n    return list;\n}\n
        binary_tree_bfs.go
        /* Обход в ширину */\nfunc levelOrder(root *TreeNode) []any {\n    // Инициализировать очередь и добавить корневой узел\n    queue := list.New()\n    queue.PushBack(root)\n    // Инициализировать срез для хранения последовательности обхода\n    nums := make([]any, 0)\n    for queue.Len() > 0 {\n        // Извлечение из очереди\n        node := queue.Remove(queue.Front()).(*TreeNode)\n        // Сохранить значение узла\n        nums = append(nums, node.Val)\n        if node.Left != nil {\n            // Поместить левый дочерний узел в очередь\n            queue.PushBack(node.Left)\n        }\n        if node.Right != nil {\n            // Поместить правый дочерний узел в очередь\n            queue.PushBack(node.Right)\n        }\n    }\n    return nums\n}\n
        binary_tree_bfs.swift
        /* Обход в ширину */\nfunc levelOrder(root: TreeNode) -> [Int] {\n    // Инициализировать очередь и добавить корневой узел\n    var queue: [TreeNode] = [root]\n    // Инициализировать список для хранения последовательности обхода\n    var list: [Int] = []\n    while !queue.isEmpty {\n        let node = queue.removeFirst() // Извлечение из очереди\n        list.append(node.val) // Сохранить значение узла\n        if let left = node.left {\n            queue.append(left) // Поместить левый дочерний узел в очередь\n        }\n        if let right = node.right {\n            queue.append(right) // Поместить правый дочерний узел в очередь\n        }\n    }\n    return list\n}\n
        binary_tree_bfs.js
        /* Обход в ширину */\nfunction levelOrder(root) {\n    // Инициализировать очередь и добавить корневой узел\n    const queue = [root];\n    // Инициализировать список для хранения последовательности обхода\n    const list = [];\n    while (queue.length) {\n        let node = queue.shift(); // Извлечение из очереди\n        list.push(node.val); // Сохранить значение узла\n        if (node.left) queue.push(node.left); // Поместить левый дочерний узел в очередь\n        if (node.right) queue.push(node.right); // Поместить правый дочерний узел в очередь\n    }\n    return list;\n}\n
        binary_tree_bfs.ts
        /* Обход в ширину */\nfunction levelOrder(root: TreeNode | null): number[] {\n    // Инициализировать очередь и добавить корневой узел\n    const queue = [root];\n    // Инициализировать список для хранения последовательности обхода\n    const list: number[] = [];\n    while (queue.length) {\n        let node = queue.shift() as TreeNode; // Извлечение из очереди\n        list.push(node.val); // Сохранить значение узла\n        if (node.left) {\n            queue.push(node.left); // Поместить левый дочерний узел в очередь\n        }\n        if (node.right) {\n            queue.push(node.right); // Поместить правый дочерний узел в очередь\n        }\n    }\n    return list;\n}\n
        binary_tree_bfs.dart
        /* Обход в ширину */\nList<int> levelOrder(TreeNode? root) {\n  // Инициализировать очередь и добавить корневой узел\n  Queue<TreeNode?> queue = Queue();\n  queue.add(root);\n  // Инициализировать список для хранения последовательности обхода\n  List<int> res = [];\n  while (queue.isNotEmpty) {\n    TreeNode? node = queue.removeFirst(); // Извлечение из очереди\n    res.add(node!.val); // Сохранить значение узла\n    if (node.left != null) queue.add(node.left); // Поместить левый дочерний узел в очередь\n    if (node.right != null) queue.add(node.right); // Поместить правый дочерний узел в очередь\n  }\n  return res;\n}\n
        binary_tree_bfs.rs
        /* Обход в ширину */\nfn level_order(root: &Rc<RefCell<TreeNode>>) -> Vec<i32> {\n    // Инициализировать очередь и добавить корневой узел\n    let mut que = VecDeque::new();\n    que.push_back(root.clone());\n    // Инициализировать список для хранения последовательности обхода\n    let mut vec = Vec::new();\n\n    while let Some(node) = que.pop_front() {\n        // Извлечение из очереди\n        vec.push(node.borrow().val); // Сохранить значение узла\n        if let Some(left) = node.borrow().left.as_ref() {\n            que.push_back(left.clone()); // Поместить левый дочерний узел в очередь\n        }\n        if let Some(right) = node.borrow().right.as_ref() {\n            que.push_back(right.clone()); // Поместить правый дочерний узел в очередь\n        };\n    }\n    vec\n}\n
        binary_tree_bfs.c
        /* Обход в ширину */\nint *levelOrder(TreeNode *root, int *size) {\n    /* Вспомогательная очередь */\n    int front, rear;\n    int index, *arr;\n    TreeNode *node;\n    TreeNode **queue;\n\n    /* Вспомогательная очередь */\n    queue = (TreeNode **)malloc(sizeof(TreeNode *) * MAX_SIZE);\n    // Указатель очереди\n    front = 0, rear = 0;\n    // Добавить корневой узел\n    queue[rear++] = root;\n    // Инициализировать список для хранения последовательности обхода\n    /* Вспомогательный массив */\n    arr = (int *)malloc(sizeof(int) * MAX_SIZE);\n    // Указатель на массив\n    index = 0;\n    while (front < rear) {\n        // Извлечение из очереди\n        node = queue[front++];\n        // Сохранить значение узла\n        arr[index++] = node->val;\n        if (node->left != NULL) {\n            // Поместить левый дочерний узел в очередь\n            queue[rear++] = node->left;\n        }\n        if (node->right != NULL) {\n            // Поместить правый дочерний узел в очередь\n            queue[rear++] = node->right;\n        }\n    }\n    // Обновить значение длины массива\n    *size = index;\n    arr = realloc(arr, sizeof(int) * (*size));\n\n    // Освободить память вспомогательного массива\n    free(queue);\n    return arr;\n}\n
        binary_tree_bfs.kt
        /* Обход в ширину */\nfun levelOrder(root: TreeNode?): MutableList<Int> {\n    // Инициализировать очередь и добавить корневой узел\n    val queue = LinkedList<TreeNode?>()\n    queue.add(root)\n    // Инициализировать список для хранения последовательности обхода\n    val list = mutableListOf<Int>()\n    while (queue.isNotEmpty()) {\n        val node = queue.poll()      // Извлечение из очереди\n        list.add(node?._val!!)       // Сохранить значение узла\n        if (node.left != null)\n            queue.offer(node.left)   // Поместить левый дочерний узел в очередь\n        if (node.right != null)\n            queue.offer(node.right)  // Поместить правый дочерний узел в очередь\n    }\n    return list\n}\n
        binary_tree_bfs.rb
        ### Обход в ширину ###\ndef level_order(root)\n  # Инициализировать очередь и добавить корневой узел\n  queue = [root]\n  # Инициализировать список для хранения последовательности обхода\n  res = []\n  while !queue.empty?\n    node = queue.shift # Извлечение из очереди\n    res << node.val # Сохранить значение узла\n    queue << node.left unless node.left.nil? # Поместить левый дочерний узел в очередь\n    queue << node.right unless node.right.nil? # Поместить правый дочерний узел в очередь\n  end\n  res\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 7. Деревья","7.2   Обход двоичного дерева"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#2","level":3,"title":"2.   Анализ сложности","text":"
        • Временная сложность равна \\(O(n)\\) : все узлы посещаются по одному разу, поэтому требуется \\(O(n)\\) времени, где \\(n\\) - число узлов.
        • Пространственная сложность равна \\(O(n)\\) : в худшем случае, то есть для полной двоичной деревообразной структуры, до достижения самого нижнего уровня в очереди одновременно может находиться до \\((n + 1) / 2\\) узлов, что требует \\(O(n)\\) памяти.
        ","path":["Глава 7. Деревья","7.2   Обход двоичного дерева"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#722","level":2,"title":"7.2.2   Прямой, симметричный и обратный обходы","text":"

        Соответственно, прямой, симметричный и обратный обходы относятся к обходу в глубину (depth-first traversal), также называемому поиском в глубину (depth-first search, DFS); он отражает идею \"сначала идти до конца, затем возвращаться и продолжать\".

        На рисунке 7-10 показан принцип работы обхода двоичного дерева в глубину. Обход в глубину можно представить как обход всей двоичной структуры по внешнему контуру , и у каждого узла встречаются три позиции, соответствующие прямому, симметричному и обратному обходам.

        Рисунок 7-10   Прямой, симметричный и обратный обходы двоичного дерева поиска

        ","path":["Глава 7. Деревья","7.2   Обход двоичного дерева"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#1_1","level":3,"title":"1.   Код реализации","text":"

        Поиск в глубину обычно реализуется через рекурсию:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree_dfs.py
        def pre_order(root: TreeNode | None):\n    \"\"\"Предварительный обход\"\"\"\n    if root is None:\n        return\n    # Порядок обхода: корень -> левое поддерево -> правое поддерево\n    res.append(root.val)\n    pre_order(root=root.left)\n    pre_order(root=root.right)\n\ndef in_order(root: TreeNode | None):\n    \"\"\"Симметричный обход\"\"\"\n    if root is None:\n        return\n    # Порядок обхода: левое поддерево -> корень -> правое поддерево\n    in_order(root=root.left)\n    res.append(root.val)\n    in_order(root=root.right)\n\ndef post_order(root: TreeNode | None):\n    \"\"\"Обратный обход\"\"\"\n    if root is None:\n        return\n    # Порядок обхода: левое поддерево -> правое поддерево -> корень\n    post_order(root=root.left)\n    post_order(root=root.right)\n    res.append(root.val)\n
        binary_tree_dfs.cpp
        /* Предварительный обход */\nvoid preOrder(TreeNode *root) {\n    if (root == nullptr)\n        return;\n    // Порядок обхода: корень -> левое поддерево -> правое поддерево\n    vec.push_back(root->val);\n    preOrder(root->left);\n    preOrder(root->right);\n}\n\n/* Симметричный обход */\nvoid inOrder(TreeNode *root) {\n    if (root == nullptr)\n        return;\n    // Порядок обхода: левое поддерево -> корень -> правое поддерево\n    inOrder(root->left);\n    vec.push_back(root->val);\n    inOrder(root->right);\n}\n\n/* Обратный обход */\nvoid postOrder(TreeNode *root) {\n    if (root == nullptr)\n        return;\n    // Порядок обхода: левое поддерево -> правое поддерево -> корень\n    postOrder(root->left);\n    postOrder(root->right);\n    vec.push_back(root->val);\n}\n
        binary_tree_dfs.java
        /* Предварительный обход */\nvoid preOrder(TreeNode root) {\n    if (root == null)\n        return;\n    // Порядок обхода: корень -> левое поддерево -> правое поддерево\n    list.add(root.val);\n    preOrder(root.left);\n    preOrder(root.right);\n}\n\n/* Симметричный обход */\nvoid inOrder(TreeNode root) {\n    if (root == null)\n        return;\n    // Порядок обхода: левое поддерево -> корень -> правое поддерево\n    inOrder(root.left);\n    list.add(root.val);\n    inOrder(root.right);\n}\n\n/* Обратный обход */\nvoid postOrder(TreeNode root) {\n    if (root == null)\n        return;\n    // Порядок обхода: левое поддерево -> правое поддерево -> корень\n    postOrder(root.left);\n    postOrder(root.right);\n    list.add(root.val);\n}\n
        binary_tree_dfs.cs
        /* Предварительный обход */\nvoid PreOrder(TreeNode? root) {\n    if (root == null) return;\n    // Порядок обхода: корень -> левое поддерево -> правое поддерево\n    list.Add(root.val!.Value);\n    PreOrder(root.left);\n    PreOrder(root.right);\n}\n\n/* Симметричный обход */\nvoid InOrder(TreeNode? root) {\n    if (root == null) return;\n    // Порядок обхода: левое поддерево -> корень -> правое поддерево\n    InOrder(root.left);\n    list.Add(root.val!.Value);\n    InOrder(root.right);\n}\n\n/* Обратный обход */\nvoid PostOrder(TreeNode? root) {\n    if (root == null) return;\n    // Порядок обхода: левое поддерево -> правое поддерево -> корень\n    PostOrder(root.left);\n    PostOrder(root.right);\n    list.Add(root.val!.Value);\n}\n
        binary_tree_dfs.go
        /* Предварительный обход */\nfunc preOrder(node *TreeNode) {\n    if node == nil {\n        return\n    }\n    // Порядок обхода: корень -> левое поддерево -> правое поддерево\n    nums = append(nums, node.Val)\n    preOrder(node.Left)\n    preOrder(node.Right)\n}\n\n/* Симметричный обход */\nfunc inOrder(node *TreeNode) {\n    if node == nil {\n        return\n    }\n    // Порядок обхода: левое поддерево -> корень -> правое поддерево\n    inOrder(node.Left)\n    nums = append(nums, node.Val)\n    inOrder(node.Right)\n}\n\n/* Обратный обход */\nfunc postOrder(node *TreeNode) {\n    if node == nil {\n        return\n    }\n    // Порядок обхода: левое поддерево -> правое поддерево -> корень\n    postOrder(node.Left)\n    postOrder(node.Right)\n    nums = append(nums, node.Val)\n}\n
        binary_tree_dfs.swift
        /* Предварительный обход */\nfunc preOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    // Порядок обхода: корень -> левое поддерево -> правое поддерево\n    list.append(root.val)\n    preOrder(root: root.left)\n    preOrder(root: root.right)\n}\n\n/* Симметричный обход */\nfunc inOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    // Порядок обхода: левое поддерево -> корень -> правое поддерево\n    inOrder(root: root.left)\n    list.append(root.val)\n    inOrder(root: root.right)\n}\n\n/* Обратный обход */\nfunc postOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    // Порядок обхода: левое поддерево -> правое поддерево -> корень\n    postOrder(root: root.left)\n    postOrder(root: root.right)\n    list.append(root.val)\n}\n
        binary_tree_dfs.js
        /* Предварительный обход */\nfunction preOrder(root) {\n    if (root === null) return;\n    // Порядок обхода: корень -> левое поддерево -> правое поддерево\n    list.push(root.val);\n    preOrder(root.left);\n    preOrder(root.right);\n}\n\n/* Симметричный обход */\nfunction inOrder(root) {\n    if (root === null) return;\n    // Порядок обхода: левое поддерево -> корень -> правое поддерево\n    inOrder(root.left);\n    list.push(root.val);\n    inOrder(root.right);\n}\n\n/* Обратный обход */\nfunction postOrder(root) {\n    if (root === null) return;\n    // Порядок обхода: левое поддерево -> правое поддерево -> корень\n    postOrder(root.left);\n    postOrder(root.right);\n    list.push(root.val);\n}\n
        binary_tree_dfs.ts
        /* Предварительный обход */\nfunction preOrder(root: TreeNode | null): void {\n    if (root === null) {\n        return;\n    }\n    // Порядок обхода: корень -> левое поддерево -> правое поддерево\n    list.push(root.val);\n    preOrder(root.left);\n    preOrder(root.right);\n}\n\n/* Симметричный обход */\nfunction inOrder(root: TreeNode | null): void {\n    if (root === null) {\n        return;\n    }\n    // Порядок обхода: левое поддерево -> корень -> правое поддерево\n    inOrder(root.left);\n    list.push(root.val);\n    inOrder(root.right);\n}\n\n/* Обратный обход */\nfunction postOrder(root: TreeNode | null): void {\n    if (root === null) {\n        return;\n    }\n    // Порядок обхода: левое поддерево -> правое поддерево -> корень\n    postOrder(root.left);\n    postOrder(root.right);\n    list.push(root.val);\n}\n
        binary_tree_dfs.dart
        /* Предварительный обход */\nvoid preOrder(TreeNode? node) {\n  if (node == null) return;\n  // Порядок обхода: корень -> левое поддерево -> правое поддерево\n  list.add(node.val);\n  preOrder(node.left);\n  preOrder(node.right);\n}\n\n/* Симметричный обход */\nvoid inOrder(TreeNode? node) {\n  if (node == null) return;\n  // Порядок обхода: левое поддерево -> корень -> правое поддерево\n  inOrder(node.left);\n  list.add(node.val);\n  inOrder(node.right);\n}\n\n/* Обратный обход */\nvoid postOrder(TreeNode? node) {\n  if (node == null) return;\n  // Порядок обхода: левое поддерево -> правое поддерево -> корень\n  postOrder(node.left);\n  postOrder(node.right);\n  list.add(node.val);\n}\n
        binary_tree_dfs.rs
        /* Предварительный обход */\nfn pre_order(root: Option<&Rc<RefCell<TreeNode>>>) -> Vec<i32> {\n    let mut result = vec![];\n\n    fn dfs(root: Option<&Rc<RefCell<TreeNode>>>, res: &mut Vec<i32>) {\n        if let Some(node) = root {\n            // Порядок обхода: корень -> левое поддерево -> правое поддерево\n            let node = node.borrow();\n            res.push(node.val);\n            dfs(node.left.as_ref(), res);\n            dfs(node.right.as_ref(), res);\n        }\n    }\n    dfs(root, &mut result);\n\n    result\n}\n\n/* Симметричный обход */\nfn in_order(root: Option<&Rc<RefCell<TreeNode>>>) -> Vec<i32> {\n    let mut result = vec![];\n\n    fn dfs(root: Option<&Rc<RefCell<TreeNode>>>, res: &mut Vec<i32>) {\n        if let Some(node) = root {\n            // Порядок обхода: левое поддерево -> корень -> правое поддерево\n            let node = node.borrow();\n            dfs(node.left.as_ref(), res);\n            res.push(node.val);\n            dfs(node.right.as_ref(), res);\n        }\n    }\n    dfs(root, &mut result);\n\n    result\n}\n\n/* Обратный обход */\nfn post_order(root: Option<&Rc<RefCell<TreeNode>>>) -> Vec<i32> {\n    let mut result = vec![];\n\n    fn dfs(root: Option<&Rc<RefCell<TreeNode>>>, res: &mut Vec<i32>) {\n        if let Some(node) = root {\n            // Порядок обхода: левое поддерево -> правое поддерево -> корень\n            let node = node.borrow();\n            dfs(node.left.as_ref(), res);\n            dfs(node.right.as_ref(), res);\n            res.push(node.val);\n        }\n    }\n\n    dfs(root, &mut result);\n\n    result\n}\n
        binary_tree_dfs.c
        /* Предварительный обход */\nvoid preOrder(TreeNode *root, int *size) {\n    if (root == NULL)\n        return;\n    // Порядок обхода: корень -> левое поддерево -> правое поддерево\n    arr[(*size)++] = root->val;\n    preOrder(root->left, size);\n    preOrder(root->right, size);\n}\n\n/* Симметричный обход */\nvoid inOrder(TreeNode *root, int *size) {\n    if (root == NULL)\n        return;\n    // Порядок обхода: левое поддерево -> корень -> правое поддерево\n    inOrder(root->left, size);\n    arr[(*size)++] = root->val;\n    inOrder(root->right, size);\n}\n\n/* Обратный обход */\nvoid postOrder(TreeNode *root, int *size) {\n    if (root == NULL)\n        return;\n    // Порядок обхода: левое поддерево -> правое поддерево -> корень\n    postOrder(root->left, size);\n    postOrder(root->right, size);\n    arr[(*size)++] = root->val;\n}\n
        binary_tree_dfs.kt
        /* Предварительный обход */\nfun preOrder(root: TreeNode?) {\n    if (root == null) return\n    // Порядок обхода: корень -> левое поддерево -> правое поддерево\n    list.add(root._val)\n    preOrder(root.left)\n    preOrder(root.right)\n}\n\n/* Симметричный обход */\nfun inOrder(root: TreeNode?) {\n    if (root == null) return\n    // Порядок обхода: левое поддерево -> корень -> правое поддерево\n    inOrder(root.left)\n    list.add(root._val)\n    inOrder(root.right)\n}\n\n/* Обратный обход */\nfun postOrder(root: TreeNode?) {\n    if (root == null) return\n    // Порядок обхода: левое поддерево -> правое поддерево -> корень\n    postOrder(root.left)\n    postOrder(root.right)\n    list.add(root._val)\n}\n
        binary_tree_dfs.rb
        ### Предварительный обход ###\ndef pre_order(root)\n  return if root.nil?\n\n  # Порядок обхода: корень -> левое поддерево -> правое поддерево\n  $res << root.val\n  pre_order(root.left)\n  pre_order(root.right)\nend\n\n### Симметричный обход ###\ndef in_order(root)\n  return if root.nil?\n\n  # Порядок обхода: левое поддерево -> корень -> правое поддерево\n  in_order(root.left)\n  $res << root.val\n  in_order(root.right)\nend\n\n### Обратный обход ###\ndef post_order(root)\n  return if root.nil?\n\n  # Порядок обхода: левое поддерево -> правое поддерево -> корень\n  post_order(root.left)\n  post_order(root.right)\n  $res << root.val\nend\n
        Визуализация кода

        Во весь экран >

        Tip

        Поиск в глубину можно реализовать и итеративно; заинтересованные читатели могут изучить это самостоятельно.

        На рисунках ниже показан рекурсивный процесс прямого обхода двоичного дерева. Его можно разделить на две противоположные части: \"вход в рекурсию\" и \"возврат\".

        1. \"Вход в рекурсию\" означает запуск нового вызова функции; в этом процессе программа переходит к следующему узлу.
        2. \"Возврат\" означает завершение вызова функции и возврат назад, то есть текущий узел уже полностью обработан.
        <1><2><3><4><5><6><7><8><9><10><11>

        Рисунок 7-11   Рекурсивный процесс прямого обхода

        ","path":["Глава 7. Деревья","7.2   Обход двоичного дерева"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#2_1","level":3,"title":"2.   Анализ сложности","text":"
        • Временная сложность равна \\(O(n)\\) : все узлы посещаются по одному разу, поэтому требуется \\(O(n)\\) времени.
        • Пространственная сложность равна \\(O(n)\\) : в худшем случае, когда дерево вырождается в связный список, глубина рекурсии достигает \\(n\\) , и система тратит \\(O(n)\\) памяти на стек вызовов.
        ","path":["Глава 7. Деревья","7.2   Обход двоичного дерева"],"tags":[]},{"location":"chapter_tree/summary/","level":1,"title":"7.6   Краткие итоги","text":"","path":["Глава 7. Деревья","7.6   Краткие итоги"],"tags":[]},{"location":"chapter_tree/summary/#1","level":3,"title":"1.   Основные моменты","text":"
        • Двоичное дерево - это нелинейная структура данных, отражающая логику \"разделяй и властвуй\". Каждый узел двоичного дерева содержит значение и два указателя, которые соответственно ведут к левому и правому дочерним узлам.
        • Для любого узла двоичного дерева дерево, образованное его левым (правым) дочерним узлом и всеми нижележащими узлами, называется левым (правым) поддеревом этого узла.
        • К связанным с двоичным деревом терминам относятся корневой узел, листовой узел, уровень, степень, ребро, высота, глубина и так далее.
        • Инициализация двоичного дерева, вставка узлов и удаление узлов аналогичны операциям со связным списком.
        • К распространенным видам двоичного дерева относятся идеальное двоичное дерево, полное двоичное дерево, строгое двоичное дерево и сбалансированное двоичное дерево. Идеальное двоичное дерево - наиболее желательное состояние, а связный список - худший случай после вырождения.
        • Двоичное дерево можно представить массивом: значения узлов и пустые позиции располагаются в порядке обхода по уровням, а связи между родителем и детьми реализуются через индексацию.
        • Обход двоичного дерева по уровням является методом поиска в ширину; он отражает идею \"расширяться от центра к периферии слой за слоем\" и обычно реализуется через очередь.
        • Прямой, симметричный и обратный обходы относятся к поиску в глубину; они отражают идею \"сначала дойти до конца, затем вернуться и продолжить\" и обычно реализуются рекурсивно.
        • Двоичное дерево поиска - это эффективная структура данных для поиска элементов; его поиск, вставка и удаление имеют временную сложность \\(O(\\log n)\\) . Когда двоичное дерево поиска вырождается в связный список, все эти сложности деградируют до \\(O(n)\\) .
        • AVL-дерево, также называемое сбалансированным двоичным деревом поиска, с помощью вращений гарантирует, что после постоянных вставок и удалений узлов дерево остается сбалансированным.
        • Вращения AVL-дерева включают правое вращение, левое вращение, сначала правое затем левое и сначала левое затем правое. После вставки или удаления узла AVL-дерево выполняет вращения снизу вверх, чтобы снова восстановить баланс.
        ","path":["Глава 7. Деревья","7.6   Краткие итоги"],"tags":[]},{"location":"chapter_tree/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

        Q: Для двоичного дерева, состоящего из одного узла, высота дерева и глубина корня обе равны \\(0\\) ?

        Да, потому что высота и глубина обычно определяются как \"число пройденных ребер\".

        Q: Вставка и удаление в двоичном дереве обычно выполняются в составе набора операций. Что именно означает этот \"набор операций\"? Можно ли понимать это как освобождение ресурсов у дочерних узлов ресурса?

        Возьмем в качестве примера двоичное дерево поиска: операция удаления узла делится на три случая, и каждый из этих случаев требует нескольких последовательных шагов работы с узлами.

        Q: Почему у DFS для двоичного дерева есть три порядка: прямой, симметричный и обратный? Для чего они нужны?

        Подобно прямому и обратному обходу массива, прямой, симметричный и обратный обходы - это три способа обхода двоичного дерева, с помощью которых можно получить результаты в определенном порядке. Например, в двоичном дереве поиска, где соблюдается отношение значение левого дочернего узла < значение корня < значение правого дочернего узла , если обходить дерево с приоритетом \"лево \\(\\rightarrow\\) корень \\(\\rightarrow\\) право\", то получится упорядоченная последовательность узлов.

        Q: Правое вращение работает с отношениями между node , child и grand_child . А связь между node и его исходным родителем разве не нужно поддерживать? После правого вращения она ведь не оборвется?

        На это нужно смотреть с точки зрения рекурсии. В правое вращение right_rotate(root) передается корень поддерева, а затем через return child возвращается корень этого поддерева уже после вращения. Соединение между новым корнем поддерева и его родителем восстанавливается после возврата функции и не входит в обязанности самой операции правого вращения.

        Q: В C++ функции делятся на private и public . Какая логика стоит за этим? Почему height() и updateHeight() помещают в разные области видимости?

        Главный критерий - область использования метода. Если метод нужен только внутри класса, его следует проектировать как private . Например, самостоятельный вызов updateHeight() пользователем не имеет смысла: это лишь один из шагов внутри вставки или удаления. А height() используется для чтения высоты узла, подобно vector.size() , поэтому его разумно делать public .

        Q: Как построить двоичное дерево поиска из набора входных данных? Важен ли выбор корневого узла?

        Да, важен. Способ построения дерева уже показан в методе build_tree() в коде двоичного дерева поиска. Что касается выбора корня, обычно входные данные сортируют, берут средний элемент как корень, а затем рекурсивно строят левое и правое поддеревья. Это позволяет в наибольшей степени сохранить баланс дерева.

        Q: Нужно ли в Java всегда использовать equals() для сравнения строк?

        В Java для базовых типов == используется, чтобы сравнивать, равны ли значения двух переменных. Для ссылочных типов логика у этих двух способов уже разная.

        • == : сравнивает, ссылаются ли две переменные на один и тот же объект, то есть совпадает ли их адрес в памяти.
        • equals(): сравнивает, равны ли значения двух объектов.

        Поэтому если нужно сравнить значения, то следует использовать equals() . Но строки, инициализированные как String a = \"hi\"; String b = \"hi\"; , хранятся в строковом пуле констант и указывают на один и тот же объект, поэтому в таком случае a == b тоже может дать истинный результат при сравнении содержимого.

        Q: До достижения самого нижнего уровня при обходе в ширину число узлов в очереди равно \\(2^h\\) ?

        Да. Например, для полного двоичного дерева высоты \\(h = 2\\) общее число узлов равно \\(n = 7\\) , а число узлов на нижнем уровне равно \\(4 = 2^h = (n + 1) / 2\\) .

        ","path":["Глава 7. Деревья","7.6   Краткие итоги"],"tags":[]}]} \ No newline at end of file +{"config":{"separator":"[\\s\\-_,:!=\\[\\]()\\\\\"`/]+|\\.(?!\\d)"},"items":[{"location":"chapter_appendix/","level":1,"title":"Глава 16.   Приложение","text":"","path":["Глава 16. Приложение","Глава 16.   Приложение"],"tags":[]},{"location":"chapter_appendix/#_1","level":2,"title":"Содержание главы","text":"
        • 16.1   Установка среды программирования
        • 16.2   Присоединяйтесь к созданию книги
        • 16.3   Глоссарий
        ","path":["Глава 16. Приложение","Глава 16.   Приложение"],"tags":[]},{"location":"chapter_appendix/contribution/","level":1,"title":"16.2   Присоединяйтесь к созданию книги","text":"

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

        Все GitHub ID авторов будут указаны на главных страницах репозитория книги, веб-версии и PDF-версии в знак благодарности за их бескорыстный вклад в сообщество открытого исходного кода.

        Сила открытого исходного кода

        Интервал между двумя тиражами бумажной книги обычно довольно велик, поэтому обновлять содержание очень неудобно.

        В этой же открытой книге цикл обновления содержания сокращается до нескольких дней, а иногда даже до нескольких часов.

        ","path":["Глава 16. Приложение","16.2   Присоединяйтесь к созданию книги"],"tags":[]},{"location":"chapter_appendix/contribution/#1","level":3,"title":"1.   Небольшие правки содержания","text":"

        Как показано на рисунке 16-3, в правом верхнем углу каждой страницы есть «значок редактирования». Текст или код можно изменить следующим образом.

        1. Нажмите на «значок редактирования». Если появится сообщение «You need to fork this repository», согласитесь с этим действием.
        2. Измените содержимое исходного Markdown-файла, проверьте корректность правок и постарайтесь сохранить единый стиль оформления.
        3. Внизу страницы заполните описание изменений, затем нажмите кнопку «Propose file change». После перехода на следующую страницу нажмите кнопку «Create pull request», чтобы отправить pull request.

        Рисунок 16-3   Кнопка редактирования страницы

        Изображения нельзя изменить напрямую, поэтому проблему с ними нужно описывать через новый Issue или комментарий. Мы постараемся как можно быстрее исправить и обновить изображение.

        ","path":["Глава 16. Приложение","16.2   Присоединяйтесь к созданию книги"],"tags":[]},{"location":"chapter_appendix/contribution/#2","level":3,"title":"2.   Создание содержания","text":"

        Если вам интересно участвовать в этом проекте с открытым исходным кодом, например переводить код на другие языки программирования или расширять содержание статей, то следует придерживаться следующего процесса Pull Request.

        1. Войдите в GitHub и сделайте Fork репозитория книги в свой личный аккаунт.
        2. Перейдите на страницу своего Fork-репозитория и с помощью команды git clone клонируйте репозиторий локально.
        3. Создавайте и редактируйте содержание локально, затем проведите полное тестирование и проверьте корректность кода.
        4. Зафиксируйте локальные изменения, после чего выполните Push в удаленный репозиторий.
        5. Обновите страницу репозитория и нажмите кнопку «Create pull request», чтобы инициировать pull request.
        ","path":["Глава 16. Приложение","16.2   Присоединяйтесь к созданию книги"],"tags":[]},{"location":"chapter_appendix/contribution/#3-docker","level":3,"title":"3.   Развертывание Docker","text":"

        В корневом каталоге hello-algo выполните следующий Docker-скрипт, после чего проект станет доступен по адресу http://localhost:8000:

        docker-compose up -d\n

        Удалить развертывание можно следующей командой:

        docker-compose down\n
        ","path":["Глава 16. Приложение","16.2   Присоединяйтесь к созданию книги"],"tags":[]},{"location":"chapter_appendix/installation/","level":1,"title":"16.1   Установка среды программирования","text":"","path":["Глава 16. Приложение","16.1   Установка среды программирования"],"tags":[]},{"location":"chapter_appendix/installation/#1611-ide","level":2,"title":"16.1.1   Установка IDE","text":"

        В качестве локальной интегрированной среды разработки (IDE) рекомендуется использовать открытую и быструю VS Code. Перейдите на официальный сайт VS Code, выберите версию для своей операционной системы и установите ее.

        Рисунок 16-1   Загрузка VS Code с официального сайта

        VS Code обладает мощной экосистемой расширений и поддерживает выполнение и отладку большинства языков программирования. Например, после установки расширения «Python Extension Pack» можно отлаживать код на Python. Процесс установки показан на рисунке 16-2.

        Рисунок 16-2   Установка расширений VS Code

        ","path":["Глава 16. Приложение","16.1   Установка среды программирования"],"tags":[]},{"location":"chapter_appendix/installation/#1612","level":2,"title":"16.1.2   Установка языковой среды","text":"","path":["Глава 16. Приложение","16.1   Установка среды программирования"],"tags":[]},{"location":"chapter_appendix/installation/#1-python","level":3,"title":"1.   Среда Python","text":"
        1. Загрузите и установите Miniconda3, требуется Python 3.10 или более поздняя версия.
        2. В магазине расширений VS Code найдите python и установите Python Extension Pack.
        3. (Необязательно) Введите в командной строке pip install black, чтобы установить инструмент форматирования кода.
        ","path":["Глава 16. Приложение","16.1   Установка среды программирования"],"tags":[]},{"location":"chapter_appendix/installation/#2-cc","level":3,"title":"2.   Среда C/C++","text":"
        1. В Windows требуется установить MinGW (руководство по настройке). В macOS компилятор Clang уже установлен по умолчанию.
        2. В магазине расширений VS Code найдите c++ и установите C/C++ Extension Pack.
        3. (Необязательно) Откройте страницу Settings, найдите параметр форматирования Clang_format_fallback Style и задайте значение { BasedOnStyle: Microsoft, BreakBeforeBraces: Attach }.
        ","path":["Глава 16. Приложение","16.1   Установка среды программирования"],"tags":[]},{"location":"chapter_appendix/installation/#3-java","level":3,"title":"3.   Среда Java","text":"
        1. Загрузите и установите OpenJDK (требуемая версия: > JDK 9).
        2. В магазине расширений VS Code найдите java и установите Extension Pack for Java.
        ","path":["Глава 16. Приложение","16.1   Установка среды программирования"],"tags":[]},{"location":"chapter_appendix/installation/#4-c","level":3,"title":"4.   Среда C","text":"
        1. Загрузите и установите .Net 8.0.
        2. В магазине расширений VS Code найдите C# Dev Kit и установите C# Dev Kit (руководство по настройке).
        3. Также можно использовать Visual Studio (руководство по установке).
        ","path":["Глава 16. Приложение","16.1   Установка среды программирования"],"tags":[]},{"location":"chapter_appendix/installation/#5-go","level":3,"title":"5.   Среда Go","text":"
        1. Загрузите и установите go.
        2. В магазине расширений VS Code найдите go и установите Go.
        3. Нажмите Ctrl + Shift + P, чтобы открыть командную палитру, введите go, выберите Go: Install/Update Tools, отметьте все инструменты и установите их.
        ","path":["Глава 16. Приложение","16.1   Установка среды программирования"],"tags":[]},{"location":"chapter_appendix/installation/#6-swift","level":3,"title":"6.   Среда Swift","text":"
        1. Загрузите и установите Swift.
        2. В магазине расширений VS Code найдите swift и установите Swift for Visual Studio Code.
        ","path":["Глава 16. Приложение","16.1   Установка среды программирования"],"tags":[]},{"location":"chapter_appendix/installation/#7-javascript","level":3,"title":"7.   Среда JavaScript","text":"
        1. Загрузите и установите Node.js.
        2. (Необязательно) В магазине расширений VS Code найдите Prettier и установите инструмент форматирования кода.
        ","path":["Глава 16. Приложение","16.1   Установка среды программирования"],"tags":[]},{"location":"chapter_appendix/installation/#8-typescript","level":3,"title":"8.   Среда TypeScript","text":"
        1. Выполните те же шаги, что и для среды JavaScript.
        2. Установите TypeScript Execute (tsx).
        3. В магазине расширений VS Code найдите typescript и установите Pretty TypeScript Errors.
        ","path":["Глава 16. Приложение","16.1   Установка среды программирования"],"tags":[]},{"location":"chapter_appendix/installation/#9-dart","level":3,"title":"9.   Среда Dart","text":"
        1. Загрузите и установите Dart.
        2. В магазине расширений VS Code найдите dart и установите Dart.
        ","path":["Глава 16. Приложение","16.1   Установка среды программирования"],"tags":[]},{"location":"chapter_appendix/installation/#10-rust","level":3,"title":"10.   Среда Rust","text":"
        1. Загрузите и установите Rust.
        2. В магазине расширений VS Code найдите rust и установите rust-analyzer.
        ","path":["Глава 16. Приложение","16.1   Установка среды программирования"],"tags":[]},{"location":"chapter_appendix/terminology/","level":1,"title":"16.3   Глоссарий","text":"

        В таблице 16-1 приведены важные термины, встречающиеся в книге. Рекомендуем запомнить английские названия терминов, чтобы легче читать англоязычные материалы.

        Таблица 16-1   Важные термины по структурам данных и алгоритмам

        English Русский algorithm алгоритм data structure структура данных code код file файл function функция method метод variable переменная asymptotic complexity analysis асимптотический анализ сложности time complexity временная сложность space complexity пространственная сложность loop цикл iteration итерация recursion рекурсия tail recursion хвостовая рекурсия recursion tree дерево рекурсии big-\\(O\\) notation нотация big-\\(O\\) asymptotic upper bound асимптотическая верхняя граница sign-magnitude прямой код 1’s complement обратный код 2’s complement дополнительный код array массив index индекс linked list связный список linked list node, list node узел связного списка head node головной узел tail node хвостовой узел list список dynamic array динамический массив hard disk жесткий диск random-access memory (RAM) оперативная память cache memory кеш-память cache miss промах кеша cache hit rate коэффициент попадания в кеш stack стек top of the stack вершина стека bottom of the stack основание стека queue очередь double-ended queue двусторонняя очередь front of the queue голова очереди rear of the queue хвост очереди hash table хеш-таблица hash set хеш-набор bucket бакет hash function хеш-функция hash collision хеш-коллизия load factor коэффициент заполнения separate chaining цепная адресация open addressing открытая адресация linear probing линейное зондирование lazy deletion ленивое удаление binary tree двоичное дерево tree node узел дерева left-child node левый дочерний узел right-child node правый дочерний узел parent node родительский узел left subtree левое поддерево right subtree правое поддерево root node корневой узел leaf node листовой узел edge ребро level уровень degree степень height высота depth глубина perfect binary tree идеальное двоичное дерево complete binary tree полное двоичное дерево full binary tree строгое двоичное дерево balanced binary tree сбалансированное двоичное дерево binary search tree двоичное дерево поиска AVL tree АВЛ-дерево red-black tree красно-черное дерево level-order traversal обход по уровням breadth-first traversal обход в ширину depth-first traversal обход в глубину pre-order traversal прямой обход in-order traversal симметричный обход post-order traversal обратный обход balanced binary search tree сбалансированное двоичное дерево поиска balance factor фактор баланса heap куча max heap максимальная куча min heap минимальная куча priority queue приоритетная очередь heapify упорядочивание кучи top-\\(k\\) problem поиск \\(k\\) наибольших элементов graph граф vertex вершина undirected graph неориентированный граф directed graph ориентированный граф connected graph связный граф disconnected graph несвязный граф weighted graph взвешенный граф adjacency смежность path путь in-degree входящая степень out-degree исходящая степень adjacency matrix матрица смежности adjacency list список смежности breadth-first search поиск в ширину depth-first search поиск в глубину binary search двоичный поиск searching algorithm алгоритм поиска sorting algorithm алгоритм сортировки selection sort сортировка выбором bubble sort сортировка пузырьком insertion sort сортировка вставкой quick sort быстрая сортировка merge sort сортировка слиянием heap sort пирамидальная сортировка bucket sort блочная сортировка counting sort сортировка подсчетом radix sort поразрядная сортировка divide and conquer разделяй и властвуй hanota problem задача о Ханойской башне backtracking algorithm алгоритм поиска с возвратом constraint ограничение solution решение state состояние pruning отсечение permutations problem задача о перестановках subset-sum problem задача о сумме подмножеств \\(n\\)-queens problem задача о \\(n\\) ферзях dynamic programming динамическое программирование initial state начальное состояние state-transition equation уравнение перехода состояния knapsack problem задача о рюкзаке edit distance problem задача о расстоянии редактирования greedy algorithm жадный алгоритм","path":["Глава 16. Приложение","16.3   Глоссарий"],"tags":[]},{"location":"chapter_array_and_linkedlist/","level":1,"title":"Глава 4.   Массивы и списки","text":"

        Abstract

        Мир структур данных напоминает прочную кирпичную стену.

        Кирпичи массива уложены ровно и плотно прилегают друг к другу. Узлы связного списка, напротив, разбросаны в разных местах, а соединяющие их связи свободно тянутся между промежутками.

        ","path":["Глава 4. Массивы и списки","Глава 4.   Массивы и списки"],"tags":[]},{"location":"chapter_array_and_linkedlist/#_1","level":2,"title":"Содержание главы","text":"
        • 4.1   Массив
        • 4.2   Связный список
        • 4.3   Список
        • 4.4   Оперативная память и кэш *
        • 4.5   Резюме
        ","path":["Глава 4. Массивы и списки","Глава 4.   Массивы и списки"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/","level":1,"title":"4.1   Массив","text":"

        Массив (array) - это линейная структура данных, в которой элементы одного типа хранятся в непрерывной области памяти. Положение элемента в массиве называется его индексом (index). На рисунке 4-1 показаны основные понятия, связанные с массивом, и способ его хранения.

        Рисунок 4-1   Определение массива и способ хранения

        ","path":["Глава 4. Массивы и списки","4.1   Массив"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#411","level":2,"title":"4.1.1   Основные операции с массивом","text":"","path":["Глава 4. Массивы и списки","4.1   Массив"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#1","level":3,"title":"1.   Инициализация массива","text":"

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

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
        # Инициализация массива\narr: list[int] = [0] * 5  # [ 0, 0, 0, 0, 0 ]\nnums: list[int] = [1, 3, 2, 5, 4]\n
        array.cpp
        /* Инициализация массива */\n// Хранится в стеке\nint arr[5];\nint nums[5] = { 1, 3, 2, 5, 4 };\n// Хранится в куче (требуется ручное освобождение памяти)\nint* arr1 = new int[5];\nint* nums1 = new int[5] { 1, 3, 2, 5, 4 };\n
        array.java
        /* Инициализация массива */\nint[] arr = new int[5]; // { 0, 0, 0, 0, 0 }\nint[] nums = { 1, 3, 2, 5, 4 };\n
        array.cs
        /* Инициализация массива */\nint[] arr = new int[5]; // [ 0, 0, 0, 0, 0 ]\nint[] nums = [1, 3, 2, 5, 4];\n
        array.go
        /* Инициализация массива */\nvar arr [5]int\n// В Go указание длины ([5]int) создает массив, а отсутствие длины ([]int) - срез\n// Поскольку длина массива в Go определяется на этапе компиляции, для задания длины можно использовать только константы\n// Чтобы упростить реализацию метода extend(), ниже будем рассматривать срезы (Slice) как массивы (Array)\nnums := []int{1, 3, 2, 5, 4}\n
        array.swift
        /* Инициализация массива */\nlet arr = Array(repeating: 0, count: 5) // [0, 0, 0, 0, 0]\nlet nums = [1, 3, 2, 5, 4]\n
        array.js
        /* Инициализация массива */\nvar arr = new Array(5).fill(0);\nvar nums = [1, 3, 2, 5, 4];\n
        array.ts
        /* Инициализация массива */\nlet arr: number[] = new Array(5).fill(0);\nlet nums: number[] = [1, 3, 2, 5, 4];\n
        array.dart
        /* Инициализация массива */\nList<int> arr = List.filled(5, 0); // [0, 0, 0, 0, 0]\nList<int> nums = [1, 3, 2, 5, 4];\n
        array.rs
        /* Инициализация массива */\nlet arr: [i32; 5] = [0; 5]; // [0, 0, 0, 0, 0]\nlet slice: &[i32] = &[0; 5];\n// В Rust указание длины ([i32; 5]) создает массив, а отсутствие длины (&[i32]) - срез\n// Поскольку длина массива в Rust определяется на этапе компиляции, для задания длины можно использовать только константы\n// Vector в Rust обычно используется как динамический массив\n// Чтобы упростить реализацию метода extend(), ниже будем рассматривать vector как массив (array)\nlet nums: Vec<i32> = vec![1, 3, 2, 5, 4];\n
        array.c
        /* Инициализация массива */\nint arr[5] = { 0 }; // { 0, 0, 0, 0, 0 }\nint nums[5] = { 1, 3, 2, 5, 4 };\n
        array.kt
        /* Инициализация массива */\nvar arr = IntArray(5) // { 0, 0, 0, 0, 0 }\nvar nums = intArrayOf(1, 3, 2, 5, 4)\n
        array.rb
        # Инициализация массива\narr = Array.new(5, 0)\nnums = [1, 3, 2, 5, 4]\n
        Визуализация выполнения

        https://pythontutor.com/render.html#code=%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D0%BC%D0%B0%D1%81%D1%81%D0%B8%D0%B2%0Aarr%20%3D%20%5B0%5D%20%2A%205%20%20%23%20%5B%200%2C%200%2C%200%2C%200%2C%200%20%5D%0Anums%20%3D%20%5B1%2C%203%2C%202%2C%205%2C%204%5D&cumulative=false&curInstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

        ","path":["Глава 4. Массивы и списки","4.1   Массив"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#2","level":3,"title":"2.   Доступ к элементам","text":"

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

        Рисунок 4-2   Вычисление адреса элемента массива

        Как видно на рисунке 4-2, индекс первого элемента массива равен \\(0\\) , и это кажется не слишком интуитивным, ведь естественнее было бы начинать счет с \\(1\\) . Однако с точки зрения формулы адресации индекс по сути является смещением относительно адреса памяти. Смещение первого элемента равно \\(0\\) , поэтому индекс \\(0\\) полностью логичен.

        Доступ к элементам массива очень эффективен: любой элемент массива можно получить за \\(O(1)\\) времени.

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
        def random_access(nums: list[int]) -> int:\n    \"\"\"Случайный доступ к элементу\"\"\"\n    # Случайным образом выбрать число из интервала [0, len(nums)-1]\n    random_index = random.randint(0, len(nums) - 1)\n    # Получить и вернуть случайный элемент\n    random_num = nums[random_index]\n    return random_num\n
        array.cpp
        /* Случайный доступ к элементу */\nint randomAccess(int *nums, int size) {\n    // Случайным образом выбрать число из интервала [0, size)\n    int randomIndex = rand() % size;\n    // Получить и вернуть случайный элемент\n    int randomNum = nums[randomIndex];\n    return randomNum;\n}\n
        array.java
        /* Случайный доступ к элементу */\nint randomAccess(int[] nums) {\n    // Случайным образом выбрать число из интервала [0, nums.length)\n    int randomIndex = ThreadLocalRandom.current().nextInt(0, nums.length);\n    // Получить и вернуть случайный элемент\n    int randomNum = nums[randomIndex];\n    return randomNum;\n}\n
        array.cs
        /* Случайный доступ к элементу */\nint RandomAccess(int[] nums) {\n    Random random = new();\n    // Случайным образом выбрать число из интервала [0, nums.Length)\n    int randomIndex = random.Next(nums.Length);\n    // Получить и вернуть случайный элемент\n    int randomNum = nums[randomIndex];\n    return randomNum;\n}\n
        array.go
        /* Случайный доступ к элементу */\nfunc randomAccess(nums []int) (randomNum int) {\n    // Случайным образом выбрать число из интервала [0, nums.length)\n    randomIndex := rand.Intn(len(nums))\n    // Получить и вернуть случайный элемент\n    randomNum = nums[randomIndex]\n    return\n}\n
        array.swift
        /* Случайный доступ к элементу */\nfunc randomAccess(nums: [Int]) -> Int {\n    // Случайным образом выбрать число из интервала [0, nums.count)\n    let randomIndex = nums.indices.randomElement()!\n    // Получить и вернуть случайный элемент\n    let randomNum = nums[randomIndex]\n    return randomNum\n}\n
        array.js
        /* Случайный доступ к элементу */\nfunction randomAccess(nums) {\n    // Случайным образом выбрать число из интервала [0, nums.length)\n    const random_index = Math.floor(Math.random() * nums.length);\n    // Получить и вернуть случайный элемент\n    const random_num = nums[random_index];\n    return random_num;\n}\n
        array.ts
        /* Случайный доступ к элементу */\nfunction randomAccess(nums: number[]): number {\n    // Случайным образом выбрать число из интервала [0, nums.length)\n    const random_index = Math.floor(Math.random() * nums.length);\n    // Получить и вернуть случайный элемент\n    const random_num = nums[random_index];\n    return random_num;\n}\n
        array.dart
        /* Случайный доступ к элементу */\nint randomAccess(List<int> nums) {\n  // Случайным образом выбрать число из интервала [0, nums.length)\n  int randomIndex = Random().nextInt(nums.length);\n  // Получить и вернуть случайный элемент\n  int randomNum = nums[randomIndex];\n  return randomNum;\n}\n
        array.rs
        /* Случайный доступ к элементу */\nfn random_access(nums: &[i32]) -> i32 {\n    // Случайным образом выбрать число из интервала [0, nums.len())\n    let random_index = rand::thread_rng().gen_range(0..nums.len());\n    // Получить и вернуть случайный элемент\n    let random_num = nums[random_index];\n    random_num\n}\n
        array.c
        /* Случайный доступ к элементу */\nint randomAccess(int *nums, int size) {\n    // Случайным образом выбрать число из интервала [0, size)\n    int randomIndex = rand() % size;\n    // Получить и вернуть случайный элемент\n    int randomNum = nums[randomIndex];\n    return randomNum;\n}\n
        array.kt
        /* Случайный доступ к элементу */\nfun randomAccess(nums: IntArray): Int {\n    // Случайным образом выбрать число из интервала [0, nums.size)\n    val randomIndex = ThreadLocalRandom.current().nextInt(0, nums.size)\n    // Получить и вернуть случайный элемент\n    val randomNum = nums[randomIndex]\n    return randomNum\n}\n
        array.rb
        ### Случайный доступ к элементу ###\ndef random_access(nums)\n  # Случайным образом выбрать число из интервала [0, nums.length)\n  random_index = Random.rand(0...nums.length)\n\n  # Получить и вернуть случайный элемент\n  nums[random_index]\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 4. Массивы и списки","4.1   Массив"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#3","level":3,"title":"3.   Вставка элемента","text":"

        Элементы массива в памяти расположены вплотную друг к другу, и между ними нет места для размещения новых данных. Как показано на рисунке 4-3, если мы хотим вставить элемент в середину массива, то все элементы после этой позиции нужно сдвинуть на одну позицию вправо, а затем записать новое значение в освободившийся индекс.

        Рисунок 4-3   Пример вставки элемента в массив

        Стоит отметить, что длина массива фиксирована, поэтому вставка нового элемента неизбежно приведет к потере элемента на конце массива. Решение этой проблемы мы оставим для обсуждения в разделе о «списках».

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
        def insert(nums: list[int], num: int, index: int):\n    \"\"\"Вставить элемент num по индексу index в массив\"\"\"\n    # Сдвинуть элемент с индексом index и все последующие элементы на одну позицию назад\n    for i in range(len(nums) - 1, index, -1):\n        nums[i] = nums[i - 1]\n    # Присвоить num элементу по индексу index\n    nums[index] = num\n
        array.cpp
        /* Вставить элемент num по индексу index в массив */\nvoid insert(int *nums, int size, int num, int index) {\n    // Сдвинуть элемент с индексом index и все последующие элементы на одну позицию назад\n    for (int i = size - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // Присвоить num элементу по индексу index\n    nums[index] = num;\n}\n
        array.java
        /* Вставить элемент num по индексу index в массив */\nvoid insert(int[] nums, int num, int index) {\n    // Сдвинуть элемент с индексом index и все последующие элементы на одну позицию назад\n    for (int i = nums.length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // Присвоить num элементу по индексу index\n    nums[index] = num;\n}\n
        array.cs
        /* Вставить элемент num по индексу index в массив */\nvoid Insert(int[] nums, int num, int index) {\n    // Сдвинуть элемент с индексом index и все последующие элементы на одну позицию назад\n    for (int i = nums.Length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // Присвоить num элементу по индексу index\n    nums[index] = num;\n}\n
        array.go
        /* Вставить элемент num по индексу index в массив */\nfunc insert(nums []int, num int, index int) {\n    // Сдвинуть элемент с индексом index и все последующие элементы на одну позицию назад\n    for i := len(nums) - 1; i > index; i-- {\n        nums[i] = nums[i-1]\n    }\n    // Присвоить num элементу по индексу index\n    nums[index] = num\n}\n
        array.swift
        /* Вставить элемент num по индексу index в массив */\nfunc insert(nums: inout [Int], num: Int, index: Int) {\n    // Сдвинуть элемент с индексом index и все последующие элементы на одну позицию назад\n    for i in nums.indices.dropFirst(index).reversed() {\n        nums[i] = nums[i - 1]\n    }\n    // Присвоить num элементу по индексу index\n    nums[index] = num\n}\n
        array.js
        /* Вставить элемент num по индексу index в массив */\nfunction insert(nums, num, index) {\n    // Сдвинуть элемент с индексом index и все последующие элементы на одну позицию назад\n    for (let i = nums.length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // Присвоить num элементу по индексу index\n    nums[index] = num;\n}\n
        array.ts
        /* Вставить элемент num по индексу index в массив */\nfunction insert(nums: number[], num: number, index: number): void {\n    // Сдвинуть элемент с индексом index и все последующие элементы на одну позицию назад\n    for (let i = nums.length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // Присвоить num элементу по индексу index\n    nums[index] = num;\n}\n
        array.dart
        /* Вставить элемент _num по индексу index в массив */\nvoid insert(List<int> nums, int _num, int index) {\n  // Сдвинуть элемент с индексом index и все последующие элементы на одну позицию назад\n  for (var i = nums.length - 1; i > index; i--) {\n    nums[i] = nums[i - 1];\n  }\n  // Присвоить _num элементу по индексу index\n  nums[index] = _num;\n}\n
        array.rs
        /* Вставить элемент num по индексу index в массив */\nfn insert(nums: &mut [i32], num: i32, index: usize) {\n    // Сдвинуть элемент с индексом index и все последующие элементы на одну позицию назад\n    for i in (index + 1..nums.len()).rev() {\n        nums[i] = nums[i - 1];\n    }\n    // Присвоить num элементу по индексу index\n    nums[index] = num;\n}\n
        array.c
        /* Вставить элемент num по индексу index в массив */\nvoid insert(int *nums, int size, int num, int index) {\n    // Сдвинуть элемент с индексом index и все последующие элементы на одну позицию назад\n    for (int i = size - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // Присвоить num элементу по индексу index\n    nums[index] = num;\n}\n
        array.kt
        /* Вставить элемент num по индексу index в массив */\nfun insert(nums: IntArray, num: Int, index: Int) {\n    // Сдвинуть элемент с индексом index и все последующие элементы на одну позицию назад\n    for (i in nums.size - 1 downTo index + 1) {\n        nums[i] = nums[i - 1]\n    }\n    // Присвоить num элементу по индексу index\n    nums[index] = num\n}\n
        array.rb
        ### Вставка элемента num по индексу index в массив ###\ndef insert(nums, num, index)\n  # Сдвинуть элемент с индексом index и все последующие элементы на одну позицию назад\n  for i in (nums.length - 1).downto(index + 1)\n    nums[i] = nums[i - 1]\n  end\n\n  # Присвоить num элементу по индексу index\n  nums[index] = num\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 4. Массивы и списки","4.1   Массив"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#4","level":3,"title":"4.   Удаление элемента","text":"

        Аналогично, как показано на рисунке 4-4, если нужно удалить элемент по индексу \\(i\\) , то все элементы после индекса \\(i\\) необходимо сдвинуть на одну позицию влево.

        Рисунок 4-4   Пример удаления элемента из массива

        Обрати внимание: после удаления исходный последний элемент становится бессмысленным, поэтому специально изменять его не требуется.

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
        def remove(nums: list[int], index: int):\n    \"\"\"Удалить элемент по индексу index\"\"\"\n    # Сдвинуть все элементы после индекса index на одну позицию вперед\n    for i in range(index, len(nums) - 1):\n        nums[i] = nums[i + 1]\n
        array.cpp
        /* Удалить элемент по индексу index */\nvoid remove(int *nums, int size, int index) {\n    // Сдвинуть все элементы после индекса index на одну позицию вперед\n    for (int i = index; i < size - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
        array.java
        /* Удалить элемент по индексу index */\nvoid remove(int[] nums, int index) {\n    // Сдвинуть все элементы после индекса index на одну позицию вперед\n    for (int i = index; i < nums.length - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
        array.cs
        /* Удалить элемент по индексу index */\nvoid Remove(int[] nums, int index) {\n    // Сдвинуть все элементы после индекса index на одну позицию вперед\n    for (int i = index; i < nums.Length - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
        array.go
        /* Удалить элемент по индексу index */\nfunc remove(nums []int, index int) {\n    // Сдвинуть все элементы после индекса index на одну позицию вперед\n    for i := index; i < len(nums)-1; i++ {\n        nums[i] = nums[i+1]\n    }\n}\n
        array.swift
        /* Удалить элемент по индексу index */\nfunc remove(nums: inout [Int], index: Int) {\n    // Сдвинуть все элементы после индекса index на одну позицию вперед\n    for i in nums.indices.dropFirst(index).dropLast() {\n        nums[i] = nums[i + 1]\n    }\n}\n
        array.js
        /* Удалить элемент по индексу index */\nfunction remove(nums, index) {\n    // Сдвинуть все элементы после индекса index на одну позицию вперед\n    for (let i = index; i < nums.length - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
        array.ts
        /* Удалить элемент по индексу index */\nfunction remove(nums: number[], index: number): void {\n    // Сдвинуть все элементы после индекса index на одну позицию вперед\n    for (let i = index; i < nums.length - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
        array.dart
        /* Удалить элемент по индексу index */\nvoid remove(List<int> nums, int index) {\n  // Сдвинуть все элементы после индекса index на одну позицию вперед\n  for (var i = index; i < nums.length - 1; i++) {\n    nums[i] = nums[i + 1];\n  }\n}\n
        array.rs
        /* Удалить элемент по индексу index */\nfn remove(nums: &mut [i32], index: usize) {\n    // Сдвинуть все элементы после индекса index на одну позицию вперед\n    for i in index..nums.len() - 1 {\n        nums[i] = nums[i + 1];\n    }\n}\n
        array.c
        /* Удалить элемент по индексу index */\n// Внимание: stdio.h уже использует ключевое слово remove\nvoid removeItem(int *nums, int size, int index) {\n    // Сдвинуть все элементы после индекса index на одну позицию вперед\n    for (int i = index; i < size - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
        array.kt
        /* Удалить элемент по индексу index */\nfun remove(nums: IntArray, index: Int) {\n    // Сдвинуть все элементы после индекса index на одну позицию вперед\n    for (i in index..<nums.size - 1) {\n        nums[i] = nums[i + 1]\n    }\n}\n
        array.rb
        ### Удаление элемента по индексу index ###\ndef remove(nums, index)\n  # Сдвинуть все элементы после индекса index на одну позицию вперед\n  for i in index...(nums.length - 1)\n    nums[i] = nums[i + 1]\n  end\nend\n
        Визуализация кода

        Во весь экран >

        В целом операции вставки и удаления в массиве имеют следующие недостатки.

        • Высокая временная сложность: средняя временная сложность и вставки, и удаления равна \\(O(n)\\) , где \\(n\\) - длина массива.
        • Потеря элементов: поскольку длина массива неизменяема, после вставки элементы, выходящие за пределы длины массива, будут потеряны.
        • Потери памяти: можно заранее инициализировать более длинный массив и использовать только его переднюю часть. Тогда теряемые при вставке элементы на конце не будут нести смысла, но такой подход приводит к лишнему расходу памяти.
        ","path":["Глава 4. Массивы и списки","4.1   Массив"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#5","level":3,"title":"5.   Обход массива","text":"

        В большинстве языков программирования массив можно обходить как по индексу, так и напрямую перебирая каждый элемент:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
        def traverse(nums: list[int]):\n    \"\"\"Обход массива\"\"\"\n    count = 0\n    # Обход массива по индексам\n    for i in range(len(nums)):\n        count += nums[i]\n    # Непосредственно обходить элементы массива\n    for num in nums:\n        count += num\n    # Одновременно обходить индексы и элементы данных\n    for i, num in enumerate(nums):\n        count += nums[i]\n        count += num\n
        array.cpp
        /* Обход массива */\nvoid traverse(int *nums, int size) {\n    int count = 0;\n    // Обход массива по индексам\n    for (int i = 0; i < size; i++) {\n        count += nums[i];\n    }\n}\n
        array.java
        /* Обход массива */\nvoid traverse(int[] nums) {\n    int count = 0;\n    // Обход массива по индексам\n    for (int i = 0; i < nums.length; i++) {\n        count += nums[i];\n    }\n    // Непосредственно обходить элементы массива\n    for (int num : nums) {\n        count += num;\n    }\n}\n
        array.cs
        /* Обход массива */\nvoid Traverse(int[] nums) {\n    int count = 0;\n    // Обход массива по индексам\n    for (int i = 0; i < nums.Length; i++) {\n        count += nums[i];\n    }\n    // Непосредственно обходить элементы массива\n    foreach (int num in nums) {\n        count += num;\n    }\n}\n
        array.go
        /* Обход массива */\nfunc traverse(nums []int) {\n    count := 0\n    // Обход массива по индексам\n    for i := 0; i < len(nums); i++ {\n        count += nums[i]\n    }\n    count = 0\n    // Непосредственно обходить элементы массива\n    for _, num := range nums {\n        count += num\n    }\n    // Одновременно обходить индексы и элементы данных\n    for i, num := range nums {\n        count += nums[i]\n        count += num\n    }\n}\n
        array.swift
        /* Обход массива */\nfunc traverse(nums: [Int]) {\n    var count = 0\n    // Обход массива по индексам\n    for i in nums.indices {\n        count += nums[i]\n    }\n    // Непосредственно обходить элементы массива\n    for num in nums {\n        count += num\n    }\n    // Одновременно обходить индексы и элементы данных\n    for (i, num) in nums.enumerated() {\n        count += nums[i]\n        count += num\n    }\n}\n
        array.js
        /* Обход массива */\nfunction traverse(nums) {\n    let count = 0;\n    // Обход массива по индексам\n    for (let i = 0; i < nums.length; i++) {\n        count += nums[i];\n    }\n    // Непосредственно обходить элементы массива\n    for (const num of nums) {\n        count += num;\n    }\n}\n
        array.ts
        /* Обход массива */\nfunction traverse(nums: number[]): void {\n    let count = 0;\n    // Обход массива по индексам\n    for (let i = 0; i < nums.length; i++) {\n        count += nums[i];\n    }\n    // Непосредственно обходить элементы массива\n    for (const num of nums) {\n        count += num;\n    }\n}\n
        array.dart
        /* Перебрать элементы массива */\nvoid traverse(List<int> nums) {\n  int count = 0;\n  // Обход массива по индексам\n  for (var i = 0; i < nums.length; i++) {\n    count += nums[i];\n  }\n  // Непосредственно обходить элементы массива\n  for (int _num in nums) {\n    count += _num;\n  }\n  // Перебрать массив методом forEach\n  nums.forEach((_num) {\n    count += _num;\n  });\n}\n
        array.rs
        /* Обход массива */\nfn traverse(nums: &[i32]) {\n    let mut _count = 0;\n    // Обход массива по индексам\n    for i in 0..nums.len() {\n        _count += nums[i];\n    }\n    // Непосредственно обходить элементы массива\n    _count = 0;\n    for &num in nums {\n        _count += num;\n    }\n}\n
        array.c
        /* Обход массива */\nvoid traverse(int *nums, int size) {\n    int count = 0;\n    // Обход массива по индексам\n    for (int i = 0; i < size; i++) {\n        count += nums[i];\n    }\n}\n
        array.kt
        /* Обход массива */\nfun traverse(nums: IntArray) {\n    var count = 0\n    // Обход массива по индексам\n    for (i in nums.indices) {\n        count += nums[i]\n    }\n    // Непосредственно обходить элементы массива\n    for (j in nums) {\n        count += j\n    }\n}\n
        array.rb
        ### Обход массива ###\ndef traverse(nums)\n  count = 0\n\n  # Обход массива по индексам\n  for i in 0...nums.length\n    count += nums[i]\n  end\n\n  # Непосредственно обходить элементы массива\n  for num in nums\n    count += num\n  end\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 4. Массивы и списки","4.1   Массив"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#6","level":3,"title":"6.   Поиск элемента","text":"

        Чтобы найти заданный элемент в массиве, нужно пройти по массиву и на каждой итерации проверять, совпадает ли значение. Если совпадает, вернуть соответствующий индекс.

        Поскольку массив - это линейная структура данных, такая операция поиска называется линейным поиском.

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
        def find(nums: list[int], target: int) -> int:\n    \"\"\"Найти заданный элемент в массиве\"\"\"\n    for i in range(len(nums)):\n        if nums[i] == target:\n            return i\n    return -1\n
        array.cpp
        /* Найти заданный элемент в массиве */\nint find(int *nums, int size, int target) {\n    for (int i = 0; i < size; i++) {\n        if (nums[i] == target)\n            return i;\n    }\n    return -1;\n}\n
        array.java
        /* Найти заданный элемент в массиве */\nint find(int[] nums, int target) {\n    for (int i = 0; i < nums.length; i++) {\n        if (nums[i] == target)\n            return i;\n    }\n    return -1;\n}\n
        array.cs
        /* Найти заданный элемент в массиве */\nint Find(int[] nums, int target) {\n    for (int i = 0; i < nums.Length; i++) {\n        if (nums[i] == target)\n            return i;\n    }\n    return -1;\n}\n
        array.go
        /* Найти заданный элемент в массиве */\nfunc find(nums []int, target int) (index int) {\n    index = -1\n    for i := 0; i < len(nums); i++ {\n        if nums[i] == target {\n            index = i\n            break\n        }\n    }\n    return\n}\n
        array.swift
        /* Найти заданный элемент в массиве */\nfunc find(nums: [Int], target: Int) -> Int {\n    for i in nums.indices {\n        if nums[i] == target {\n            return i\n        }\n    }\n    return -1\n}\n
        array.js
        /* Найти заданный элемент в массиве */\nfunction find(nums, target) {\n    for (let i = 0; i < nums.length; i++) {\n        if (nums[i] === target) return i;\n    }\n    return -1;\n}\n
        array.ts
        /* Найти заданный элемент в массиве */\nfunction find(nums: number[], target: number): number {\n    for (let i = 0; i < nums.length; i++) {\n        if (nums[i] === target) {\n            return i;\n        }\n    }\n    return -1;\n}\n
        array.dart
        /* Найти заданный элемент в массиве */\nint find(List<int> nums, int target) {\n  for (var i = 0; i < nums.length; i++) {\n    if (nums[i] == target) return i;\n  }\n  return -1;\n}\n
        array.rs
        /* Найти заданный элемент в массиве */\nfn find(nums: &[i32], target: i32) -> Option<usize> {\n    for i in 0..nums.len() {\n        if nums[i] == target {\n            return Some(i);\n        }\n    }\n    None\n}\n
        array.c
        /* Найти заданный элемент в массиве */\nint find(int *nums, int size, int target) {\n    for (int i = 0; i < size; i++) {\n        if (nums[i] == target)\n            return i;\n    }\n    return -1;\n}\n
        array.kt
        /* Найти заданный элемент в массиве */\nfun find(nums: IntArray, target: Int): Int {\n    for (i in nums.indices) {\n        if (nums[i] == target)\n            return i\n    }\n    return -1\n}\n
        array.rb
        ### Поиск заданного элемента в массиве ###\ndef find(nums, target)\n  for i in 0...nums.length\n    return i if nums[i] == target\n  end\n\n  -1\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 4. Массивы и списки","4.1   Массив"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#7","level":3,"title":"7.   Расширение массива","text":"

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

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

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
        def extend(nums: list[int], enlarge: int) -> list[int]:\n    \"\"\"Увеличить длину массива\"\"\"\n    # Инициализировать массив увеличенной длины\n    res = [0] * (len(nums) + enlarge)\n    # Скопировать все элементы исходного массива в новый массив\n    for i in range(len(nums)):\n        res[i] = nums[i]\n    # Вернуть новый массив после расширения\n    return res\n
        array.cpp
        /* Увеличить длину массива */\nint *extend(int *nums, int size, int enlarge) {\n    // Инициализировать массив увеличенной длины\n    int *res = new int[size + enlarge];\n    // Скопировать все элементы исходного массива в новый массив\n    for (int i = 0; i < size; i++) {\n        res[i] = nums[i];\n    }\n    // Освободить память\n    delete[] nums;\n    // Вернуть новый массив после расширения\n    return res;\n}\n
        array.java
        /* Увеличить длину массива */\nint[] extend(int[] nums, int enlarge) {\n    // Инициализировать массив увеличенной длины\n    int[] res = new int[nums.length + enlarge];\n    // Скопировать все элементы исходного массива в новый массив\n    for (int i = 0; i < nums.length; i++) {\n        res[i] = nums[i];\n    }\n    // Вернуть новый массив после расширения\n    return res;\n}\n
        array.cs
        /* Увеличить длину массива */\nint[] Extend(int[] nums, int enlarge) {\n    // Инициализировать массив увеличенной длины\n    int[] res = new int[nums.Length + enlarge];\n    // Скопировать все элементы исходного массива в новый массив\n    for (int i = 0; i < nums.Length; i++) {\n        res[i] = nums[i];\n    }\n    // Вернуть новый массив после расширения\n    return res;\n}\n
        array.go
        /* Увеличить длину массива */\nfunc extend(nums []int, enlarge int) []int {\n    // Инициализировать массив увеличенной длины\n    res := make([]int, len(nums)+enlarge)\n    // Скопировать все элементы исходного массива в новый массив\n    for i, num := range nums {\n        res[i] = num\n    }\n    // Вернуть новый массив после расширения\n    return res\n}\n
        array.swift
        /* Увеличить длину массива */\nfunc extend(nums: [Int], enlarge: Int) -> [Int] {\n    // Инициализировать массив увеличенной длины\n    var res = Array(repeating: 0, count: nums.count + enlarge)\n    // Скопировать все элементы исходного массива в новый массив\n    for i in nums.indices {\n        res[i] = nums[i]\n    }\n    // Вернуть новый массив после расширения\n    return res\n}\n
        array.js
        /* Увеличить длину массива */\n// Обратите внимание: Array в JavaScript — это динамический массив, его можно расширять напрямую\n// Для удобства обучения в этой функции Array рассматривается как массив неизменяемой длины\nfunction extend(nums, enlarge) {\n    // Инициализировать массив увеличенной длины\n    const res = new Array(nums.length + enlarge).fill(0);\n    // Скопировать все элементы исходного массива в новый массив\n    for (let i = 0; i < nums.length; i++) {\n        res[i] = nums[i];\n    }\n    // Вернуть новый массив после расширения\n    return res;\n}\n
        array.ts
        /* Увеличить длину массива */\n// Обратите внимание: Array в TypeScript — это динамический массив, его можно расширять напрямую\n// Для удобства обучения в этой функции Array рассматривается как массив неизменяемой длины\nfunction extend(nums: number[], enlarge: number): number[] {\n    // Инициализировать массив увеличенной длины\n    const res = new Array(nums.length + enlarge).fill(0);\n    // Скопировать все элементы исходного массива в новый массив\n    for (let i = 0; i < nums.length; i++) {\n        res[i] = nums[i];\n    }\n    // Вернуть новый массив после расширения\n    return res;\n}\n
        array.dart
        /* Увеличить длину массива */\nList<int> extend(List<int> nums, int enlarge) {\n  // Инициализировать массив увеличенной длины\n  List<int> res = List.filled(nums.length + enlarge, 0);\n  // Скопировать все элементы исходного массива в новый массив\n  for (var i = 0; i < nums.length; i++) {\n    res[i] = nums[i];\n  }\n  // Вернуть новый массив после расширения\n  return res;\n}\n
        array.rs
        /* Увеличить длину массива */\nfn extend(nums: &[i32], enlarge: usize) -> Vec<i32> {\n    // Инициализировать массив увеличенной длины\n    let mut res: Vec<i32> = vec![0; nums.len() + enlarge];\n    // Скопировать все элементы исходного массива в новый\n    res[0..nums.len()].copy_from_slice(nums);\n\n    // Вернуть новый массив после расширения\n    res\n}\n
        array.c
        /* Увеличить длину массива */\nint *extend(int *nums, int size, int enlarge) {\n    // Инициализировать массив увеличенной длины\n    int *res = (int *)malloc(sizeof(int) * (size + enlarge));\n    // Скопировать все элементы исходного массива в новый массив\n    for (int i = 0; i < size; i++) {\n        res[i] = nums[i];\n    }\n    // Инициализировать расширенное пространство\n    for (int i = size; i < size + enlarge; i++) {\n        res[i] = 0;\n    }\n    // Вернуть новый массив после расширения\n    return res;\n}\n
        array.kt
        /* Увеличить длину массива */\nfun extend(nums: IntArray, enlarge: Int): IntArray {\n    // Инициализировать массив увеличенной длины\n    val res = IntArray(nums.size + enlarge)\n    // Скопировать все элементы исходного массива в новый массив\n    for (i in nums.indices) {\n        res[i] = nums[i]\n    }\n    // Вернуть новый массив после расширения\n    return res\n}\n
        array.rb
        ### Увеличить длину массива ###\n# Обратите внимание: Array в Ruby является динамическим массивом и может расширяться напрямую\n# Для удобства обучения в этой функции Array рассматривается как массив неизменяемой длины\ndef extend(nums, enlarge)\n  # Инициализировать массив увеличенной длины\n  res = Array.new(nums.length + enlarge, 0)\n\n  # Скопировать все элементы исходного массива в новый массив\n  for i in 0...nums.length\n    res[i] = nums[i]\n  end\n\n  # Вернуть новый массив после расширения\n  res\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 4. Массивы и списки","4.1   Массив"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#412","level":2,"title":"4.1.2   Преимущества и ограничения массива","text":"

        Массив хранится в непрерывной области памяти, и все его элементы имеют один и тот же тип. Такой подход содержит богатую априорную информацию, которую система может использовать для оптимизации эффективности операций с этой структурой данных.

        • Высокая пространственная эффективность: массив выделяет для данных непрерывный блок памяти без дополнительного структурного накладного расхода.
        • Поддержка произвольного доступа: массив позволяет обращаться к любому элементу за \\(O(1)\\) времени.
        • Локальность кэша: при обращении к элементу массива компьютер загружает не только сам элемент, но и соседние данные, что позволяет использовать кэш для ускорения последующих операций.

        Непрерывное хранение данных - это палка о двух концах, и у него есть следующие ограничения.

        • Низкая эффективность вставки и удаления: когда элементов в массиве много, вставка и удаление требуют сдвига большого количества элементов.
        • Неизменяемая длина: после инициализации длина массива фиксирована. Расширение массива требует копирования всех данных в новый массив, что стоит дорого.
        • Потери памяти: если выделенный массив больше, чем реально необходимо, лишнее пространство пропадает впустую.
        ","path":["Глава 4. Массивы и списки","4.1   Массив"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#413","level":2,"title":"4.1.3   Типичные применения массива","text":"

        Массив - это базовая и очень распространенная структура данных. Он часто используется как в различных алгоритмах, так и при реализации более сложных структур данных.

        • Произвольный доступ: если мы хотим случайным образом выбирать некоторые образцы, можно сохранить их в массиве и сгенерировать случайную последовательность индексов для выборки.
        • Сортировка и поиск: массив - самая распространенная структура данных для алгоритмов сортировки и поиска. Быстрая сортировка, сортировка слиянием, двоичный поиск и многие другие алгоритмы в основном работают именно с массивами.
        • Таблица поиска: когда нужно быстро находить элемент или его соответствие, массив можно использовать как таблицу поиска. Например, если мы хотим реализовать отображение символов в коды ASCII, можно использовать значение ASCII как индекс, а соответствующий элемент хранить по этой позиции массива.
        • Машинное обучение: в нейронных сетях широко используются операции линейной алгебры над векторами, матрицами и тензорами, и все эти данные строятся в форме массивов. Массив - самая часто используемая структура данных в программировании нейросетей.
        • Реализация структур данных: массивы можно использовать для реализации стеков, очередей, хеш-таблиц, куч, графов и других структур данных. Например, матрица смежности графа по сути является двумерным массивом.
        ","path":["Глава 4. Массивы и списки","4.1   Массив"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/","level":1,"title":"4.2   Связный список","text":"

        Память - общий ресурс для всех программ, и в сложной среде выполнения свободные участки памяти могут быть разбросаны по всему адресному пространству. Мы знаем, что память для хранения массива должна быть непрерывной, а если массив очень велик, в памяти может не оказаться столь большого непрерывного блока. Именно здесь и проявляется преимущество гибкости связного списка.

        Связный список (linked list) - это линейная структура данных, в которой каждый элемент представляет собой объект-узел, а сами узлы соединены между собой с помощью ссылок. Ссылка хранит адрес памяти следующего узла, благодаря чему из текущего узла можно перейти к следующему.

        Конструкция связного списка позволяет хранить отдельные узлы в разных местах памяти, и их адреса вовсе не обязаны быть последовательными.

        Рисунок 4-5   Определение связного списка и способ хранения

        Как видно на рисунке 4-5, базовой единицей связного списка является объект узел (node). Каждый узел содержит две части данных: значение узла и ссылку на следующий узел.

        • Первый узел связного списка называется головным узлом, а последний - хвостовым узлом.
        • Хвостовой узел указывает на пустое значение, что в Java, C++ и Python обозначается как null , nullptr и None соответственно.
        • В языках, поддерживающих указатели, таких как C, C++, Go и Rust, упомянутую выше ссылку следует заменить на указатель.

        Как показано в коде ниже, узел связного списка ListNode хранит не только значение, но и дополнительную ссылку (указатель). Поэтому при одинаковом объеме данных связный список занимает больше памяти, чем массив.

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
        class ListNode:\n    \"\"\"Класс узла связного списка\"\"\"\n    def __init__(self, val: int):\n        self.val: int = val               # Значение узла\n        self.next: ListNode | None = None # Ссылка на следующий узел\n
        /* Структура узла связного списка */\nstruct ListNode {\n    int val;         // Значение узла\n    ListNode *next;  // Указатель на следующий узел\n    ListNode(int x) : val(x), next(nullptr) {}  // Конструктор\n};\n
        /* Класс узла связного списка */\nclass ListNode {\n    int val;        // Значение узла\n    ListNode next;  // Ссылка на следующий узел\n    ListNode(int x) { val = x; }  // Конструктор\n}\n
        /* Класс узла связного списка */\nclass ListNode(int x) {  // Конструктор\n    int val = x;         // Значение узла\n    ListNode? next;      // Ссылка на следующий узел\n}\n
        /* Структура узла связного списка */\ntype ListNode struct {\n    Val  int       // Значение узла\n    Next *ListNode // Указатель на следующий узел\n}\n\n// NewListNode Конструктор, создает новый узел\nfunc NewListNode(val int) *ListNode {\n    return &ListNode{\n        Val:  val,\n        Next: nil,\n    }\n}\n
        /* Класс узла связного списка */\nclass ListNode {\n    var val: Int // Значение узла\n    var next: ListNode? // Ссылка на следующий узел\n\n    init(x: Int) { // Конструктор\n        val = x\n    }\n}\n
        /* Класс узла связного списка */\nclass ListNode {\n    constructor(val, next) {\n        this.val = (val === undefined ? 0 : val);       // Значение узла\n        this.next = (next === undefined ? null : next); // Ссылка на следующий узел\n    }\n}\n
        /* Класс узла связного списка */\nclass ListNode {\n    val: number;\n    next: ListNode | null;\n    constructor(val?: number, next?: ListNode | null) {\n        this.val = val === undefined ? 0 : val;        // Значение узла\n        this.next = next === undefined ? null : next;  // Ссылка на следующий узел\n    }\n}\n
        /* Класс узла связного списка */\nclass ListNode {\n  int val; // Значение узла\n  ListNode? next; // Ссылка на следующий узел\n  ListNode(this.val, [this.next]); // Конструктор\n}\n
        use std::rc::Rc;\nuse std::cell::RefCell;\n/* Класс узла связного списка */\n#[derive(Debug)]\nstruct ListNode {\n    val: i32, // Значение узла\n    next: Option<Rc<RefCell<ListNode>>>, // Указатель на следующий узел\n}\n
        /* Структура узла связного списка */\ntypedef struct ListNode {\n    int val;               // Значение узла\n    struct ListNode *next; // Указатель на следующий узел\n} ListNode;\n\n/* Конструктор */\nListNode *newListNode(int val) {\n    ListNode *node;\n    node = (ListNode *) malloc(sizeof(ListNode));\n    node->val = val;\n    node->next = NULL;\n    return node;\n}\n
        /* Класс узла связного списка */\n// Конструктор\nclass ListNode(x: Int) {\n    val _val: Int = x          // Значение узла\n    val next: ListNode? = null // Ссылка на следующий узел\n}\n
        # Класс узла связного списка\nclass ListNode\n  attr_accessor :val  # Значение узла\n  attr_accessor :next # Ссылка на следующий узел\n\n  def initialize(val=0, next_node=nil)\n    @val = val\n    @next = next_node\n  end\nend\n
        ","path":["Глава 4. Массивы и списки","4.2   Связный список"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#421","level":2,"title":"4.2.1   Основные операции со связным списком","text":"","path":["Глава 4. Массивы и списки","4.2   Связный список"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#1","level":3,"title":"1.   Инициализация связного списка","text":"

        Построение связного списка состоит из двух шагов: сначала нужно инициализировать объекты всех узлов, затем установить ссылочные связи между ними. После завершения инициализации мы можем, начиная с головы списка, последовательно проходить все узлы по ссылке next.

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
        # Инициализация связного списка 1 -> 3 -> 2 -> 5 -> 4\n# Инициализация отдельных узлов\nn0 = ListNode(1)\nn1 = ListNode(3)\nn2 = ListNode(2)\nn3 = ListNode(5)\nn4 = ListNode(4)\n# Построение ссылок между узлами\nn0.next = n1\nn1.next = n2\nn2.next = n3\nn3.next = n4\n
        linked_list.cpp
        /* Инициализация связного списка 1 -> 3 -> 2 -> 5 -> 4 */\n// Инициализация отдельных узлов\nListNode* n0 = new ListNode(1);\nListNode* n1 = new ListNode(3);\nListNode* n2 = new ListNode(2);\nListNode* n3 = new ListNode(5);\nListNode* n4 = new ListNode(4);\n// Построение ссылок между узлами\nn0->next = n1;\nn1->next = n2;\nn2->next = n3;\nn3->next = n4;\n
        linked_list.java
        /* Инициализация связного списка 1 -> 3 -> 2 -> 5 -> 4 */\n// Инициализация отдельных узлов\nListNode n0 = new ListNode(1);\nListNode n1 = new ListNode(3);\nListNode n2 = new ListNode(2);\nListNode n3 = new ListNode(5);\nListNode n4 = new ListNode(4);\n// Построение ссылок между узлами\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
        linked_list.cs
        /* Инициализация связного списка 1 -> 3 -> 2 -> 5 -> 4 */\n// Инициализация отдельных узлов\nListNode n0 = new(1);\nListNode n1 = new(3);\nListNode n2 = new(2);\nListNode n3 = new(5);\nListNode n4 = new(4);\n// Построение ссылок между узлами\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
        linked_list.go
        /* Инициализация связного списка 1 -> 3 -> 2 -> 5 -> 4 */\n// Инициализация отдельных узлов\nn0 := NewListNode(1)\nn1 := NewListNode(3)\nn2 := NewListNode(2)\nn3 := NewListNode(5)\nn4 := NewListNode(4)\n// Построение ссылок между узлами\nn0.Next = n1\nn1.Next = n2\nn2.Next = n3\nn3.Next = n4\n
        linked_list.swift
        /* Инициализация связного списка 1 -> 3 -> 2 -> 5 -> 4 */\n// Инициализация отдельных узлов\nlet n0 = ListNode(x: 1)\nlet n1 = ListNode(x: 3)\nlet n2 = ListNode(x: 2)\nlet n3 = ListNode(x: 5)\nlet n4 = ListNode(x: 4)\n// Построение ссылок между узлами\nn0.next = n1\nn1.next = n2\nn2.next = n3\nn3.next = n4\n
        linked_list.js
        /* Инициализация связного списка 1 -> 3 -> 2 -> 5 -> 4 */\n// Инициализация отдельных узлов\nconst n0 = new ListNode(1);\nconst n1 = new ListNode(3);\nconst n2 = new ListNode(2);\nconst n3 = new ListNode(5);\nconst n4 = new ListNode(4);\n// Построение ссылок между узлами\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
        linked_list.ts
        /* Инициализация связного списка 1 -> 3 -> 2 -> 5 -> 4 */\n// Инициализация отдельных узлов\nconst n0 = new ListNode(1);\nconst n1 = new ListNode(3);\nconst n2 = new ListNode(2);\nconst n3 = new ListNode(5);\nconst n4 = new ListNode(4);\n// Построение ссылок между узлами\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
        linked_list.dart
        /* Инициализация связного списка 1 -> 3 -> 2 -> 5 -> 4 */\\\n// Инициализация отдельных узлов\nListNode n0 = ListNode(1);\nListNode n1 = ListNode(3);\nListNode n2 = ListNode(2);\nListNode n3 = ListNode(5);\nListNode n4 = ListNode(4);\n// Построение ссылок между узлами\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
        linked_list.rs
        /* Инициализация связного списка 1 -> 3 -> 2 -> 5 -> 4 */\n// Инициализация отдельных узлов\nlet n0 = Rc::new(RefCell::new(ListNode { val: 1, next: None }));\nlet n1 = Rc::new(RefCell::new(ListNode { val: 3, next: None }));\nlet n2 = Rc::new(RefCell::new(ListNode { val: 2, next: None }));\nlet n3 = Rc::new(RefCell::new(ListNode { val: 5, next: None }));\nlet n4 = Rc::new(RefCell::new(ListNode { val: 4, next: None }));\n\n// Построение ссылок между узлами\nn0.borrow_mut().next = Some(n1.clone());\nn1.borrow_mut().next = Some(n2.clone());\nn2.borrow_mut().next = Some(n3.clone());\nn3.borrow_mut().next = Some(n4.clone());\n
        linked_list.c
        /* Инициализация связного списка 1 -> 3 -> 2 -> 5 -> 4 */\n// Инициализация отдельных узлов\nListNode* n0 = newListNode(1);\nListNode* n1 = newListNode(3);\nListNode* n2 = newListNode(2);\nListNode* n3 = newListNode(5);\nListNode* n4 = newListNode(4);\n// Построение ссылок между узлами\nn0->next = n1;\nn1->next = n2;\nn2->next = n3;\nn3->next = n4;\n
        linked_list.kt
        /* Инициализация связного списка 1 -> 3 -> 2 -> 5 -> 4 */\n// Инициализация отдельных узлов\nval n0 = ListNode(1)\nval n1 = ListNode(3)\nval n2 = ListNode(2)\nval n3 = ListNode(5)\nval n4 = ListNode(4)\n// Построение ссылок между узлами\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
        linked_list.rb
        # Инициализация связного списка 1 -> 3 -> 2 -> 5 -> 4\n# Инициализация отдельных узлов\nn0 = ListNode.new(1)\nn1 = ListNode.new(3)\nn2 = ListNode.new(2)\nn3 = ListNode.new(5)\nn4 = ListNode.new(4)\n# Построение ссылок между узлами\nn0.next = n1\nn1.next = n2\nn2.next = n3\nn3.next = n4\n
        Визуализация выполнения

        https://pythontutor.com/render.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%D1%81%D0%B2%D1%8F%D0%B7%D0%BD%D1%8B%D0%B9%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%D1%83%D0%B7%D0%B5%D0%BB%D0%BA%D0%BB%D0%B0%D1%81%D1%81%22%22%22%0A%20%20%20%20def%20__init__%28self%2C%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%D0%97%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%D1%83%D0%B7%D0%BB%D0%B0%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%D0%A1%D1%81%D1%8B%D0%BB%D0%BA%D0%B0%20%D0%BD%D0%B0%20%D1%81%D0%BB%D0%B5%D0%B4%D1%83%D1%8E%D1%89%D0%B8%D0%B9%20%D1%83%D0%B7%D0%B5%D0%BB%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%81%D0%B2%D1%8F%D0%B7%D0%BD%D1%8B%D0%B9%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%201%20-%3E%203%20-%3E%202%20-%3E%205%20-%3E%204%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D0%BA%D0%B0%D0%B6%D0%B4%D1%8B%D0%B9%20%D1%83%D0%B7%D0%B5%D0%BB%0A%20%20%20%20n0%20%3D%20ListNode%281%29%0A%20%20%20%20n1%20%3D%20ListNode%283%29%0A%20%20%20%20n2%20%3D%20ListNode%282%29%0A%20%20%20%20n3%20%3D%20ListNode%285%29%0A%20%20%20%20n4%20%3D%20ListNode%284%29%0A%20%20%20%20%23%20%D0%9F%D0%BE%D1%81%D1%82%D1%80%D0%BE%D0%B8%D1%82%D1%8C%20%D1%81%D1%81%D1%8B%D0%BB%D0%BA%D0%B8%20%D0%BC%D0%B5%D0%B6%D0%B4%D1%83%20%D1%83%D0%B7%D0%BB%D0%B0%D0%BC%D0%B8%0A%20%20%20%20n0.next%20%3D%20n1%0A%20%20%20%20n1.next%20%3D%20n2%0A%20%20%20%20n2.next%20%3D%20n3%0A%20%20%20%20n3.next%20%3D%20n4&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

        Массив в целом - это одна переменная: например, массив nums содержит элементы nums[0] , nums[1] и т.д. Связный список же состоит из множества независимых объектов-узлов. Обычно в качестве обозначения всего связного списка используют головной узел. Например, в приведенном выше коде связный список можно обозначить как n0 .

        ","path":["Глава 4. Массивы и списки","4.2   Связный список"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#2","level":3,"title":"2.   Вставка узла","text":"

        Вставить узел в связный список очень легко. Как показано на рисунке 4-6, предположим, что мы хотим вставить новый узел P между двумя соседними узлами n0 и n1. Для этого нужно изменить всего две ссылки (указателя), а временная сложность будет равна \\(O(1)\\) .

        Для сравнения: временная сложность вставки элемента в массив составляет \\(O(n)\\) , и при большом объеме данных это менее эффективно.

        Рисунок 4-6   Пример вставки узла в связный список

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
        def insert(n0: ListNode, P: ListNode):\n    \"\"\"Вставить узел P после узла n0 в связном списке\"\"\"\n    n1 = n0.next\n    P.next = n1\n    n0.next = P\n
        linked_list.cpp
        /* Вставить узел P после узла n0 в связном списке */\nvoid insert(ListNode *n0, ListNode *P) {\n    ListNode *n1 = n0->next;\n    P->next = n1;\n    n0->next = P;\n}\n
        linked_list.java
        /* Вставить узел P после узла n0 в связном списке */\nvoid insert(ListNode n0, ListNode P) {\n    ListNode n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
        linked_list.cs
        /* Вставить узел P после узла n0 в связном списке */\nvoid Insert(ListNode n0, ListNode P) {\n    ListNode? n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
        linked_list.go
        /* Вставить узел P после узла n0 в связном списке */\nfunc insertNode(n0 *ListNode, P *ListNode) {\n    n1 := n0.Next\n    P.Next = n1\n    n0.Next = P\n}\n
        linked_list.swift
        /* Вставить узел P после узла n0 в связном списке */\nfunc insert(n0: ListNode, P: ListNode) {\n    let n1 = n0.next\n    P.next = n1\n    n0.next = P\n}\n
        linked_list.js
        /* Вставить узел P после узла n0 в связном списке */\nfunction insert(n0, P) {\n    const n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
        linked_list.ts
        /* Вставить узел P после узла n0 в связном списке */\nfunction insert(n0: ListNode, P: ListNode): void {\n    const n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
        linked_list.dart
        /* Вставить узел P после узла n0 в связном списке */\nvoid insert(ListNode n0, ListNode P) {\n  ListNode? n1 = n0.next;\n  P.next = n1;\n  n0.next = P;\n}\n
        linked_list.rs
        /* Вставить узел P после узла n0 в связном списке */\n#[allow(non_snake_case)]\npub fn insert<T>(n0: &Rc<RefCell<ListNode<T>>>, P: Rc<RefCell<ListNode<T>>>) {\n    let n1 = n0.borrow_mut().next.take();\n    P.borrow_mut().next = n1;\n    n0.borrow_mut().next = Some(P);\n}\n
        linked_list.c
        /* Вставить узел P после узла n0 в связном списке */\nvoid insert(ListNode *n0, ListNode *P) {\n    ListNode *n1 = n0->next;\n    P->next = n1;\n    n0->next = P;\n}\n
        linked_list.kt
        /* Вставить узел P после узла n0 в связном списке */\nfun insert(n0: ListNode?, p: ListNode?) {\n    val n1 = n0?.next\n    p?.next = n1\n    n0?.next = p\n}\n
        linked_list.rb
        ### Вставка узла _p после узла n0 в связном списке ###\n# В Ruby `p` является встроенной функцией, а `P` — константой, поэтому вместо них можно использовать `_p`\ndef insert(n0, _p)\n  n1 = n0.next\n  _p.next = n1\n  n0.next = _p\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 4. Массивы и списки","4.2   Связный список"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#3","level":3,"title":"3.   Удаление узла","text":"

        Как показано на рисунке 4-7, удалить узел из связного списка тоже очень просто: нужно изменить всего одну ссылку (указатель).

        Стоит отметить, что хотя после завершения операции удаления узел P все еще указывает на n1 , при обходе связного списка до P уже нельзя добраться. Это означает, что P фактически больше не принадлежит данному списку.

        Рисунок 4-7   Удаление узла из связного списка

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
        def remove(n0: ListNode):\n    \"\"\"Удалить первый узел после узла n0 в связном списке\"\"\"\n    if not n0.next:\n        return\n    # n0 -> P -> n1\n    P = n0.next\n    n1 = P.next\n    n0.next = n1\n
        linked_list.cpp
        /* Удалить первый узел после узла n0 в связном списке */\nvoid remove(ListNode *n0) {\n    if (n0->next == nullptr)\n        return;\n    // n0 -> P -> n1\n    ListNode *P = n0->next;\n    ListNode *n1 = P->next;\n    n0->next = n1;\n    // Освободить память\n    delete P;\n}\n
        linked_list.java
        /* Удалить первый узел после узла n0 в связном списке */\nvoid remove(ListNode n0) {\n    if (n0.next == null)\n        return;\n    // n0 -> P -> n1\n    ListNode P = n0.next;\n    ListNode n1 = P.next;\n    n0.next = n1;\n}\n
        linked_list.cs
        /* Удалить первый узел после узла n0 в связном списке */\nvoid Remove(ListNode n0) {\n    if (n0.next == null)\n        return;\n    // n0 -> P -> n1\n    ListNode P = n0.next;\n    ListNode? n1 = P.next;\n    n0.next = n1;\n}\n
        linked_list.go
        /* Удалить первый узел после узла n0 в связном списке */\nfunc removeItem(n0 *ListNode) {\n    if n0.Next == nil {\n        return\n    }\n    // n0 -> P -> n1\n    P := n0.Next\n    n1 := P.Next\n    n0.Next = n1\n}\n
        linked_list.swift
        /* Удалить первый узел после узла n0 в связном списке */\nfunc remove(n0: ListNode) {\n    if n0.next == nil {\n        return\n    }\n    // n0 -> P -> n1\n    let P = n0.next\n    let n1 = P?.next\n    n0.next = n1\n}\n
        linked_list.js
        /* Удалить первый узел после узла n0 в связном списке */\nfunction remove(n0) {\n    if (!n0.next) return;\n    // n0 -> P -> n1\n    const P = n0.next;\n    const n1 = P.next;\n    n0.next = n1;\n}\n
        linked_list.ts
        /* Удалить первый узел после узла n0 в связном списке */\nfunction remove(n0: ListNode): void {\n    if (!n0.next) {\n        return;\n    }\n    // n0 -> P -> n1\n    const P = n0.next;\n    const n1 = P.next;\n    n0.next = n1;\n}\n
        linked_list.dart
        /* Удалить первый узел после узла n0 в связном списке */\nvoid remove(ListNode n0) {\n  if (n0.next == null) return;\n  // n0 -> P -> n1\n  ListNode P = n0.next!;\n  ListNode? n1 = P.next;\n  n0.next = n1;\n}\n
        linked_list.rs
        /* Удалить первый узел после узла n0 в связном списке */\n#[allow(non_snake_case)]\npub fn remove<T>(n0: &Rc<RefCell<ListNode<T>>>) {\n    // n0 -> P -> n1\n    let P = n0.borrow_mut().next.take();\n    if let Some(node) = P {\n        let n1 = node.borrow_mut().next.take();\n        n0.borrow_mut().next = n1;\n    }\n}\n
        linked_list.c
        /* Удалить первый узел после узла n0 в связном списке */\n// Внимание: stdio.h уже использует ключевое слово remove\nvoid removeItem(ListNode *n0) {\n    if (!n0->next)\n        return;\n    // n0 -> P -> n1\n    ListNode *P = n0->next;\n    ListNode *n1 = P->next;\n    n0->next = n1;\n    // Освободить память\n    free(P);\n}\n
        linked_list.kt
        /* Удалить первый узел после узла n0 в связном списке */\nfun remove(n0: ListNode?) {\n    if (n0?.next == null)\n        return\n    // n0 -> P -> n1\n    val p = n0.next\n    val n1 = p?.next\n    n0.next = n1\n}\n
        linked_list.rb
        ### Удаление первого узла после узла n0 в связном списке ###\ndef remove(n0)\n  return if n0.next.nil?\n\n  # n0 -> remove_node -> n1\n  remove_node = n0.next\n  n1 = remove_node.next\n  n0.next = n1\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 4. Массивы и списки","4.2   Связный список"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#4","level":3,"title":"4.   Доступ к узлу","text":"

        Доступ к узлам в связном списке менее эффективен. Как уже обсуждалось в предыдущем разделе, к любому элементу массива можно обратиться за \\(O(1)\\) времени. Со связным списком это не так: программе нужно начать с головного узла и последовательно двигаться дальше, пока не будет найден целевой узел. То есть для доступа к \\(i\\) -му узлу списка нужно выполнить \\(i - 1\\) итераций, а временная сложность составляет \\(O(n)\\) .

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
        def access(head: ListNode, index: int) -> ListNode | None:\n    \"\"\"Доступ к узлу связного списка по индексу index\"\"\"\n    for _ in range(index):\n        if not head:\n            return None\n        head = head.next\n    return head\n
        linked_list.cpp
        /* Доступ к узлу связного списка по индексу index */\nListNode *access(ListNode *head, int index) {\n    for (int i = 0; i < index; i++) {\n        if (head == nullptr)\n            return nullptr;\n        head = head->next;\n    }\n    return head;\n}\n
        linked_list.java
        /* Доступ к узлу связного списка по индексу index */\nListNode access(ListNode head, int index) {\n    for (int i = 0; i < index; i++) {\n        if (head == null)\n            return null;\n        head = head.next;\n    }\n    return head;\n}\n
        linked_list.cs
        /* Доступ к узлу связного списка по индексу index */\nListNode? Access(ListNode? head, int index) {\n    for (int i = 0; i < index; i++) {\n        if (head == null)\n            return null;\n        head = head.next;\n    }\n    return head;\n}\n
        linked_list.go
        /* Доступ к узлу связного списка по индексу index */\nfunc access(head *ListNode, index int) *ListNode {\n    for i := 0; i < index; i++ {\n        if head == nil {\n            return nil\n        }\n        head = head.Next\n    }\n    return head\n}\n
        linked_list.swift
        /* Доступ к узлу связного списка по индексу index */\nfunc access(head: ListNode, index: Int) -> ListNode? {\n    var head: ListNode? = head\n    for _ in 0 ..< index {\n        if head == nil {\n            return nil\n        }\n        head = head?.next\n    }\n    return head\n}\n
        linked_list.js
        /* Доступ к узлу связного списка по индексу index */\nfunction access(head, index) {\n    for (let i = 0; i < index; i++) {\n        if (!head) {\n            return null;\n        }\n        head = head.next;\n    }\n    return head;\n}\n
        linked_list.ts
        /* Доступ к узлу связного списка по индексу index */\nfunction access(head: ListNode | null, index: number): ListNode | null {\n    for (let i = 0; i < index; i++) {\n        if (!head) {\n            return null;\n        }\n        head = head.next;\n    }\n    return head;\n}\n
        linked_list.dart
        /* Доступ к узлу связного списка по индексу index */\nListNode? access(ListNode? head, int index) {\n  for (var i = 0; i < index; i++) {\n    if (head == null) return null;\n    head = head.next;\n  }\n  return head;\n}\n
        linked_list.rs
        /* Доступ к узлу связного списка по индексу index */\npub fn access<T>(head: Rc<RefCell<ListNode<T>>>, index: i32) -> Option<Rc<RefCell<ListNode<T>>>> {\n    fn dfs<T>(\n        head: Option<&Rc<RefCell<ListNode<T>>>>,\n        index: i32,\n    ) -> Option<Rc<RefCell<ListNode<T>>>> {\n        if index <= 0 {\n            return head.cloned();\n        }\n\n        if let Some(node) = head {\n            dfs(node.borrow().next.as_ref(), index - 1)\n        } else {\n            None\n        }\n    }\n\n    dfs(Some(head).as_ref(), index)\n}\n
        linked_list.c
        /* Доступ к узлу связного списка по индексу index */\nListNode *access(ListNode *head, int index) {\n    for (int i = 0; i < index; i++) {\n        if (head == NULL)\n            return NULL;\n        head = head->next;\n    }\n    return head;\n}\n
        linked_list.kt
        /* Доступ к узлу связного списка по индексу index */\nfun access(head: ListNode?, index: Int): ListNode? {\n    var h = head\n    for (i in 0..<index) {\n        if (h == null)\n            return null\n        h = h.next\n    }\n    return h\n}\n
        linked_list.rb
        ### Доступ к узлу связного списка по индексу index ###\ndef access(head, index)\n  for i in 0...index\n    return nil if head.nil?\n    head = head.next\n  end\n\n  head\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 4. Массивы и списки","4.2   Связный список"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#5","level":3,"title":"5.   Поиск узла","text":"

        Поиск узла заключается в обходе связного списка, нахождении узла со значением target и возврате его индекса в списке. Этот процесс тоже относится к линейному поиску. Код выглядит следующим образом:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
        def find(head: ListNode, target: int) -> int:\n    \"\"\"Найти в связном списке первый узел со значением target\"\"\"\n    index = 0\n    while head:\n        if head.val == target:\n            return index\n        head = head.next\n        index += 1\n    return -1\n
        linked_list.cpp
        /* Найти в связном списке первый узел со значением target */\nint find(ListNode *head, int target) {\n    int index = 0;\n    while (head != nullptr) {\n        if (head->val == target)\n            return index;\n        head = head->next;\n        index++;\n    }\n    return -1;\n}\n
        linked_list.java
        /* Найти в связном списке первый узел со значением target */\nint find(ListNode head, int target) {\n    int index = 0;\n    while (head != null) {\n        if (head.val == target)\n            return index;\n        head = head.next;\n        index++;\n    }\n    return -1;\n}\n
        linked_list.cs
        /* Найти в связном списке первый узел со значением target */\nint Find(ListNode? head, int target) {\n    int index = 0;\n    while (head != null) {\n        if (head.val == target)\n            return index;\n        head = head.next;\n        index++;\n    }\n    return -1;\n}\n
        linked_list.go
        /* Найти в связном списке первый узел со значением target */\nfunc findNode(head *ListNode, target int) int {\n    index := 0\n    for head != nil {\n        if head.Val == target {\n            return index\n        }\n        head = head.Next\n        index++\n    }\n    return -1\n}\n
        linked_list.swift
        /* Найти в связном списке первый узел со значением target */\nfunc find(head: ListNode, target: Int) -> Int {\n    var head: ListNode? = head\n    var index = 0\n    while head != nil {\n        if head?.val == target {\n            return index\n        }\n        head = head?.next\n        index += 1\n    }\n    return -1\n}\n
        linked_list.js
        /* Найти в связном списке первый узел со значением target */\nfunction find(head, target) {\n    let index = 0;\n    while (head !== null) {\n        if (head.val === target) {\n            return index;\n        }\n        head = head.next;\n        index += 1;\n    }\n    return -1;\n}\n
        linked_list.ts
        /* Найти в связном списке первый узел со значением target */\nfunction find(head: ListNode | null, target: number): number {\n    let index = 0;\n    while (head !== null) {\n        if (head.val === target) {\n            return index;\n        }\n        head = head.next;\n        index += 1;\n    }\n    return -1;\n}\n
        linked_list.dart
        /* Найти в связном списке первый узел со значением target */\nint find(ListNode? head, int target) {\n  int index = 0;\n  while (head != null) {\n    if (head.val == target) {\n      return index;\n    }\n    head = head.next;\n    index++;\n  }\n  return -1;\n}\n
        linked_list.rs
        /* Найти в связном списке первый узел со значением target */\npub fn find<T: PartialEq>(head: Rc<RefCell<ListNode<T>>>, target: T) -> i32 {\n    fn find<T: PartialEq>(head: Option<&Rc<RefCell<ListNode<T>>>>, target: T, idx: i32) -> i32 {\n        if let Some(node) = head {\n            if node.borrow().val == target {\n                return idx;\n            }\n            return find(node.borrow().next.as_ref(), target, idx + 1);\n        } else {\n            -1\n        }\n    }\n\n    find(Some(head).as_ref(), target, 0)\n}\n
        linked_list.c
        /* Найти в связном списке первый узел со значением target */\nint find(ListNode *head, int target) {\n    int index = 0;\n    while (head) {\n        if (head->val == target)\n            return index;\n        head = head->next;\n        index++;\n    }\n    return -1;\n}\n
        linked_list.kt
        /* Найти в связном списке первый узел со значением target */\nfun find(head: ListNode?, target: Int): Int {\n    var index = 0\n    var h = head\n    while (h != null) {\n        if (h._val == target)\n            return index\n        h = h.next\n        index++\n    }\n    return -1\n}\n
        linked_list.rb
        ### Поиск первого узла со значением target в связном списке ###\ndef find(head, target)\n  index = 0\n  while head\n    return index if head.val == target\n    head = head.next\n    index += 1\n  end\n\n  -1\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 4. Массивы и списки","4.2   Связный список"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#422","level":2,"title":"4.2.2   Сравнение массива и связного списка","text":"

        В таблице 4-1 обобщаются свойства массива и связного списка, а также сравнивается эффективность соответствующих операций. Поскольку они используют противоположные стратегии хранения, их свойства и эффективность операций тоже во многом противоположны.

        Таблица 4-1   Сравнение эффективности массива и связного списка

        Массив Связный список Способ хранения Непрерывная область памяти Разрозненная область памяти Расширение емкости Длина неизменяема Гибкое расширение Эффективность памяти Элементы занимают меньше памяти, но возможны потери пространства Элементы занимают больше памяти Доступ к элементу \\(O(1)\\) \\(O(n)\\) Добавление элемента \\(O(n)\\) \\(O(1)\\) Удаление элемента \\(O(n)\\) \\(O(1)\\)","path":["Глава 4. Массивы и списки","4.2   Связный список"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#423","level":2,"title":"4.2.3   Основные типы связных списков","text":"

        Как показано на рисунке 4-8, существует три распространенных типа связных списков.

        • Односвязный список: это обычный связный список, рассмотренный выше. Узел односвязного списка содержит значение и ссылку на следующий узел. Первый узел называется головным, последний - хвостовым, и хвост указывает на None .
        • Циклический список: если заставить хвостовой узел односвязного списка указывать на головной, то есть соединить хвост с головой, получится циклический список. В циклическом списке любой узел можно рассматривать как головной.
        • Двусвязный список: по сравнению с односвязным списком двусвязный хранит ссылки в двух направлениях. Определение узла двусвязного списка включает как ссылку на следующий узел, так и ссылку на предыдущий узел. По сравнению с односвязным списком двусвязный более гибок и позволяет обходить список в обе стороны, но за это приходится платить дополнительной памятью.
        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
        class ListNode:\n    \"\"\"Класс узла двусвязного списка\"\"\"\n    def __init__(self, val: int):\n        self.val: int = val                # Значение узла\n        self.next: ListNode | None = None  # Ссылка на следующий узел\n        self.prev: ListNode | None = None  # Ссылка на предыдущий узел\n
        /* Структура узла двусвязного списка */\nstruct ListNode {\n    int val;         // Значение узла\n    ListNode *next;  // Указатель на следующий узел\n    ListNode *prev;  // Указатель на предыдущий узел\n    ListNode(int x) : val(x), next(nullptr), prev(nullptr) {}  // Конструктор\n};\n
        /* Класс узла двусвязного списка */\nclass ListNode {\n    int val;        // Значение узла\n    ListNode next;  // Ссылка на следующий узел\n    ListNode prev;  // Ссылка на предыдущий узел\n    ListNode(int x) { val = x; }  // Конструктор\n}\n
        /* Класс узла двусвязного списка */\nclass ListNode(int x) {  // Конструктор\n    int val = x;    // Значение узла\n    ListNode next;  // Ссылка на следующий узел\n    ListNode prev;  // Ссылка на предыдущий узел\n}\n
        /* Структура узла двусвязного списка */\ntype DoublyListNode struct {\n    Val  int             // Значение узла\n    Next *DoublyListNode // Указатель на следующий узел\n    Prev *DoublyListNode // Указатель на предыдущий узел\n}\n\n// NewDoublyListNode Инициализация\nfunc NewDoublyListNode(val int) *DoublyListNode {\n    return &DoublyListNode{\n        Val:  val,\n        Next: nil,\n        Prev: nil,\n    }\n}\n
        /* Класс узла двусвязного списка */\nclass ListNode {\n    var val: Int // Значение узла\n    var next: ListNode? // Ссылка на следующий узел\n    var prev: ListNode? // Ссылка на предыдущий узел\n\n    init(x: Int) { // Конструктор\n        val = x\n    }\n}\n
        /* Класс узла двусвязного списка */\nclass ListNode {\n    constructor(val, next, prev) {\n        this.val = val  ===  undefined ? 0 : val;        // Значение узла\n        this.next = next  ===  undefined ? null : next;  // Ссылка на следующий узел\n        this.prev = prev  ===  undefined ? null : prev;  // Ссылка на предыдущий узел\n    }\n}\n
        /* Класс узла двусвязного списка */\nclass ListNode {\n    val: number;\n    next: ListNode | null;\n    prev: ListNode | null;\n    constructor(val?: number, next?: ListNode | null, prev?: ListNode | null) {\n        this.val = val  ===  undefined ? 0 : val;        // Значение узла\n        this.next = next  ===  undefined ? null : next;  // Ссылка на следующий узел\n        this.prev = prev  ===  undefined ? null : prev;  // Ссылка на предыдущий узел\n    }\n}\n
        /* Класс узла двусвязного списка */\nclass ListNode {\n    int val;        // Значение узла\n    ListNode? next;  // Ссылка на следующий узел\n    ListNode? prev;  // Ссылка на предыдущий узел\n    ListNode(this.val, [this.next, this.prev]);  // Конструктор\n}\n
        use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* Тип узла двусвязного списка */\n#[derive(Debug)]\nstruct ListNode {\n    val: i32, // Значение узла\n    next: Option<Rc<RefCell<ListNode>>>, // Указатель на следующий узел\n    prev: Option<Rc<RefCell<ListNode>>>, // Указатель на предыдущий узел\n}\n\n/* Конструктор */\nimpl ListNode {\n    fn new(val: i32) -> Self {\n        ListNode {\n            val,\n            next: None,\n            prev: None,\n        }\n    }\n}\n
        /* Структура узла двусвязного списка */\ntypedef struct ListNode {\n    int val;               // Значение узла\n    struct ListNode *next; // Указатель на следующий узел\n    struct ListNode *prev; // Указатель на предыдущий узел\n} ListNode;\n\n/* Конструктор */\nListNode *newListNode(int val) {\n    ListNode *node;\n    node = (ListNode *) malloc(sizeof(ListNode));\n    node->val = val;\n    node->next = NULL;\n    node->prev = NULL;\n    return node;\n}\n
        /* Класс узла двусвязного списка */\n// Конструктор\nclass ListNode(x: Int) {\n    val _val: Int = x           // Значение узла\n    val next: ListNode? = null  // Ссылка на следующий узел\n    val prev: ListNode? = null  // Ссылка на предыдущий узел\n}\n
        # Класс узла двусвязного списка\nclass ListNode\n  attr_accessor :val    # Значение узла\n  attr_accessor :next   # Ссылка на следующий узел\n  attr_accessor :prev   # Ссылка на предыдущий узел\n\n  def initialize(val=0, next_node=nil, prev_node=nil)\n    @val = val\n    @next = next_node\n    @prev = prev_node\n  end\nend\n

        Рисунок 4-8   Распространенные типы связных списков

        ","path":["Глава 4. Массивы и списки","4.2   Связный список"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#424","level":2,"title":"4.2.4   Типичные применения связных списков","text":"

        Односвязные списки обычно используются для реализации стеков, очередей, хеш-таблиц и графов.

        • Стеки и очереди: если операции вставки и удаления выполняются на одном конце связного списка, он проявляет свойства LIFO, соответствующие стеку. Если вставка происходит на одном конце, а удаление на другом, он проявляет свойства FIFO, соответствующие очереди.
        • Хеш-таблицы: метод цепочек - один из основных способов разрешения коллизий в хеш-таблицах. В этом подходе все конфликтующие элементы помещаются в связный список.
        • Графы: список смежности - это распространенный способ представления графа, при котором каждой вершине графа соответствует связный список, а каждый элемент этого списка представляет другую вершину, соединенную с данной.

        Двусвязные списки обычно используются там, где нужен быстрый доступ как к предыдущему, так и к следующему элементу.

        • Продвинутые структуры данных: например, в красно-черных деревьях и B-деревьях нам нужен доступ к родительскому узлу. Этого можно добиться, сохранив в узле ссылку на родителя, по аналогии с двусвязным списком.
        • История браузера: когда пользователь в браузере нажимает кнопки «вперед» или «назад», браузеру нужно знать предыдущую и следующую посещенные страницы. Свойства двусвязного списка делают такую операцию простой.
        • Алгоритм LRU: в алгоритмах вытеснения из кэша (LRU) нужно быстро находить наименее недавно использованные данные, а также быстро добавлять и удалять узлы. Для этого двусвязный список подходит очень хорошо.

        Циклические списки часто применяются в сценариях, требующих циклических операций, например при планировании ресурсов в операционной системе.

        • Алгоритм циклического распределения кванта времени: в операционных системах round-robin scheduling - это распространенный алгоритм планирования CPU, который циклически обходит набор процессов. Каждому процессу выделяется квант времени, и когда он исчерпан, CPU переключается на следующий процесс. Такую циклическую операцию удобно реализовать с помощью кольцевого списка.
        • Буферы данных: в некоторых реализациях буферов данных также могут использоваться циклические списки. Например, в аудио- и видеоплеерах поток данных может делиться на несколько буферных блоков и помещаться в кольцевой список для обеспечения непрерывного воспроизведения.
        ","path":["Глава 4. Массивы и списки","4.2   Связный список"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/","level":1,"title":"4.3   Список","text":"

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

        • Связный список естественным образом можно рассматривать как список: он поддерживает операции добавления, удаления, поиска и изменения элементов и может гибко расширяться динамически.
        • Массив тоже поддерживает операции добавления, удаления, поиска и изменения элементов, но из-за неизменяемости длины его можно считать лишь списком с ограниченной длиной.

        Когда список реализуется с помощью массива, неизменяемость длины снижает его практическую полезность. Причина в том, что мы обычно не можем заранее точно знать, сколько данных нужно хранить, а значит, трудно выбрать подходящую длину списка. Если длина слишком мала, она может не покрыть реальные потребности. Если слишком велика, будет зря расходоваться память.

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

        На практике списки из стандартных библиотек многих языков программирования реализованы именно на основе динамических массивов, например list в Python, ArrayList в Java, vector в C++ и List в C#. В дальнейшем обсуждении мы будем считать понятия «список» и «динамический массив» эквивалентными.

        ","path":["Глава 4. Массивы и списки","4.3   Список"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#431","level":2,"title":"4.3.1   Основные операции со списком","text":"","path":["Глава 4. Массивы и списки","4.3   Список"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#1","level":3,"title":"1.   Инициализация списка","text":"

        Обычно используются два способа инициализации: без начальных значений и с начальными значениями:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
        # Инициализация списка\n# Без начальных значений\nnums1: list[int] = []\n# С начальными значениями\nnums: list[int] = [1, 3, 2, 5, 4]\n
        list.cpp
        /* Инициализация списка */\n// Обрати внимание: в C++ vector соответствует описываемому здесь nums\n// Без начальных значений\nvector<int> nums1;\n// С начальными значениями\nvector<int> nums = { 1, 3, 2, 5, 4 };\n
        list.java
        /* Инициализация списка */\n// Без начальных значений\nList<Integer> nums1 = new ArrayList<>();\n// С начальными значениями (обрати внимание: элементы массива должны использовать обертку Integer[] вместо int[])\nInteger[] numbers = new Integer[] { 1, 3, 2, 5, 4 };\nList<Integer> nums = new ArrayList<>(Arrays.asList(numbers));\n
        list.cs
        /* Инициализация списка */\n// Без начальных значений\nList<int> nums1 = [];\n// С начальными значениями\nint[] numbers = [1, 3, 2, 5, 4];\nList<int> nums = [.. numbers];\n
        list_test.go
        /* Инициализация списка */\n// Без начальных значений\nnums1 := []int{}\n// С начальными значениями\nnums := []int{1, 3, 2, 5, 4}\n
        list.swift
        /* Инициализация списка */\n// Без начальных значений\nlet nums1: [Int] = []\n// С начальными значениями\nvar nums = [1, 3, 2, 5, 4]\n
        list.js
        /* Инициализация списка */\n// Без начальных значений\nconst nums1 = [];\n// С начальными значениями\nconst nums = [1, 3, 2, 5, 4];\n
        list.ts
        /* Инициализация списка */\n// Без начальных значений\nconst nums1: number[] = [];\n// С начальными значениями\nconst nums: number[] = [1, 3, 2, 5, 4];\n
        list.dart
        /* Инициализация списка */\n// Без начальных значений\nList<int> nums1 = [];\n// С начальными значениями\nList<int> nums = [1, 3, 2, 5, 4];\n
        list.rs
        /* Инициализация списка */\n// Без начальных значений\nlet nums1: Vec<i32> = Vec::new();\n// С начальными значениями\nlet nums: Vec<i32> = vec![1, 3, 2, 5, 4];\n
        list.c
        // В C нет встроенного динамического массива\n
        list.kt
        /* Инициализация списка */\n// Без начальных значений\nvar nums1 = listOf<Int>()\n// С начальными значениями\nvar numbers = arrayOf(1, 3, 2, 5, 4)\nvar nums = numbers.toMutableList()\n
        list.rb
        # Инициализация списка\n# Без начальных значений\nnums1 = []\n# С начальными значениями\nnums = [1, 3, 2, 5, 4]\n
        Визуализация выполнения

        https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%0A%20%20%20%20%23%20%D0%91%D0%B5%D0%B7%20%D0%BD%D0%B0%D1%87%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D1%85%20%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B9%0A%20%20%20%20nums1%20%3D%20%5B%5D%0A%20%20%20%20%23%20%D0%A1%20%D0%BD%D0%B0%D1%87%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%BC%D0%B8%20%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D1%8F%D0%BC%D0%B8%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%202%2C%205%2C%204%5D&cumulative=false&curInstr=4&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

        ","path":["Глава 4. Массивы и списки","4.3   Список"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#2","level":3,"title":"2.   Доступ к элементам","text":"

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

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
        # Доступ к элементу\nnum: int = nums[1]  # Доступ к элементу по индексу 1\n\n# Обновление элемента\nnums[1] = 0    # Обновить элемент по индексу 1 значением 0\n
        list.cpp
        /* Доступ к элементу */\nint num = nums[1];  // Доступ к элементу по индексу 1\n\n/* Обновление элемента */\nnums[1] = 0;  // Обновить элемент по индексу 1 значением 0\n
        list.java
        /* Доступ к элементу */\nint num = nums.get(1);  // Доступ к элементу по индексу 1\n\n/* Обновление элемента */\nnums.set(1, 0);  // Обновить элемент по индексу 1 значением 0\n
        list.cs
        /* Доступ к элементу */\nint num = nums[1];  // Доступ к элементу по индексу 1\n\n/* Обновление элемента */\nnums[1] = 0;  // Обновить элемент по индексу 1 значением 0\n
        list_test.go
        /* Доступ к элементу */\nnum := nums[1]  // Доступ к элементу по индексу 1\n\n/* Обновление элемента */\nnums[1] = 0     // Обновить элемент по индексу 1 значением 0\n
        list.swift
        /* Доступ к элементу */\nlet num = nums[1] // Доступ к элементу по индексу 1\n\n/* Обновление элемента */\nnums[1] = 0 // Обновить элемент по индексу 1 значением 0\n
        list.js
        /* Доступ к элементу */\nconst num = nums[1];  // Доступ к элементу по индексу 1\n\n/* Обновление элемента */\nnums[1] = 0;  // Обновить элемент по индексу 1 значением 0\n
        list.ts
        /* Доступ к элементу */\nconst num: number = nums[1];  // Доступ к элементу по индексу 1\n\n/* Обновление элемента */\nnums[1] = 0;  // Обновить элемент по индексу 1 значением 0\n
        list.dart
        /* Доступ к элементу */\nint num = nums[1];  // Доступ к элементу по индексу 1\n\n/* Обновление элемента */\nnums[1] = 0;  // Обновить элемент по индексу 1 значением 0\n
        list.rs
        /* Доступ к элементу */\nlet num: i32 = nums[1];  // Доступ к элементу по индексу 1\n/* Обновление элемента */\nnums[1] = 0;             // Обновить элемент по индексу 1 значением 0\n
        list.c
        // В C нет встроенного динамического массива\n
        list.kt
        /* Доступ к элементу */\nval num = nums[1]       // Доступ к элементу по индексу 1\n/* Обновление элемента */\nnums[1] = 0             // Обновить элемент по индексу 1 значением 0\n
        list.rb
        # Доступ к элементу\nnum = nums[1] # Доступ к элементу по индексу 1\n# Обновление элемента\nnums[1] = 0 # Обновить элемент по индексу 1 значением 0\n
        Визуализация выполнения

        https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%202%2C%205%2C%204%5D%0A%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B8%D1%82%D1%8C%20%D0%B4%D0%BE%D1%81%D1%82%D1%83%D0%BF%20%D0%BA%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D1%83%0A%20%20%20%20num%20%3D%20nums%5B1%5D%20%20%23%20%D0%BE%D0%B1%D1%80%D0%B0%D1%82%D0%B8%D1%82%D1%8C%D1%81%D1%8F%20%D0%BA%D0%B8%D0%BD%D0%B4%D0%B5%D0%BA%D1%81%201%20%D0%BF%D0%BE%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%0A%0A%20%20%20%20%23%20%D0%9E%D0%B1%D0%BD%D0%BE%D0%B2%D0%B8%D1%82%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%0A%20%20%20%20nums%5B1%5D%20%3D%200%20%20%20%20%23%20%D0%9E%D0%B1%D0%BD%D0%BE%D0%B2%D0%B8%D1%82%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%BF%D0%BE%20%D0%B8%D0%BD%D0%B4%D0%B5%D0%BA%D1%81%D1%83%201%20%D0%B4%D0%BE%200&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

        ","path":["Глава 4. Массивы и списки","4.3   Список"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#3","level":3,"title":"3.   Вставка и удаление элементов","text":"

        В отличие от массива список позволяет свободно добавлять и удалять элементы. Добавление элемента в конец списка имеет временную сложность \\(O(1)\\) , но операции вставки и удаления по-прежнему имеют ту же эффективность, что и у массива, то есть \\(O(n)\\) .

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
        # Очистить список\nnums.clear()\n\n# Добавить элементы в конец\nnums.append(1)\nnums.append(3)\nnums.append(2)\nnums.append(5)\nnums.append(4)\n\n# Вставить элемент в середину\nnums.insert(3, 6)  # Вставить число 6 по индексу 3\n\n# Удалить элемент\nnums.pop(3)        # Удалить элемент по индексу 3\n
        list.cpp
        /* Очистить список */\nnums.clear();\n\n/* Добавить элементы в конец */\nnums.push_back(1);\nnums.push_back(3);\nnums.push_back(2);\nnums.push_back(5);\nnums.push_back(4);\n\n/* Вставить элемент в середину */\nnums.insert(nums.begin() + 3, 6);  // Вставить число 6 по индексу 3\n\n/* Удалить элемент */\nnums.erase(nums.begin() + 3);      // Удалить элемент по индексу 3\n
        list.java
        /* Очистить список */\nnums.clear();\n\n/* Добавить элементы в конец */\nnums.add(1);\nnums.add(3);\nnums.add(2);\nnums.add(5);\nnums.add(4);\n\n/* Вставить элемент в середину */\nnums.add(3, 6);  // Вставить число 6 по индексу 3\n\n/* Удалить элемент */\nnums.remove(3);  // Удалить элемент по индексу 3\n
        list.cs
        /* Очистить список */\nnums.Clear();\n\n/* Добавить элементы в конец */\nnums.Add(1);\nnums.Add(3);\nnums.Add(2);\nnums.Add(5);\nnums.Add(4);\n\n/* Вставить элемент в середину */\nnums.Insert(3, 6);  // Вставить число 6 по индексу 3\n\n/* Удалить элемент */\nnums.RemoveAt(3);  // Удалить элемент по индексу 3\n
        list_test.go
        /* Очистить список */\nnums = nil\n\n/* Добавить элементы в конец */\nnums = append(nums, 1)\nnums = append(nums, 3)\nnums = append(nums, 2)\nnums = append(nums, 5)\nnums = append(nums, 4)\n\n/* Вставить элемент в середину */\nnums = append(nums[:3], append([]int{6}, nums[3:]...)...) // Вставить число 6 по индексу 3\n\n/* Удалить элемент */\nnums = append(nums[:3], nums[4:]...) // Удалить элемент по индексу 3\n
        list.swift
        /* Очистить список */\nnums.removeAll()\n\n/* Добавить элементы в конец */\nnums.append(1)\nnums.append(3)\nnums.append(2)\nnums.append(5)\nnums.append(4)\n\n/* Вставить элемент в середину */\nnums.insert(6, at: 3) // Вставить число 6 по индексу 3\n\n/* Удалить элемент */\nnums.remove(at: 3) // Удалить элемент по индексу 3\n
        list.js
        /* Очистить список */\nnums.length = 0;\n\n/* Добавить элементы в конец */\nnums.push(1);\nnums.push(3);\nnums.push(2);\nnums.push(5);\nnums.push(4);\n\n/* Вставить элемент в середину */\nnums.splice(3, 0, 6); // Вставить число 6 по индексу 3\n\n/* Удалить элемент */\nnums.splice(3, 1);  // Удалить элемент по индексу 3\n
        list.ts
        /* Очистить список */\nnums.length = 0;\n\n/* Добавить элементы в конец */\nnums.push(1);\nnums.push(3);\nnums.push(2);\nnums.push(5);\nnums.push(4);\n\n/* Вставить элемент в середину */\nnums.splice(3, 0, 6); // Вставить число 6 по индексу 3\n\n/* Удалить элемент */\nnums.splice(3, 1);  // Удалить элемент по индексу 3\n
        list.dart
        /* Очистить список */\nnums.clear();\n\n/* Добавить элементы в конец */\nnums.add(1);\nnums.add(3);\nnums.add(2);\nnums.add(5);\nnums.add(4);\n\n/* Вставить элемент в середину */\nnums.insert(3, 6); // Вставить число 6 по индексу 3\n\n/* Удалить элемент */\nnums.removeAt(3); // Удалить элемент по индексу 3\n
        list.rs
        /* Очистить список */\nnums.clear();\n\n/* Добавить элементы в конец */\nnums.push(1);\nnums.push(3);\nnums.push(2);\nnums.push(5);\nnums.push(4);\n\n/* Вставить элемент в середину */\nnums.insert(3, 6);  // Вставить число 6 по индексу 3\n\n/* Удалить элемент */\nnums.remove(3);    // Удалить элемент по индексу 3\n
        list.c
        // В C нет встроенного динамического массива\n
        list.kt
        /* Очистить список */\nnums.clear();\n\n/* Добавить элементы в конец */\nnums.add(1);\nnums.add(3);\nnums.add(2);\nnums.add(5);\nnums.add(4);\n\n/* Вставить элемент в середину */\nnums.add(3, 6);  // Вставить число 6 по индексу 3\n\n/* Удалить элемент */\nnums.remove(3);  // Удалить элемент по индексу 3\n
        list.rb
        # Очистить список\nnums.clear\n\n# Добавить элементы в конец\nnums << 1\nnums << 3\nnums << 2\nnums << 5\nnums << 4\n\n# Вставить элемент в середину\nnums.insert(3, 6) # Вставить число 6 по индексу 3\n\n# Удалить элемент\nnums.delete_at(3) # Удалить элемент по индексу 3\n
        Визуализация выполнения

        https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%A1%20%D0%BD%D0%B0%D1%87%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%BC%D0%B8%20%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D1%8F%D0%BC%D0%B8%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%202%2C%205%2C%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%9E%D1%87%D0%B8%D1%81%D1%82%D0%B8%D1%82%D1%8C%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%0A%20%20%20%20nums.clear%28%29%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%94%D0%BE%D0%B1%D0%B0%D0%B2%D0%B8%D1%82%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B2%20%D0%BA%D0%BE%D0%BD%D0%B5%D1%86%0A%20%20%20%20nums.append%281%29%0A%20%20%20%20nums.append%283%29%0A%20%20%20%20nums.append%282%29%0A%20%20%20%20nums.append%285%29%0A%20%20%20%20nums.append%284%29%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%92%D1%81%D1%82%D0%B0%D0%B2%D0%B8%D1%82%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B2%20%D1%81%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%D0%BD%D1%83%0A%20%20%20%20nums.insert%283%2C%206%29%20%20%23%20%D0%92%D1%81%D1%82%D0%B0%D0%B2%D0%B8%D1%82%D1%8C%20%D1%87%D0%B8%D1%81%D0%BB%D0%BE%206%20%D0%BF%D0%BE%20%D0%B8%D0%BD%D0%B4%D0%B5%D0%BA%D1%81%D1%83%203%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%A3%D0%B4%D0%B0%D0%BB%D0%B8%D1%82%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%0A%20%20%20%20nums.pop%283%29%20%20%20%20%20%20%20%20%23%20%D0%A3%D0%B4%D0%B0%D0%BB%D0%B8%D1%82%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%BF%D0%BE%20%D0%B8%D0%BD%D0%B4%D0%B5%D0%BA%D1%81%D1%83%203&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

        ","path":["Глава 4. Массивы и списки","4.3   Список"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#4","level":3,"title":"4.   Обход списка","text":"

        Как и массив, список можно обходить как по индексам, так и напрямую по элементам.

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
        # Обход списка по индексам\ncount = 0\nfor i in range(len(nums)):\n    count += nums[i]\n\n# Прямой обход элементов списка\nfor num in nums:\n    count += num\n
        list.cpp
        /* Обход списка по индексам */\nint count = 0;\nfor (int i = 0; i < nums.size(); i++) {\n    count += nums[i];\n}\n\n/* Прямой обход элементов списка */\ncount = 0;\nfor (int num : nums) {\n    count += num;\n}\n
        list.java
        /* Обход списка по индексам */\nint count = 0;\nfor (int i = 0; i < nums.size(); i++) {\n    count += nums.get(i);\n}\n\n/* Прямой обход элементов списка */\nfor (int num : nums) {\n    count += num;\n}\n
        list.cs
        /* Обход списка по индексам */\nint count = 0;\nfor (int i = 0; i < nums.Count; i++) {\n    count += nums[i];\n}\n\n/* Прямой обход элементов списка */\ncount = 0;\nforeach (int num in nums) {\n    count += num;\n}\n
        list_test.go
        /* Обход списка по индексам */\ncount := 0\nfor i := 0; i < len(nums); i++ {\n    count += nums[i]\n}\n\n/* Прямой обход элементов списка */\ncount = 0\nfor _, num := range nums {\n    count += num\n}\n
        list.swift
        /* Обход списка по индексам */\nvar count = 0\nfor i in nums.indices {\n    count += nums[i]\n}\n\n/* Прямой обход элементов списка */\ncount = 0\nfor num in nums {\n    count += num\n}\n
        list.js
        /* Обход списка по индексам */\nlet count = 0;\nfor (let i = 0; i < nums.length; i++) {\n    count += nums[i];\n}\n\n/* Прямой обход элементов списка */\ncount = 0;\nfor (const num of nums) {\n    count += num;\n}\n
        list.ts
        /* Обход списка по индексам */\nlet count = 0;\nfor (let i = 0; i < nums.length; i++) {\n    count += nums[i];\n}\n\n/* Прямой обход элементов списка */\ncount = 0;\nfor (const num of nums) {\n    count += num;\n}\n
        list.dart
        /* Обход списка по индексам */\nint count = 0;\nfor (var i = 0; i < nums.length; i++) {\n    count += nums[i];\n}\n\n/* Прямой обход элементов списка */\ncount = 0;\nfor (var num in nums) {\n    count += num;\n}\n
        list.rs
        // Обход списка по индексам\nlet mut _count = 0;\nfor i in 0..nums.len() {\n    _count += nums[i];\n}\n\n// Прямой обход элементов списка\n_count = 0;\nfor num in &nums {\n    _count += num;\n}\n
        list.c
        // В C нет встроенного динамического массива\n
        list.kt
        /* Обход списка по индексам */\nvar count = 0\nfor (i in nums.indices) {\n    count += nums[i]\n}\n\n/* Прямой обход элементов списка */\nfor (num in nums) {\n    count += num\n}\n
        list.rb
        # Обход списка по индексам\ncount = 0\nfor i in 0...nums.length\n    count += nums[i]\nend\n\n# Прямой обход элементов списка\ncount = 0\nfor num in nums\n    count += num\nend\n
        Визуализация выполнения

        https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%202%2C%205%2C%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%9E%D0%B1%D1%85%D0%BE%D0%B4%D0%B8%D1%82%D1%8C%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%20%D0%BF%D0%BE%20%D0%B8%D0%BD%D0%B4%D0%B5%D0%BA%D1%81%D0%B0%D0%BC%0A%20%20%20%20count%20%3D%200%0A%20%20%20%20for%20i%20in%20range%28len%28nums%29%29%3A%0A%20%20%20%20%20%20%20%20count%20%2B%3D%20nums%5Bi%5D%0A%0A%20%20%20%20%23%20%D0%9D%D0%B5%D0%BF%D0%BE%D1%81%D1%80%D0%B5%D0%B4%D1%81%D1%82%D0%B2%D0%B5%D0%BD%D0%BD%D0%BE%20%D0%BE%D0%B1%D1%85%D0%BE%D0%B4%D0%B8%D1%82%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D1%8B%20%D1%81%D0%BF%D0%B8%D1%81%D0%BA%D0%B0%0A%20%20%20%20for%20num%20in%20nums%3A%0A%20%20%20%20%20%20%20%20count%20%2B%3D%20num&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

        ","path":["Глава 4. Массивы и списки","4.3   Список"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#5","level":3,"title":"5.   Конкатенация списков","text":"

        Создав новый список nums1 , мы можем присоединить его к хвосту исходного списка.

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
        # Конкатенация двух списков\nnums1: list[int] = [6, 8, 7, 10, 9]\nnums += nums1  # Присоединить список nums1 к концу nums\n
        list.cpp
        /* Конкатенация двух списков */\nvector<int> nums1 = { 6, 8, 7, 10, 9 };\n// Присоединить список nums1 к концу nums\nnums.insert(nums.end(), nums1.begin(), nums1.end());\n
        list.java
        /* Конкатенация двух списков */\nList<Integer> nums1 = new ArrayList<>(Arrays.asList(new Integer[] { 6, 8, 7, 10, 9 }));\nnums.addAll(nums1);  // Присоединить список nums1 к концу nums\n
        list.cs
        /* Конкатенация двух списков */\nList<int> nums1 = [6, 8, 7, 10, 9];\nnums.AddRange(nums1);  // Присоединить список nums1 к концу nums\n
        list_test.go
        /* Конкатенация двух списков */\nnums1 := []int{6, 8, 7, 10, 9}\nnums = append(nums, nums1...)  // Присоединить список nums1 к концу nums\n
        list.swift
        /* Конкатенация двух списков */\nlet nums1 = [6, 8, 7, 10, 9]\nnums.append(contentsOf: nums1) // Присоединить список nums1 к концу nums\n
        list.js
        /* Конкатенация двух списков */\nconst nums1 = [6, 8, 7, 10, 9];\nnums.push(...nums1);  // Присоединить список nums1 к концу nums\n
        list.ts
        /* Конкатенация двух списков */\nconst nums1: number[] = [6, 8, 7, 10, 9];\nnums.push(...nums1);  // Присоединить список nums1 к концу nums\n
        list.dart
        /* Конкатенация двух списков */\nList<int> nums1 = [6, 8, 7, 10, 9];\nnums.addAll(nums1);  // Присоединить список nums1 к концу nums\n
        list.rs
        /* Конкатенация двух списков */\nlet nums1: Vec<i32> = vec![6, 8, 7, 10, 9];\nnums.extend(nums1);\n
        list.c
        // В C нет встроенного динамического массива\n
        list.kt
        /* Конкатенация двух списков */\nval nums1 = intArrayOf(6, 8, 7, 10, 9).toMutableList()\nnums.addAll(nums1)  // Присоединить список nums1 к концу nums\n
        list.rb
        # Конкатенация двух списков\nnums1 = [6, 8, 7, 10, 9]\nnums += nums1\n
        Визуализация выполнения

        https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%202%2C%205%2C%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%9E%D0%B1%D1%8A%D0%B5%D0%B4%D0%B8%D0%BD%D0%B8%D1%82%D1%8C%20%D0%B4%D0%B2%D0%B0%20%D1%81%D0%BF%D0%B8%D1%81%D0%BA%D0%B0%0A%20%20%20%20nums1%20%3D%20%5B6%2C%208%2C%207%2C%2010%2C%209%5D%0A%20%20%20%20nums%20%2B%3D%20nums1%20%20%23%20%D0%9F%D1%80%D0%B8%D1%81%D0%BE%D0%B5%D0%B4%D0%B8%D0%BD%D0%B8%D1%82%D1%8C%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%20nums1%20%D0%BA%20nums&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

        ","path":["Глава 4. Массивы и списки","4.3   Список"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#6","level":3,"title":"6.   Сортировка списка","text":"

        После сортировки списка мы сможем применять алгоритмы «двоичный поиск» и «два указателя», которые очень часто встречаются в задачах по массивам.

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
        # Отсортировать список\nnums.sort()  # После сортировки элементы списка идут по возрастанию\n
        list.cpp
        /* Отсортировать список */\nsort(nums.begin(), nums.end());  // После сортировки элементы списка идут по возрастанию\n
        list.java
        /* Отсортировать список */\nCollections.sort(nums);  // После сортировки элементы списка идут по возрастанию\n
        list.cs
        /* Отсортировать список */\nnums.Sort(); // После сортировки элементы списка идут по возрастанию\n
        list_test.go
        /* Отсортировать список */\nsort.Ints(nums)  // После сортировки элементы списка идут по возрастанию\n
        list.swift
        /* Отсортировать список */\nnums.sort() // После сортировки элементы списка идут по возрастанию\n
        list.js
        /* Отсортировать список */\nnums.sort((a, b) => a - b);  // После сортировки элементы списка идут по возрастанию\n
        list.ts
        /* Отсортировать список */\nnums.sort((a, b) => a - b);  // После сортировки элементы списка идут по возрастанию\n
        list.dart
        /* Отсортировать список */\nnums.sort(); // После сортировки элементы списка идут по возрастанию\n
        list.rs
        /* Отсортировать список */\nnums.sort(); // После сортировки элементы списка идут по возрастанию\n
        list.c
        // В C нет встроенного динамического массива\n
        list.kt
        /* Отсортировать список */\nnums.sort() // После сортировки элементы списка идут по возрастанию\n
        list.rb
        # Отсортировать список\nnums = nums.sort { |a, b| a <=> b } # После сортировки элементы списка идут по возрастанию\n
        Визуализация выполнения

        https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%202%2C%205%2C%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%9E%D1%82%D1%81%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%0A%20%20%20%20nums.sort%28%29%20%20%23%20%D0%A1%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D0%B0%D0%BF%D0%BE%D1%81%D0%BB%D0%B5%2C%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D1%80%D0%B0%D1%81%D0%BF%D0%BE%D0%BB%D0%BE%D0%B6%D0%B5%D0%BD%D1%8B%20%D0%BF%D0%BE%20%D0%B2%D0%BE%D0%B7%D1%80%D0%B0%D1%81%D1%82%D0%B0%D0%BD%D0%B8%D1%8E&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

        ","path":["Глава 4. Массивы и списки","4.3   Список"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#432","level":2,"title":"4.3.2   Реализация списка","text":"

        Во многих языках программирования списки встроены в стандартную библиотеку, например в Java, C++, Python и других языках. Их реализация довольно сложна, а настройки параметров тщательно продуманы: начальная емкость, коэффициент расширения и так далее. Если это интересно, стоит заглянуть в исходный код.

        Чтобы лучше понять принцип работы списка, попробуем реализовать его упрощенную версию, в которой есть три ключевых аспекта проектирования.

        • Начальная емкость: выбрать разумную начальную емкость внутреннего массива. В этом примере мы берем 10.
        • Учет количества элементов: объявить переменную size , которая будет хранить текущее число элементов в списке и обновляться в реальном времени при вставке и удалении элементов. С помощью этой переменной можно определять конец списка и понимать, требуется ли расширение.
        • Механизм расширения: если при вставке элементов емкость списка исчерпана, нужно выполнить расширение. Для этого сначала создается больший массив с учетом коэффициента расширения, а затем все элементы текущего массива по порядку переносятся в новый. В этом примере мы считаем, что каждый раз массив расширяется в 2 раза.
        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_list.py
        class MyList:\n    \"\"\"Класс списка\"\"\"\n\n    def __init__(self):\n        \"\"\"Конструктор\"\"\"\n        self._capacity: int = 10  # Вместимость списка\n        self._arr: list[int] = [0] * self._capacity  # Массив (для хранения элементов списка)\n        self._size: int = 0  # Длина списка (текущее число элементов)\n        self._extend_ratio: int = 2  # Коэффициент увеличения списка при каждом расширении\n\n    def size(self) -> int:\n        \"\"\"Получить длину списка (текущее число элементов)\"\"\"\n        return self._size\n\n    def capacity(self) -> int:\n        \"\"\"Получить вместимость списка\"\"\"\n        return self._capacity\n\n    def get(self, index: int) -> int:\n        \"\"\"Доступ к элементу\"\"\"\n        # Если индекс выходит за границы, выбрасывается исключение; далее аналогично\n        if index < 0 or index >= self._size:\n            raise IndexError(\"индекс выходит за границы\")\n        return self._arr[index]\n\n    def set(self, num: int, index: int):\n        \"\"\"Обновление элемента\"\"\"\n        if index < 0 or index >= self._size:\n            raise IndexError(\"индекс выходит за границы\")\n        self._arr[index] = num\n\n    def add(self, num: int):\n        \"\"\"Добавление элемента в конец\"\"\"\n        # При превышении вместимости по числу элементов запускается расширение\n        if self.size() == self.capacity():\n            self.extend_capacity()\n        self._arr[self._size] = num\n        self._size += 1\n\n    def insert(self, num: int, index: int):\n        \"\"\"Вставка элемента в середину\"\"\"\n        if index < 0 or index >= self._size:\n            raise IndexError(\"индекс выходит за границы\")\n        # При превышении вместимости по числу элементов запускается расширение\n        if self._size == self.capacity():\n            self.extend_capacity()\n        # Сдвинуть элемент с индексом index и все следующие элементы на одну позицию назад\n        for j in range(self._size - 1, index - 1, -1):\n            self._arr[j + 1] = self._arr[j]\n        self._arr[index] = num\n        # Обновить число элементов\n        self._size += 1\n\n    def remove(self, index: int) -> int:\n        \"\"\"Удаление элемента\"\"\"\n        if index < 0 or index >= self._size:\n            raise IndexError(\"индекс выходит за границы\")\n        num = self._arr[index]\n        # Сдвинуть все элементы после индекса index на одну позицию вперед\n        for j in range(index, self._size - 1):\n            self._arr[j] = self._arr[j + 1]\n        # Обновить число элементов\n        self._size -= 1\n        # Вернуть удаленный элемент\n        return num\n\n    def extend_capacity(self):\n        \"\"\"Расширение списка\"\"\"\n        # Создать новый массив длиной в _extend_ratio раз больше исходного массива и скопировать в него исходный массив\n        self._arr = self._arr + [0] * self.capacity() * (self._extend_ratio - 1)\n        # Обновить вместимость списка\n        self._capacity = len(self._arr)\n\n    def to_array(self) -> list[int]:\n        \"\"\"Вернуть список фактической длины\"\"\"\n        return self._arr[: self._size]\n
        my_list.cpp
        /* Класс списка */\nclass MyList {\n  private:\n    int *arr;             // Массив (для хранения элементов списка)\n    int arrCapacity = 10; // Вместимость списка\n    int arrSize = 0;      // Длина списка (текущее число элементов)\n    int extendRatio = 2;   // Коэффициент увеличения списка при каждом расширении\n\n  public:\n    /* Конструктор */\n    MyList() {\n        arr = new int[arrCapacity];\n    }\n\n    /* Метод-деструктор */\n    ~MyList() {\n        delete[] arr;\n    }\n\n    /* Получить длину списка (текущее число элементов) */\n    int size() {\n        return arrSize;\n    }\n\n    /* Получить вместимость списка */\n    int capacity() {\n        return arrCapacity;\n    }\n\n    /* Доступ к элементу */\n    int get(int index) {\n        // Если индекс выходит за границы, выбрасывается исключение; далее аналогично\n        if (index < 0 || index >= size())\n            throw out_of_range(\"индекс выходит за границы\");\n        return arr[index];\n    }\n\n    /* Обновление элемента */\n    void set(int index, int num) {\n        if (index < 0 || index >= size())\n            throw out_of_range(\"индекс выходит за границы\");\n        arr[index] = num;\n    }\n\n    /* Добавление элемента в конец */\n    void add(int num) {\n        // При превышении вместимости по числу элементов запускается расширение\n        if (size() == capacity())\n            extendCapacity();\n        arr[size()] = num;\n        // Обновить число элементов\n        arrSize++;\n    }\n\n    /* Вставка элемента в середину */\n    void insert(int index, int num) {\n        if (index < 0 || index >= size())\n            throw out_of_range(\"индекс выходит за границы\");\n        // При превышении вместимости по числу элементов запускается расширение\n        if (size() == capacity())\n            extendCapacity();\n        // Сдвинуть элемент с индексом index и все следующие элементы на одну позицию назад\n        for (int j = size() - 1; j >= index; j--) {\n            arr[j + 1] = arr[j];\n        }\n        arr[index] = num;\n        // Обновить число элементов\n        arrSize++;\n    }\n\n    /* Удаление элемента */\n    int remove(int index) {\n        if (index < 0 || index >= size())\n            throw out_of_range(\"индекс выходит за границы\");\n        int num = arr[index];\n        // Сдвинуть все элементы после индекса index на одну позицию вперед\n        for (int j = index; j < size() - 1; j++) {\n            arr[j] = arr[j + 1];\n        }\n        // Обновить число элементов\n        arrSize--;\n        // Вернуть удаленный элемент\n        return num;\n    }\n\n    /* Расширение списка */\n    void extendCapacity() {\n        // Создать новый массив длиной в extendRatio раз больше исходного массива\n        int newCapacity = capacity() * extendRatio;\n        int *tmp = arr;\n        arr = new int[newCapacity];\n        // Скопировать все элементы исходного массива в новый массив\n        for (int i = 0; i < size(); i++) {\n            arr[i] = tmp[i];\n        }\n        // Освободить память\n        delete[] tmp;\n        arrCapacity = newCapacity;\n    }\n\n    /* Преобразовать список в Vector для вывода */\n    vector<int> toVector() {\n        // Преобразовывать только элементы списка в пределах фактической длины\n        vector<int> vec(size());\n        for (int i = 0; i < size(); i++) {\n            vec[i] = arr[i];\n        }\n        return vec;\n    }\n};\n
        my_list.java
        /* Класс списка */\nclass MyList {\n    private int[] arr; // Массив (для хранения элементов списка)\n    private int capacity = 10; // Вместимость списка\n    private int size = 0; // Длина списка (текущее число элементов)\n    private int extendRatio = 2; // Коэффициент увеличения списка при каждом расширении\n\n    /* Конструктор */\n    public MyList() {\n        arr = new int[capacity];\n    }\n\n    /* Получить длину списка (текущее число элементов) */\n    public int size() {\n        return size;\n    }\n\n    /* Получить вместимость списка */\n    public int capacity() {\n        return capacity;\n    }\n\n    /* Доступ к элементу */\n    public int get(int index) {\n        // Если индекс выходит за границы, выбрасывается исключение; далее аналогично\n        if (index < 0 || index >= size)\n            throw new IndexOutOfBoundsException(\"индекс выходит за границы\");\n        return arr[index];\n    }\n\n    /* Обновление элемента */\n    public void set(int index, int num) {\n        if (index < 0 || index >= size)\n            throw new IndexOutOfBoundsException(\"индекс выходит за границы\");\n        arr[index] = num;\n    }\n\n    /* Добавление элемента в конец */\n    public void add(int num) {\n        // При превышении вместимости по числу элементов запускается расширение\n        if (size == capacity())\n            extendCapacity();\n        arr[size] = num;\n        // Обновить число элементов\n        size++;\n    }\n\n    /* Вставка элемента в середину */\n    public void insert(int index, int num) {\n        if (index < 0 || index >= size)\n            throw new IndexOutOfBoundsException(\"индекс выходит за границы\");\n        // При превышении вместимости по числу элементов запускается расширение\n        if (size == capacity())\n            extendCapacity();\n        // Сдвинуть элемент с индексом index и все следующие элементы на одну позицию назад\n        for (int j = size - 1; j >= index; j--) {\n            arr[j + 1] = arr[j];\n        }\n        arr[index] = num;\n        // Обновить число элементов\n        size++;\n    }\n\n    /* Удаление элемента */\n    public int remove(int index) {\n        if (index < 0 || index >= size)\n            throw new IndexOutOfBoundsException(\"индекс выходит за границы\");\n        int num = arr[index];\n        // Сдвинуть все элементы после индекса index на одну позицию вперед\n        for (int j = index; j < size - 1; j++) {\n            arr[j] = arr[j + 1];\n        }\n        // Обновить число элементов\n        size--;\n        // Вернуть удаленный элемент\n        return num;\n    }\n\n    /* Расширение списка */\n    public void extendCapacity() {\n        // Создать новый массив длиной в extendRatio раз больше исходного и скопировать в него исходный массив\n        arr = Arrays.copyOf(arr, capacity() * extendRatio);\n        // Обновить вместимость списка\n        capacity = arr.length;\n    }\n\n    /* Преобразовать список в массив */\n    public int[] toArray() {\n        int size = size();\n        // Преобразовывать только элементы списка в пределах фактической длины\n        int[] arr = new int[size];\n        for (int i = 0; i < size; i++) {\n            arr[i] = get(i);\n        }\n        return arr;\n    }\n}\n
        my_list.cs
        /* Класс списка */\nclass MyList {\n    private int[] arr;           // Массив (для хранения элементов списка)\n    private int arrCapacity = 10;    // Вместимость списка\n    private int arrSize = 0;         // Длина списка (текущее число элементов)\n    private readonly int extendRatio = 2;  // Коэффициент увеличения списка при каждом расширении\n\n    /* Конструктор */\n    public MyList() {\n        arr = new int[arrCapacity];\n    }\n\n    /* Получить длину списка (текущее число элементов) */\n    public int Size() {\n        return arrSize;\n    }\n\n    /* Получить вместимость списка */\n    public int Capacity() {\n        return arrCapacity;\n    }\n\n    /* Доступ к элементу */\n    public int Get(int index) {\n        // Если индекс выходит за границы, выбрасывается исключение; далее аналогично\n        if (index < 0 || index >= arrSize)\n            throw new IndexOutOfRangeException(\"индекс выходит за границы\");\n        return arr[index];\n    }\n\n    /* Обновление элемента */\n    public void Set(int index, int num) {\n        if (index < 0 || index >= arrSize)\n            throw new IndexOutOfRangeException(\"индекс выходит за границы\");\n        arr[index] = num;\n    }\n\n    /* Добавление элемента в конец */\n    public void Add(int num) {\n        // При превышении вместимости по числу элементов запускается расширение\n        if (arrSize == arrCapacity)\n            ExtendCapacity();\n        arr[arrSize] = num;\n        // Обновить число элементов\n        arrSize++;\n    }\n\n    /* Вставка элемента в середину */\n    public void Insert(int index, int num) {\n        if (index < 0 || index >= arrSize)\n            throw new IndexOutOfRangeException(\"индекс выходит за границы\");\n        // При превышении вместимости по числу элементов запускается расширение\n        if (arrSize == arrCapacity)\n            ExtendCapacity();\n        // Сдвинуть элемент с индексом index и все следующие элементы на одну позицию назад\n        for (int j = arrSize - 1; j >= index; j--) {\n            arr[j + 1] = arr[j];\n        }\n        arr[index] = num;\n        // Обновить число элементов\n        arrSize++;\n    }\n\n    /* Удаление элемента */\n    public int Remove(int index) {\n        if (index < 0 || index >= arrSize)\n            throw new IndexOutOfRangeException(\"индекс выходит за границы\");\n        int num = arr[index];\n        // Сдвинуть все элементы после индекса index на одну позицию вперед\n        for (int j = index; j < arrSize - 1; j++) {\n            arr[j] = arr[j + 1];\n        }\n        // Обновить число элементов\n        arrSize--;\n        // Вернуть удаленный элемент\n        return num;\n    }\n\n    /* Расширение списка */\n    public void ExtendCapacity() {\n        // Создать новый массив длиной arrCapacity * extendRatio и скопировать в него исходный массив\n        Array.Resize(ref arr, arrCapacity * extendRatio);\n        // Обновить вместимость списка\n        arrCapacity = arr.Length;\n    }\n\n    /* Преобразовать список в массив */\n    public int[] ToArray() {\n        // Преобразовывать только элементы списка в пределах фактической длины\n        int[] arr = new int[arrSize];\n        for (int i = 0; i < arrSize; i++) {\n            arr[i] = Get(i);\n        }\n        return arr;\n    }\n}\n
        my_list.go
        /* Класс списка */\ntype myList struct {\n    arrCapacity int\n    arr         []int\n    arrSize     int\n    extendRatio int\n}\n\n/* Конструктор */\nfunc newMyList() *myList {\n    return &myList{\n        arrCapacity: 10,              // Вместимость списка\n        arr:         make([]int, 10), // Массив (для хранения элементов списка)\n        arrSize:     0,               // Длина списка (текущее число элементов)\n        extendRatio: 2,               // Коэффициент увеличения списка при каждом расширении\n    }\n}\n\n/* Получить длину списка (текущее число элементов) */\nfunc (l *myList) size() int {\n    return l.arrSize\n}\n\n/* Получить вместимость списка */\nfunc (l *myList) capacity() int {\n    return l.arrCapacity\n}\n\n/* Доступ к элементу */\nfunc (l *myList) get(index int) int {\n    // Если индекс выходит за границы, выбрасывается исключение; далее аналогично\n    if index < 0 || index >= l.arrSize {\n        panic(\"индекс выходит за границы\")\n    }\n    return l.arr[index]\n}\n\n/* Обновление элемента */\nfunc (l *myList) set(num, index int) {\n    if index < 0 || index >= l.arrSize {\n        panic(\"индекс выходит за границы\")\n    }\n    l.arr[index] = num\n}\n\n/* Добавление элемента в конец */\nfunc (l *myList) add(num int) {\n    // При превышении вместимости по числу элементов запускается расширение\n    if l.arrSize == l.arrCapacity {\n        l.extendCapacity()\n    }\n    l.arr[l.arrSize] = num\n    // Обновить число элементов\n    l.arrSize++\n}\n\n/* Вставка элемента в середину */\nfunc (l *myList) insert(num, index int) {\n    if index < 0 || index >= l.arrSize {\n        panic(\"индекс выходит за границы\")\n    }\n    // При превышении вместимости по числу элементов запускается расширение\n    if l.arrSize == l.arrCapacity {\n        l.extendCapacity()\n    }\n    // Сдвинуть элемент с индексом index и все следующие элементы на одну позицию назад\n    for j := l.arrSize - 1; j >= index; j-- {\n        l.arr[j+1] = l.arr[j]\n    }\n    l.arr[index] = num\n    // Обновить число элементов\n    l.arrSize++\n}\n\n/* Удаление элемента */\nfunc (l *myList) remove(index int) int {\n    if index < 0 || index >= l.arrSize {\n        panic(\"индекс выходит за границы\")\n    }\n    num := l.arr[index]\n    // Сдвинуть все элементы после индекса index на одну позицию вперед\n    for j := index; j < l.arrSize-1; j++ {\n        l.arr[j] = l.arr[j+1]\n    }\n    // Обновить число элементов\n    l.arrSize--\n    // Вернуть удаленный элемент\n    return num\n}\n\n/* Расширение списка */\nfunc (l *myList) extendCapacity() {\n    // Создать новый массив длиной в extendRatio раз больше исходного и скопировать в него исходный массив\n    l.arr = append(l.arr, make([]int, l.arrCapacity*(l.extendRatio-1))...)\n    // Обновить вместимость списка\n    l.arrCapacity = len(l.arr)\n}\n\n/* Вернуть список фактической длины */\nfunc (l *myList) toArray() []int {\n    // Преобразовывать только элементы списка в пределах фактической длины\n    return l.arr[:l.arrSize]\n}\n
        my_list.swift
        /* Класс списка */\nclass MyList {\n    private var arr: [Int] // Массив (для хранения элементов списка)\n    private var _capacity: Int // Вместимость списка\n    private var _size: Int // Длина списка (текущее число элементов)\n    private let extendRatio: Int // Коэффициент увеличения списка при каждом расширении\n\n    /* Конструктор */\n    init() {\n        _capacity = 10\n        _size = 0\n        extendRatio = 2\n        arr = Array(repeating: 0, count: _capacity)\n    }\n\n    /* Получить длину списка (текущее число элементов) */\n    func size() -> Int {\n        _size\n    }\n\n    /* Получить вместимость списка */\n    func capacity() -> Int {\n        _capacity\n    }\n\n    /* Доступ к элементу */\n    func get(index: Int) -> Int {\n        // Если индекс выходит за границы, выбросить ошибку; далее аналогично\n        if index < 0 || index >= size() {\n            fatalError(\"индекс выходит за границы\")\n        }\n        return arr[index]\n    }\n\n    /* Обновление элемента */\n    func set(index: Int, num: Int) {\n        if index < 0 || index >= size() {\n            fatalError(\"индекс выходит за границы\")\n        }\n        arr[index] = num\n    }\n\n    /* Добавление элемента в конец */\n    func add(num: Int) {\n        // При превышении вместимости по числу элементов запускается расширение\n        if size() == capacity() {\n            extendCapacity()\n        }\n        arr[size()] = num\n        // Обновить число элементов\n        _size += 1\n    }\n\n    /* Вставка элемента в середину */\n    func insert(index: Int, num: Int) {\n        if index < 0 || index >= size() {\n            fatalError(\"индекс выходит за границы\")\n        }\n        // При превышении вместимости по числу элементов запускается расширение\n        if size() == capacity() {\n            extendCapacity()\n        }\n        // Сдвинуть элемент с индексом index и все следующие элементы на одну позицию назад\n        for j in (index ..< size()).reversed() {\n            arr[j + 1] = arr[j]\n        }\n        arr[index] = num\n        // Обновить число элементов\n        _size += 1\n    }\n\n    /* Удаление элемента */\n    @discardableResult\n    func remove(index: Int) -> Int {\n        if index < 0 || index >= size() {\n            fatalError(\"индекс выходит за границы\")\n        }\n        let num = arr[index]\n        // Сдвинуть все элементы после индекса index на одну позицию вперед\n        for j in index ..< (size() - 1) {\n            arr[j] = arr[j + 1]\n        }\n        // Обновить число элементов\n        _size -= 1\n        // Вернуть удаленный элемент\n        return num\n    }\n\n    /* Расширение списка */\n    func extendCapacity() {\n        // Создать новый массив длиной в extendRatio раз больше исходного и скопировать в него исходный массив\n        arr = arr + Array(repeating: 0, count: capacity() * (extendRatio - 1))\n        // Обновить вместимость списка\n        _capacity = arr.count\n    }\n\n    /* Преобразовать список в массив */\n    func toArray() -> [Int] {\n        Array(arr.prefix(size()))\n    }\n}\n
        my_list.js
        /* Класс списка */\nclass MyList {\n    #arr = new Array(); // Массив (для хранения элементов списка)\n    #capacity = 10; // Вместимость списка\n    #size = 0; // Длина списка (текущее число элементов)\n    #extendRatio = 2; // Коэффициент увеличения списка при каждом расширении\n\n    /* Конструктор */\n    constructor() {\n        this.#arr = new Array(this.#capacity);\n    }\n\n    /* Получить длину списка (текущее число элементов) */\n    size() {\n        return this.#size;\n    }\n\n    /* Получить вместимость списка */\n    capacity() {\n        return this.#capacity;\n    }\n\n    /* Доступ к элементу */\n    get(index) {\n        // Если индекс выходит за границы, выбрасывается исключение; далее аналогично\n        if (index < 0 || index >= this.#size) throw new Error('индекс выходит за границы');\n        return this.#arr[index];\n    }\n\n    /* Обновление элемента */\n    set(index, num) {\n        if (index < 0 || index >= this.#size) throw new Error('индекс выходит за границы');\n        this.#arr[index] = num;\n    }\n\n    /* Добавление элемента в конец */\n    add(num) {\n        // Если длина равна вместимости, требуется расширение\n        if (this.#size === this.#capacity) {\n            this.extendCapacity();\n        }\n        // Добавить новый элемент в конец списка\n        this.#arr[this.#size] = num;\n        this.#size++;\n    }\n\n    /* Вставка элемента в середину */\n    insert(index, num) {\n        if (index < 0 || index >= this.#size) throw new Error('индекс выходит за границы');\n        // При превышении вместимости по числу элементов запускается расширение\n        if (this.#size === this.#capacity) {\n            this.extendCapacity();\n        }\n        // Сдвинуть элемент с индексом index и все следующие элементы на одну позицию назад\n        for (let j = this.#size - 1; j >= index; j--) {\n            this.#arr[j + 1] = this.#arr[j];\n        }\n        // Обновить число элементов\n        this.#arr[index] = num;\n        this.#size++;\n    }\n\n    /* Удаление элемента */\n    remove(index) {\n        if (index < 0 || index >= this.#size) throw new Error('индекс выходит за границы');\n        let num = this.#arr[index];\n        // Сдвинуть все элементы после индекса index на одну позицию вперед\n        for (let j = index; j < this.#size - 1; j++) {\n            this.#arr[j] = this.#arr[j + 1];\n        }\n        // Обновить число элементов\n        this.#size--;\n        // Вернуть удаленный элемент\n        return num;\n    }\n\n    /* Расширение списка */\n    extendCapacity() {\n        // Создать новый массив длиной в extendRatio раз больше исходного и скопировать в него исходный массив\n        this.#arr = this.#arr.concat(\n            new Array(this.capacity() * (this.#extendRatio - 1))\n        );\n        // Обновить вместимость списка\n        this.#capacity = this.#arr.length;\n    }\n\n    /* Преобразовать список в массив */\n    toArray() {\n        let size = this.size();\n        // Преобразовывать только элементы списка в пределах фактической длины\n        const arr = new Array(size);\n        for (let i = 0; i < size; i++) {\n            arr[i] = this.get(i);\n        }\n        return arr;\n    }\n}\n
        my_list.ts
        /* Класс списка */\nclass MyList {\n    private arr: Array<number>; // Массив (для хранения элементов списка)\n    private _capacity: number = 10; // Вместимость списка\n    private _size: number = 0; // Длина списка (текущее число элементов)\n    private extendRatio: number = 2; // Коэффициент увеличения списка при каждом расширении\n\n    /* Конструктор */\n    constructor() {\n        this.arr = new Array(this._capacity);\n    }\n\n    /* Получить длину списка (текущее число элементов) */\n    public size(): number {\n        return this._size;\n    }\n\n    /* Получить вместимость списка */\n    public capacity(): number {\n        return this._capacity;\n    }\n\n    /* Доступ к элементу */\n    public get(index: number): number {\n        // Если индекс выходит за границы, выбрасывается исключение; далее аналогично\n        if (index < 0 || index >= this._size) throw new Error('индекс выходит за границы');\n        return this.arr[index];\n    }\n\n    /* Обновление элемента */\n    public set(index: number, num: number): void {\n        if (index < 0 || index >= this._size) throw new Error('индекс выходит за границы');\n        this.arr[index] = num;\n    }\n\n    /* Добавление элемента в конец */\n    public add(num: number): void {\n        // Если длина равна вместимости, требуется расширение\n        if (this._size === this._capacity) this.extendCapacity();\n        // Добавить новый элемент в конец списка\n        this.arr[this._size] = num;\n        this._size++;\n    }\n\n    /* Вставка элемента в середину */\n    public insert(index: number, num: number): void {\n        if (index < 0 || index >= this._size) throw new Error('индекс выходит за границы');\n        // При превышении вместимости по числу элементов запускается расширение\n        if (this._size === this._capacity) {\n            this.extendCapacity();\n        }\n        // Сдвинуть элемент с индексом index и все следующие элементы на одну позицию назад\n        for (let j = this._size - 1; j >= index; j--) {\n            this.arr[j + 1] = this.arr[j];\n        }\n        // Обновить число элементов\n        this.arr[index] = num;\n        this._size++;\n    }\n\n    /* Удаление элемента */\n    public remove(index: number): number {\n        if (index < 0 || index >= this._size) throw new Error('индекс выходит за границы');\n        let num = this.arr[index];\n        // Сдвинуть все элементы после индекса index на одну позицию вперед\n        for (let j = index; j < this._size - 1; j++) {\n            this.arr[j] = this.arr[j + 1];\n        }\n        // Обновить число элементов\n        this._size--;\n        // Вернуть удаленный элемент\n        return num;\n    }\n\n    /* Расширение списка */\n    public extendCapacity(): void {\n        // Создать новый массив длиной size и скопировать в него исходный массив\n        this.arr = this.arr.concat(\n            new Array(this.capacity() * (this.extendRatio - 1))\n        );\n        // Обновить вместимость списка\n        this._capacity = this.arr.length;\n    }\n\n    /* Преобразовать список в массив */\n    public toArray(): number[] {\n        let size = this.size();\n        // Преобразовывать только элементы списка в пределах фактической длины\n        const arr = new Array(size);\n        for (let i = 0; i < size; i++) {\n            arr[i] = this.get(i);\n        }\n        return arr;\n    }\n}\n
        my_list.dart
        /* Класс списка */\nclass MyList {\n  late List<int> _arr; // Массив (для хранения элементов списка)\n  int _capacity = 10; // Вместимость списка\n  int _size = 0; // Длина списка (текущее число элементов)\n  int _extendRatio = 2; // Коэффициент увеличения списка при каждом расширении\n\n  /* Конструктор */\n  MyList() {\n    _arr = List.filled(_capacity, 0);\n  }\n\n  /* Получить длину списка (текущее число элементов) */\n  int size() => _size;\n\n  /* Получить вместимость списка */\n  int capacity() => _capacity;\n\n  /* Доступ к элементу */\n  int get(int index) {\n    if (index >= _size) throw RangeError('индекс выходит за границы');\n    return _arr[index];\n  }\n\n  /* Обновление элемента */\n  void set(int index, int _num) {\n    if (index >= _size) throw RangeError('индекс выходит за границы');\n    _arr[index] = _num;\n  }\n\n  /* Добавление элемента в конец */\n  void add(int _num) {\n    // При превышении вместимости по числу элементов запускается расширение\n    if (_size == _capacity) extendCapacity();\n    _arr[_size] = _num;\n    // Обновить число элементов\n    _size++;\n  }\n\n  /* Вставка элемента в середину */\n  void insert(int index, int _num) {\n    if (index >= _size) throw RangeError('индекс выходит за границы');\n    // При превышении вместимости по числу элементов запускается расширение\n    if (_size == _capacity) extendCapacity();\n    // Сдвинуть элемент с индексом index и все следующие элементы на одну позицию назад\n    for (var j = _size - 1; j >= index; j--) {\n      _arr[j + 1] = _arr[j];\n    }\n    _arr[index] = _num;\n    // Обновить число элементов\n    _size++;\n  }\n\n  /* Удаление элемента */\n  int remove(int index) {\n    if (index >= _size) throw RangeError('индекс выходит за границы');\n    int _num = _arr[index];\n    // Сдвинуть все элементы после индекса index на одну позицию вперед\n    for (var j = index; j < _size - 1; j++) {\n      _arr[j] = _arr[j + 1];\n    }\n    // Обновить число элементов\n    _size--;\n    // Вернуть удаленный элемент\n    return _num;\n  }\n\n  /* Расширение списка */\n  void extendCapacity() {\n    // Создать новый массив длиной в _extendRatio раз больше исходного массива\n    final _newNums = List.filled(_capacity * _extendRatio, 0);\n    // Скопировать исходный массив в новый массив\n    List.copyRange(_newNums, 0, _arr);\n    // Обновить ссылку на _arr\n    _arr = _newNums;\n    // Обновить вместимость списка\n    _capacity = _arr.length;\n  }\n\n  /* Преобразовать список в массив */\n  List<int> toArray() {\n    List<int> arr = [];\n    for (var i = 0; i < _size; i++) {\n      arr.add(get(i));\n    }\n    return arr;\n  }\n}\n
        my_list.rs
        /* Класс списка */\n#[allow(dead_code)]\nstruct MyList {\n    arr: Vec<i32>,       // Массив (для хранения элементов списка)\n    capacity: usize,     // Вместимость списка\n    size: usize,         // Длина списка (текущее число элементов)\n    extend_ratio: usize, // Коэффициент увеличения списка при каждом расширении\n}\n\n#[allow(unused, unused_comparisons)]\nimpl MyList {\n    /* Конструктор */\n    pub fn new(capacity: usize) -> Self {\n        let mut vec = vec![0; capacity];\n        Self {\n            arr: vec,\n            capacity,\n            size: 0,\n            extend_ratio: 2,\n        }\n    }\n\n    /* Получить длину списка (текущее число элементов) */\n    pub fn size(&self) -> usize {\n        return self.size;\n    }\n\n    /* Получить вместимость списка */\n    pub fn capacity(&self) -> usize {\n        return self.capacity;\n    }\n\n    /* Доступ к элементу */\n    pub fn get(&self, index: usize) -> i32 {\n        // Если индекс выходит за границы, выбрасывается исключение; далее аналогично\n        if index >= self.size {\n            panic!(\"индекс выходит за границы\")\n        };\n        return self.arr[index];\n    }\n\n    /* Обновление элемента */\n    pub fn set(&mut self, index: usize, num: i32) {\n        if index >= self.size {\n            panic!(\"индекс выходит за границы\")\n        };\n        self.arr[index] = num;\n    }\n\n    /* Добавление элемента в конец */\n    pub fn add(&mut self, num: i32) {\n        // При превышении вместимости по числу элементов запускается расширение\n        if self.size == self.capacity() {\n            self.extend_capacity();\n        }\n        self.arr[self.size] = num;\n        // Обновить число элементов\n        self.size += 1;\n    }\n\n    /* Вставка элемента в середину */\n    pub fn insert(&mut self, index: usize, num: i32) {\n        if index >= self.size() {\n            panic!(\"индекс выходит за границы\")\n        };\n        // При превышении вместимости по числу элементов запускается расширение\n        if self.size == self.capacity() {\n            self.extend_capacity();\n        }\n        // Сдвинуть элемент с индексом index и все следующие элементы на одну позицию назад\n        for j in (index..self.size).rev() {\n            self.arr[j + 1] = self.arr[j];\n        }\n        self.arr[index] = num;\n        // Обновить число элементов\n        self.size += 1;\n    }\n\n    /* Удаление элемента */\n    pub fn remove(&mut self, index: usize) -> i32 {\n        if index >= self.size() {\n            panic!(\"индекс выходит за границы\")\n        };\n        let num = self.arr[index];\n        // Сдвинуть все элементы после индекса index на одну позицию вперед\n        for j in index..self.size - 1 {\n            self.arr[j] = self.arr[j + 1];\n        }\n        // Обновить число элементов\n        self.size -= 1;\n        // Вернуть удаленный элемент\n        return num;\n    }\n\n    /* Расширение списка */\n    pub fn extend_capacity(&mut self) {\n        // Создать новый массив длиной в extend_ratio раз больше исходного и скопировать в него исходный массив\n        let new_capacity = self.capacity * self.extend_ratio;\n        self.arr.resize(new_capacity, 0);\n        // Обновить вместимость списка\n        self.capacity = new_capacity;\n    }\n\n    /* Преобразовать список в массив */\n    pub fn to_array(&self) -> Vec<i32> {\n        // Преобразовывать только элементы списка в пределах фактической длины\n        let mut arr = Vec::new();\n        for i in 0..self.size {\n            arr.push(self.get(i));\n        }\n        arr\n    }\n}\n
        my_list.c
        /* Класс списка */\ntypedef struct {\n    int *arr;        // Массив (для хранения элементов списка)\n    int capacity;    // Вместимость списка\n    int size;        // Размер списка\n    int extendRatio; // Коэффициент расширения списка при каждом увеличении\n} MyList;\n\n/* Конструктор */\nMyList *newMyList() {\n    MyList *nums = malloc(sizeof(MyList));\n    nums->capacity = 10;\n    nums->arr = malloc(sizeof(int) * nums->capacity);\n    nums->size = 0;\n    nums->extendRatio = 2;\n    return nums;\n}\n\n/* Деструктор */\nvoid delMyList(MyList *nums) {\n    free(nums->arr);\n    free(nums);\n}\n\n/* Получить длину списка */\nint size(MyList *nums) {\n    return nums->size;\n}\n\n/* Получить вместимость списка */\nint capacity(MyList *nums) {\n    return nums->capacity;\n}\n\n/* Доступ к элементу */\nint get(MyList *nums, int index) {\n    assert(index >= 0 && index < nums->size);\n    return nums->arr[index];\n}\n\n/* Обновление элемента */\nvoid set(MyList *nums, int index, int num) {\n    assert(index >= 0 && index < nums->size);\n    nums->arr[index] = num;\n}\n\n/* Добавление элемента в конец */\nvoid add(MyList *nums, int num) {\n    if (size(nums) == capacity(nums)) {\n        extendCapacity(nums); // Расширение емкости\n    }\n    nums->arr[size(nums)] = num;\n    nums->size++;\n}\n\n/* Вставка элемента в середину */\nvoid insert(MyList *nums, int index, int num) {\n    assert(index >= 0 && index < size(nums));\n    // При превышении вместимости по числу элементов запускается расширение\n    if (size(nums) == capacity(nums)) {\n        extendCapacity(nums); // Расширение емкости\n    }\n    for (int i = size(nums); i > index; --i) {\n        nums->arr[i] = nums->arr[i - 1];\n    }\n    nums->arr[index] = num;\n    nums->size++;\n}\n\n/* Удаление элемента */\n// Внимание: stdio.h уже использует ключевое слово remove\nint removeItem(MyList *nums, int index) {\n    assert(index >= 0 && index < size(nums));\n    int num = nums->arr[index];\n    for (int i = index; i < size(nums) - 1; i++) {\n        nums->arr[i] = nums->arr[i + 1];\n    }\n    nums->size--;\n    return num;\n}\n\n/* Расширение списка */\nvoid extendCapacity(MyList *nums) {\n    // Сначала выделить память\n    int newCapacity = capacity(nums) * nums->extendRatio;\n    int *extend = (int *)malloc(sizeof(int) * newCapacity);\n    int *temp = nums->arr;\n\n    // Скопировать старые данные в новые\n    for (int i = 0; i < size(nums); i++)\n        extend[i] = nums->arr[i];\n\n    // Освободить старые данные\n    free(temp);\n\n    // Обновить новые данные\n    nums->arr = extend;\n    nums->capacity = newCapacity;\n}\n\n/* Преобразовать список в Array для вывода */\nint *toArray(MyList *nums) {\n    return nums->arr;\n}\n
        my_list.kt
        /* Класс списка */\nclass MyList {\n    private var arr: IntArray = intArrayOf() // Массив (для хранения элементов списка)\n    private var capacity: Int = 10 // Вместимость списка\n    private var size: Int = 0 // Длина списка (текущее число элементов)\n    private var extendRatio: Int = 2 // Коэффициент увеличения списка при каждом расширении\n\n    /* Конструктор */\n    init {\n        arr = IntArray(capacity)\n    }\n\n    /* Получить длину списка (текущее число элементов) */\n    fun size(): Int {\n        return size\n    }\n\n    /* Получить вместимость списка */\n    fun capacity(): Int {\n        return capacity\n    }\n\n    /* Доступ к элементу */\n    fun get(index: Int): Int {\n        // Если индекс выходит за границы, выбрасывается исключение; далее аналогично\n        if (index < 0 || index >= size)\n            throw IndexOutOfBoundsException(\"индекс выходит за границы\")\n        return arr[index]\n    }\n\n    /* Обновление элемента */\n    fun set(index: Int, num: Int) {\n        if (index < 0 || index >= size)\n            throw IndexOutOfBoundsException(\"индекс выходит за границы\")\n        arr[index] = num\n    }\n\n    /* Добавление элемента в конец */\n    fun add(num: Int) {\n        // При превышении вместимости по числу элементов запускается расширение\n        if (size == capacity())\n            extendCapacity()\n        arr[size] = num\n        // Обновить число элементов\n        size++\n    }\n\n    /* Вставка элемента в середину */\n    fun insert(index: Int, num: Int) {\n        if (index < 0 || index >= size)\n            throw IndexOutOfBoundsException(\"индекс выходит за границы\")\n        // При превышении вместимости по числу элементов запускается расширение\n        if (size == capacity())\n            extendCapacity()\n        // Сдвинуть элемент с индексом index и все следующие элементы на одну позицию назад\n        for (j in size - 1 downTo index)\n            arr[j + 1] = arr[j]\n        arr[index] = num\n        // Обновить число элементов\n        size++\n    }\n\n    /* Удаление элемента */\n    fun remove(index: Int): Int {\n        if (index < 0 || index >= size)\n            throw IndexOutOfBoundsException(\"индекс выходит за границы\")\n        val num = arr[index]\n        // Сдвинуть все элементы после индекса index на одну позицию вперед\n        for (j in index..<size - 1)\n            arr[j] = arr[j + 1]\n        // Обновить число элементов\n        size--\n        // Вернуть удаленный элемент\n        return num\n    }\n\n    /* Расширение списка */\n    fun extendCapacity() {\n        // Создать новый массив длиной в extendRatio раз больше исходного и скопировать в него исходный массив\n        arr = arr.copyOf(capacity() * extendRatio)\n        // Обновить вместимость списка\n        capacity = arr.size\n    }\n\n    /* Преобразовать список в массив */\n    fun toArray(): IntArray {\n        val size = size()\n        // Преобразовывать только элементы списка в пределах фактической длины\n        val arr = IntArray(size)\n        for (i in 0..<size) {\n            arr[i] = get(i)\n        }\n        return arr\n    }\n}\n
        my_list.rb
        ### Класс списка ###\nclass MyList\n  attr_reader :size       # Получить длину списка (текущее число элементов)\n  attr_reader :capacity   # Получить вместимость списка\n\n  ### Конструктор ###\n  def initialize\n    @capacity = 10\n    @size = 0\n    @extend_ratio = 2\n    @arr = Array.new(capacity)\n  end\n\n  ### Доступ к элементу ###\n  def get(index)\n    # Если индекс выходит за границы, выбрасывается исключение; далее аналогично\n    raise IndexError, \"индекс выходит за границы\" if index < 0 || index >= size\n    @arr[index]\n  end\n\n  ### Доступ к элементу ###\n  def set(index, num)\n    raise IndexError, \"индекс выходит за границы\" if index < 0 || index >= size\n    @arr[index] = num\n  end\n\n  ### Добавление элемента в конец ###\n  def add(num)\n    # При превышении вместимости по числу элементов запускается расширение\n    extend_capacity if size == capacity\n    @arr[size] = num\n\n    # Обновить число элементов\n    @size += 1\n  end\n\n  ### Вставка элемента в середину ###\n  def insert(index, num)\n    raise IndexError, \"индекс выходит за границы\" if index < 0 || index >= size\n\n    # При превышении вместимости по числу элементов запускается расширение\n    extend_capacity if size == capacity\n\n    # Сдвинуть элемент с индексом index и все следующие элементы на одну позицию назад\n    for j in (size - 1).downto(index)\n      @arr[j + 1] = @arr[j]\n    end\n    @arr[index] = num\n\n    # Обновить число элементов\n    @size += 1\n  end\n\n  ### Удаление элемента ###\n  def remove(index)\n    raise IndexError, \"индекс выходит за границы\" if index < 0 || index >= size\n    num = @arr[index]\n\n    # Сдвинуть все элементы после индекса index на одну позицию вперед\n    for j in index...size\n      @arr[j] = @arr[j + 1]\n    end\n\n    # Обновить число элементов\n    @size -= 1\n\n    # Вернуть удаленный элемент\n    num\n  end\n\n  ### Расширение списка ###\n  def extend_capacity\n    # Создать новый массив длиной в extend_ratio раз больше исходного и скопировать в него исходный массив\n    arr = @arr.dup + Array.new(capacity * (@extend_ratio - 1))\n    # Обновить вместимость списка\n    @capacity = arr.length\n  end\n\n  ### Преобразование списка в массив ###\n  def to_array\n    sz = size\n    # Преобразовывать только элементы списка в пределах фактической длины\n    arr = Array.new(sz)\n    for i in 0...sz\n      arr[i] = get(i)\n    end\n    arr\n  end\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 4. Массивы и списки","4.3   Список"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/","level":1,"title":"4.4   Оперативная память и кэш *","text":"

        В первых двух разделах этой главы мы разобрали массивы и связные списки - две базовые и важные структуры данных, которые представляют соответственно непрерывное хранение и разрозненное хранение.

        На практике физическая структура во многом определяет, насколько эффективно программа использует память и кэш, а это, в свою очередь, влияет на общую производительность алгоритма.

        ","path":["Глава 4. Массивы и списки","4.4   Оперативная память и кэш *"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/#441","level":2,"title":"4.4.1   Устройства хранения данных в компьютере","text":"

        В компьютере есть три типа устройств хранения данных: жесткий диск (hard disk) , оперативная память (random-access memory, RAM) и кэш-память (cache memory) . В таблице 4-2 показаны их различные роли и характеристики в компьютерной системе.

        Таблица 4-2   Устройства хранения данных в компьютере

        Жесткий диск Оперативная память Кэш Назначение Долговременное хранение данных, включая ОС, программы, файлы и т.д. Временное хранение выполняемых программ и обрабатываемых данных Хранение часто используемых данных и инструкций, уменьшающее число обращений CPU к памяти Энергозависимость Данные не теряются после отключения питания Данные теряются после отключения питания Данные теряются после отключения питания Емкость Большая, уровень TB Меньшая, уровень GB Очень малая, уровень MB Скорость Низкая, от сотен до тысяч MB/s Высокая, десятки GB/s Очень высокая, десятки и сотни GB/s Цена Низкая, единицы валюты за GB Высокая, десятки и сотни валютных единиц за GB Очень высокая, входит в стоимость CPU

        Компьютерную систему хранения можно представить в виде пирамиды, показанной на рисунке 4-9. Чем ближе устройство хранения к вершине пирамиды, тем оно быстрее, тем меньше его емкость и тем выше его стоимость. Такая многоуровневая конструкция возникла не случайно, а стала результатом тщательных инженерных компромиссов.

        • Жесткий диск трудно заменить оперативной памятью. Во-первых, данные в оперативной памяти исчезают после отключения питания, поэтому она не подходит для долговременного хранения. Во-вторых, память стоит в разы дороже жесткого диска, что мешает ее широкому применению.
        • Кэш не может одновременно быть и очень большим, и очень быстрым. По мере роста емкости кэшей L1, L2 и L3 их физический размер увеличивается, расстояние до ядра CPU становится больше, время передачи данных растет, а задержка доступа к элементам увеличивается. При текущем уровне технологий многоуровневая структура кэша является лучшим балансом между емкостью, скоростью и стоимостью.

        Рисунок 4-9   Система хранения данных компьютера

        Tip

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

        В итоге жесткий диск используется для долговременного хранения больших объемов данных, оперативная память - для временного хранения данных, с которыми программа работает прямо сейчас, а кэш - для хранения часто используемых данных и инструкций, чтобы ускорять выполнение программ. Все три уровня работают совместно и обеспечивают эффективную работу компьютерной системы.

        Как показано на рисунке 4-10, во время выполнения программы данные читаются с жесткого диска в оперативную память, а затем используются CPU в вычислениях. Кэш можно рассматривать как часть CPU: он подгружает данные из оперативной памяти, обеспечивая CPU высокоскоростной доступ и тем самым значительно ускоряя выполнение программы и уменьшая зависимость от более медленной RAM.

        Рисунок 4-10   Поток данных между жестким диском, RAM и кэшем

        ","path":["Глава 4. Массивы и списки","4.4   Оперативная память и кэш *"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/#442","level":2,"title":"4.4.2   Эффективность использования памяти структурами данных","text":"

        С точки зрения использования пространства памяти массивы и связные списки имеют свои преимущества и ограничения.

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

        С другой стороны, во время выполнения программы при многократном выделении и освобождении памяти фрагментация свободной памяти становится все более серьезной, что снижает эффективность ее использования. Массивы из-за непрерывного хранения относительно менее подвержены фрагментации. Напротив, элементы связного списка распределены по памяти, и частые операции вставки и удаления легче приводят к фрагментации.

        ","path":["Глава 4. Массивы и списки","4.4   Оперативная память и кэш *"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/#443","level":2,"title":"4.4.3   Эффективность использования кэша структурами данных","text":"

        Хотя по объему кэш намного меньше оперативной памяти, он значительно быстрее и играет критически важную роль в скорости выполнения программ. Поскольку объем кэша ограничен и в нем можно хранить только небольшую долю часто используемых данных, когда CPU пытается обратиться к данным, которых в кэше нет, происходит промах кэша (cache miss) , и CPU вынужден загружать нужные данные из более медленной памяти.

        Очевидно, что чем меньше промахов кэша, тем выше эффективность чтения и записи данных CPU, а значит, тем лучше производительность программы. Долю обращений, при которых CPU успешно получает данные из кэша, называют коэффициентом попадания в кэш (cache hit rate). Этот показатель обычно используют для оценки эффективности кэша.

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

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

        На практике массивы и связные списки по-разному используют кэш, и это проявляется в нескольких аспектах.

        • Занимаемое пространство: элементы связного списка занимают больше места, чем элементы массива, поэтому в кэше помещается меньше полезных данных.
        • Строки кэша: данные списка разбросаны по памяти, а кэш загружает данные строками, поэтому доля бесполезно загружаемых данных оказывается выше.
        • Механизм предвыборки: шаблон доступа к данным у массивов более предсказуем, чем у списков, то есть системе легче угадать, какие данные понадобятся следующими.
        • Пространственная локальность: массив хранится в компактной области памяти, поэтому данные рядом с уже загруженными с большей вероятностью скоро будут использованы.

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

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

        • При решении алгоритмических задач мы обычно предпочитаем стек на основе массива, потому что он дает более высокую эффективность операций и поддерживает произвольный доступ, а цена за это - необходимость заранее выделить некоторый объем памяти под массив.
        • Если объем данных очень велик, структура сильно динамична, а ожидаемый размер стека трудно оценить заранее, то более уместен стек на основе связного списка. Список позволяет распределить большой объем данных по разным участкам памяти и избегает накладных расходов, связанных с расширением массива.
        ","path":["Глава 4. Массивы и списки","4.4   Оперативная память и кэш *"],"tags":[]},{"location":"chapter_array_and_linkedlist/summary/","level":1,"title":"4.5   Резюме","text":"","path":["Глава 4. Массивы и списки","4.5   Резюме"],"tags":[]},{"location":"chapter_array_and_linkedlist/summary/#1","level":3,"title":"1.   Ключевые выводы","text":"
        • Массивы и связные списки - это две базовые структуры данных, представляющие два способа хранения данных в памяти компьютера: хранение в непрерывном пространстве и хранение в разрозненном пространстве. Их свойства во многом взаимно дополняют друг друга.
        • Массив поддерживает произвольный доступ и занимает меньше памяти. Однако вставка и удаление элементов в нем неэффективны, а длина после инициализации фиксирована.
        • Связный список позволяет эффективно вставлять и удалять узлы путем изменения ссылок (указателей), а также гибко менять длину. Однако доступ к узлам менее эффективен, а памяти он занимает больше. Распространенные типы списков включают односвязные, циклические и двусвязные списки.
        • Список - это упорядоченная коллекция элементов, поддерживающая добавление, удаление, поиск и изменение, и обычно реализуемая на основе динамического массива. Он сохраняет преимущества массива и при этом может гибко менять длину.
        • Появление списка значительно повысило практическую ценность массива, хотя это и может приводить к потере части памяти.
        • Во время работы программы данные в основном хранятся в оперативной памяти. Массив обеспечивает более высокую эффективность использования пространства памяти, а связный список дает большую гибкость в использовании памяти.
        • Кэш, используя строки кэша, механизм предвыборки, а также пространственную и временную локальность, предоставляет CPU быстрый доступ к данным и заметно повышает эффективность выполнения программ.
        • Поскольку массивы обычно имеют более высокий коэффициент попадания в кэш, они в большинстве случаев работают эффективнее списков. При выборе структуры данных нужно исходить из конкретных требований и сценариев.
        ","path":["Глава 4. Массивы и списки","4.5   Резюме"],"tags":[]},{"location":"chapter_array_and_linkedlist/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

        Q: Влияет ли хранение массива в стеке или в куче на временную и пространственную эффективность?

        Массивы, расположенные и в стеке, и в куче, все равно хранятся в непрерывной области памяти, поэтому эффективность операций с данными у них в целом одинакова. Однако у стека и кучи есть собственные особенности, из-за которых возникают следующие различия.

        1. Эффективность выделения и освобождения: стек представляет собой относительно небольшой участок памяти, а выделение в нем обычно выполняется автоматически компилятором. Куча же обычно больше, может выделяться динамически из кода и легче фрагментируется. Поэтому выделение и освобождение памяти в куче обычно медленнее, чем в стеке.
        2. Ограничение размера: объем стека относительно невелик, а размер кучи обычно ограничивается доступной памятью. Поэтому куча лучше подходит для хранения больших массивов.
        3. Гибкость: размер массива в стеке должен быть известен во время компиляции, а размер массива в куче может определяться динамически во время выполнения.

        Q: Почему для массива требуется, чтобы все элементы были одного типа, а для связного списка это не подчеркивается?

        Связный список состоит из узлов, а узлы соединяются между собой через ссылки (указатели), поэтому каждый узел в принципе может хранить данные разного типа, например int , double , string , object и т.д.

        Напротив, элементы массива должны быть одного типа, иначе нельзя будет вычислять адрес элемента через смещение. Например, если массив одновременно содержит int и long , один элемент занимает 4 байта, а другой - 8 байт. В этом случае формула ниже уже не позволит вычислить смещение, потому что в массиве будут присутствовать элементы разной длины.

        # Адрес элемента в памяти = адрес массива в памяти (адрес первого элемента) + длина элемента * индекс элемента\n

        Q: После удаления узла P нужно ли присваивать P.next = None ?

        Можно и не изменять P.next . С точки зрения данного списка, при обходе от головы к хвосту узел P уже больше не встретится. Это означает, что узел P уже удален из списка, и то, куда он указывает после этого, на сам список больше не влияет.

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

        Q: Временная сложность вставки и удаления в связном списке равна \\(O(1)\\) . Но до вставки или удаления обычно еще нужно потратить \\(O(n)\\) на поиск элемента. Почему тогда общая сложность не \\(O(n)\\) ?

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

        Q: На рисунке «Определение связного списка и способ хранения» светло-голубой блок с указателем узла - это отдельный адрес памяти? Или он делит память пополам со значением узла?

        Этот рисунок дает только качественное представление. Количественно все зависит от конкретных условий.

        • Значения узлов разных типов занимают разный объем памяти, например int , long , double и объекты-экземпляры.
        • Размер памяти, занимаемой переменной-указателем, зависит от операционной системы и среды компиляции и обычно составляет 8 байт или 4 байта.

        Q: Всегда ли добавление элемента в конец списка имеет сложность \\(O(1)\\) ?

        Если при добавлении элемента длина списка превышается, то сначала приходится расширять список, а уже затем добавлять новый элемент. Система выделяет новый участок памяти и переносит туда все элементы исходного списка, и в этот момент временная сложность становится \\(O(n)\\) .

        Q: В утверждении «появление списка сильно повысило практическую полезность массива, но может приводить к потере части памяти» под потерями памяти имеется в виду дополнительная память под такие переменные, как емкость, длина и коэффициент расширения?

        Потери памяти здесь в основном имеют два значения: во-первых, список обычно имеет некоторую начальную емкость, которая может быть нам не нужна целиком. Во-вторых, чтобы избежать слишком частых расширений, емкость при расширении обычно умножается на некоторый коэффициент, например \\(\\times 1.5\\) . Из-за этого появляется много пустых слотов, которые обычно нельзя полностью заполнить.

        Q: В Python после инициализации n = [1, 2, 3] адреса этих трех элементов выглядят непрерывными, но после m = [2, 1, 3] можно заметить, что id элементов не идут подряд, а совпадают с одинаковыми числами из n . Если адреса элементов не непрерывны, остается ли m массивом?

        Предположим, что элементами списка являются узлы n = [n1, n2, n3, n4, n5] . Обычно эти 5 объектов-узлов тоже будут храниться в разных местах памяти. Однако, имея индекс списка, мы по-прежнему можем за \\(O(1)\\) получить адрес памяти соответствующего узла и обратиться к нему. Это связано с тем, что в массиве хранятся ссылки на узлы, а не сами узлы.

        В отличие от многих других языков, в Python даже числа обернуты в объекты, и в списке хранятся не сами числа, а ссылки на них. Поэтому мы и наблюдаем, что одинаковые числа в двух массивах имеют один и тот же id , а адреса этих чисел не обязаны быть непрерывными.

        Q: В C++ STL уже есть двусвязный список std::list , но в некоторых учебниках по алгоритмам им пользуются не так часто. Это связано с какими-то ограничениями?

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

        • Накладные расходы по памяти: поскольку каждому элементу нужны два дополнительных указателя (на предыдущий и следующий элементы), std::list обычно занимает больше памяти, чем std::vector .
        • Низкая дружелюбность к кэшу: поскольку данные не лежат непрерывно, std::list хуже использует кэш. В большинстве случаев std::vector показывает лучшую производительность.

        С другой стороны, случаи, когда связный список действительно необходим, в основном возникают в деревьях и графах. Для стеков и очередей чаще используют предоставляемые языком stack и queue , а не связный список напрямую.

        Q: Операция res = [[0]] * n создает двумерный список. Каждый [0] в нем независим?

        Нет, они не независимы. В таком двумерном списке все [0] на самом деле являются ссылками на один и тот же объект. Если изменить один из них, окажется, что меняются и все остальные соответствующие элементы.

        Если нужно, чтобы каждый [0] был независимым, можно использовать res = [[0] for _ in range(n)] . В этом варианте создаются \\(n\\) независимых объектов-списков [0] .

        Q: Операция res = [0] * n создает список. Каждый целочисленный 0 в нем независим?

        В этом списке все целые числа 0 являются ссылками на один и тот же объект. Это связано с тем, что Python использует механизм кэш-пула для маленьких целых чисел (обычно от -5 до 256), чтобы максимально переиспользовать объекты и повысить производительность.

        Хотя все элементы указывают на один и тот же объект, мы все равно можем независимо изменять элементы списка, потому что целые числа в Python - это «неизменяемые объекты». Когда мы изменяем некоторый элемент, на самом деле происходит переключение ссылки на другой объект, а не изменение исходного объекта.

        Однако если элементами списка являются «изменяемые объекты» (например списки, словари или экземпляры классов), то изменение одного элемента прямо меняет сам объект, и все элементы, ссылающиеся на него, увидят одно и то же изменение.

        ","path":["Глава 4. Массивы и списки","4.5   Резюме"],"tags":[]},{"location":"chapter_backtracking/","level":1,"title":"Глава 13.   Поиск с возвратом","text":"

        Abstract

        Мы словно исследователи в лабиринте: на пути вперед могут встречаться тупики и трудности.

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

        ","path":["Глава 13. Поиск с возвратом","Глава 13.   Поиск с возвратом"],"tags":[]},{"location":"chapter_backtracking/#_1","level":2,"title":"Содержание главы","text":"
        • 13.1   Алгоритм поиска с возвратом
        • 13.2   Задача о перестановках
        • 13.3   Задача о сумме подмножеств
        • 13.4   Задача о n ферзях
        • 13.5   Резюме
        ","path":["Глава 13. Поиск с возвратом","Глава 13.   Поиск с возвратом"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/","level":1,"title":"13.1   Алгоритм поиска с возвратом","text":"

        Алгоритм поиска с возвратом (backtracking algorithm) - это метод решения задач путем полного перебора. Его основная идея состоит в том, чтобы, начиная с некоторого исходного состояния, грубо перебрать все возможные решения, записывать корректные решения и продолжать поиск до тех пор, пока решение не будет найдено или пока не будут исчерпаны все возможные варианты.

        Обычно алгоритмы поиска с возвратом используют обход в глубину для обхода пространства решений. В главе «Бинарные деревья» мы уже упоминали, что прямой, симметричный и обратный обходы относятся к обходу в глубину. Теперь мы на основе прямого обхода построим задачу поиска с возвратом и постепенно разберем принцип работы этого алгоритма.

        Пример 1

        Дано двоичное дерево. Найдите и запишите все узлы со значением \\(7\\). Верните список этих узлов.

        Для этой задачи мы выполняем прямой обход дерева и проверяем, равно ли значение текущего узла \\(7\\). Если да, то добавляем значение этого узла в список результатов res . Соответствующий процесс показан на рисунке 13-1 и в коде:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_i_compact.py
        def pre_order(root: TreeNode):\n    \"\"\"Предварительный обход: пример 1\"\"\"\n    if root is None:\n        return\n    if root.val == 7:\n        # Записать решение\n        res.append(root)\n    pre_order(root.left)\n    pre_order(root.right)\n
        preorder_traversal_i_compact.cpp
        /* Предварительный обход: пример 1 */\nvoid preOrder(TreeNode *root) {\n    if (root == nullptr) {\n        return;\n    }\n    if (root->val == 7) {\n        // Записать решение\n        res.push_back(root);\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n}\n
        preorder_traversal_i_compact.java
        /* Предварительный обход: пример 1 */\nvoid preOrder(TreeNode root) {\n    if (root == null) {\n        return;\n    }\n    if (root.val == 7) {\n        // Записать решение\n        res.add(root);\n    }\n    preOrder(root.left);\n    preOrder(root.right);\n}\n
        preorder_traversal_i_compact.cs
        /* Предварительный обход: пример 1 */\nvoid PreOrder(TreeNode? root) {\n    if (root == null) {\n        return;\n    }\n    if (root.val == 7) {\n        // Записать решение\n        res.Add(root);\n    }\n    PreOrder(root.left);\n    PreOrder(root.right);\n}\n
        preorder_traversal_i_compact.go
        /* Предварительный обход: пример 1 */\nfunc preOrderI(root *TreeNode, res *[]*TreeNode) {\n    if root == nil {\n        return\n    }\n    if (root.Val).(int) == 7 {\n        // Записать решение\n        *res = append(*res, root)\n    }\n    preOrderI(root.Left, res)\n    preOrderI(root.Right, res)\n}\n
        preorder_traversal_i_compact.swift
        /* Предварительный обход: пример 1 */\nfunc preOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    if root.val == 7 {\n        // Записать решение\n        res.append(root)\n    }\n    preOrder(root: root.left)\n    preOrder(root: root.right)\n}\n
        preorder_traversal_i_compact.js
        /* Предварительный обход: пример 1 */\nfunction preOrder(root, res) {\n    if (root === null) {\n        return;\n    }\n    if (root.val === 7) {\n        // Записать решение\n        res.push(root);\n    }\n    preOrder(root.left, res);\n    preOrder(root.right, res);\n}\n
        preorder_traversal_i_compact.ts
        /* Предварительный обход: пример 1 */\nfunction preOrder(root: TreeNode | null, res: TreeNode[]): void {\n    if (root === null) {\n        return;\n    }\n    if (root.val === 7) {\n        // Записать решение\n        res.push(root);\n    }\n    preOrder(root.left, res);\n    preOrder(root.right, res);\n}\n
        preorder_traversal_i_compact.dart
        /* Предварительный обход: пример 1 */\nvoid preOrder(TreeNode? root, List<TreeNode> res) {\n  if (root == null) {\n    return;\n  }\n  if (root.val == 7) {\n    // Записать решение\n    res.add(root);\n  }\n  preOrder(root.left, res);\n  preOrder(root.right, res);\n}\n
        preorder_traversal_i_compact.rs
        /* Предварительный обход: пример 1 */\nfn pre_order(res: &mut Vec<Rc<RefCell<TreeNode>>>, root: Option<&Rc<RefCell<TreeNode>>>) {\n    if root.is_none() {\n        return;\n    }\n    if let Some(node) = root {\n        if node.borrow().val == 7 {\n            // Записать решение\n            res.push(node.clone());\n        }\n        pre_order(res, node.borrow().left.as_ref());\n        pre_order(res, node.borrow().right.as_ref());\n    }\n}\n
        preorder_traversal_i_compact.c
        /* Предварительный обход: пример 1 */\nvoid preOrder(TreeNode *root) {\n    if (root == NULL) {\n        return;\n    }\n    if (root->val == 7) {\n        // Записать решение\n        res[resSize++] = root;\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n}\n
        preorder_traversal_i_compact.kt
        /* Предварительный обход: пример 1 */\nfun preOrder(root: TreeNode?) {\n    if (root == null) {\n        return\n    }\n    if (root._val == 7) {\n        // Записать решение\n        res!!.add(root)\n    }\n    preOrder(root.left)\n    preOrder(root.right)\n}\n
        preorder_traversal_i_compact.rb
        ### Предварительный обход: пример 1 ###\ndef pre_order(root)\n  return unless root\n\n  # Записать решение\n  $res << root if root.val == 7\n\n  pre_order(root.left)\n  pre_order(root.right)\nend\n
        Визуализация кода

        Во весь экран >

        Рисунок 13-1   Поиск узлов при прямом обходе

        ","path":["Глава 13. Поиск с возвратом","13.1   Алгоритм поиска с возвратом"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1311","level":2,"title":"13.1.1   Попытка и откат","text":"

        Алгоритм называется поиском с возвратом, потому что при поиске в пространстве решений он использует стратегию «попытка» и «откат». Когда в процессе поиска алгоритм приходит в состояние, из которого нельзя двигаться дальше или нельзя получить удовлетворяющее условиям решение, он отменяет предыдущий выбор, возвращается к более раннему состоянию и пробует другие возможные варианты.

        Для примера 1 посещение каждого узла представляет собой «попытку», а прохождение листового узла или возврат к родителю через return означает «откат».

        Важно понимать, что откат не сводится только к возврату из функции. Чтобы показать это, слегка расширим пример 1.

        Пример 2

        Найдите в двоичном дереве все узлы со значением \\(7\\) и верните пути от корня до этих узлов.

        Взяв за основу код примера 1, добавим список path для записи пути посещенных узлов. Когда встречается узел со значением \\(7\\) , мы копируем path и добавляем его в список результатов res . После завершения обхода именно res будет содержать все решения. Код приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_ii_compact.py
        def pre_order(root: TreeNode):\n    \"\"\"Предварительный обход: пример 2\"\"\"\n    if root is None:\n        return\n    # Попытка\n    path.append(root)\n    if root.val == 7:\n        # Записать решение\n        res.append(list(path))\n    pre_order(root.left)\n    pre_order(root.right)\n    # Откат\n    path.pop()\n
        preorder_traversal_ii_compact.cpp
        /* Предварительный обход: пример 2 */\nvoid preOrder(TreeNode *root) {\n    if (root == nullptr) {\n        return;\n    }\n    // Попытка\n    path.push_back(root);\n    if (root->val == 7) {\n        // Записать решение\n        res.push_back(path);\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n    // Откат\n    path.pop_back();\n}\n
        preorder_traversal_ii_compact.java
        /* Предварительный обход: пример 2 */\nvoid preOrder(TreeNode root) {\n    if (root == null) {\n        return;\n    }\n    // Попытка\n    path.add(root);\n    if (root.val == 7) {\n        // Записать решение\n        res.add(new ArrayList<>(path));\n    }\n    preOrder(root.left);\n    preOrder(root.right);\n    // Откат\n    path.remove(path.size() - 1);\n}\n
        preorder_traversal_ii_compact.cs
        /* Предварительный обход: пример 2 */\nvoid PreOrder(TreeNode? root) {\n    if (root == null) {\n        return;\n    }\n    // Попытка\n    path.Add(root);\n    if (root.val == 7) {\n        // Записать решение\n        res.Add(new List<TreeNode>(path));\n    }\n    PreOrder(root.left);\n    PreOrder(root.right);\n    // Откат\n    path.RemoveAt(path.Count - 1);\n}\n
        preorder_traversal_ii_compact.go
        /* Предварительный обход: пример 2 */\nfunc preOrderII(root *TreeNode, res *[][]*TreeNode, path *[]*TreeNode) {\n    if root == nil {\n        return\n    }\n    // Попытка\n    *path = append(*path, root)\n    if root.Val.(int) == 7 {\n        // Записать решение\n        *res = append(*res, append([]*TreeNode{}, *path...))\n    }\n    preOrderII(root.Left, res, path)\n    preOrderII(root.Right, res, path)\n    // Откат\n    *path = (*path)[:len(*path)-1]\n}\n
        preorder_traversal_ii_compact.swift
        /* Предварительный обход: пример 2 */\nfunc preOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    // Попытка\n    path.append(root)\n    if root.val == 7 {\n        // Записать решение\n        res.append(path)\n    }\n    preOrder(root: root.left)\n    preOrder(root: root.right)\n    // Откат\n    path.removeLast()\n}\n
        preorder_traversal_ii_compact.js
        /* Предварительный обход: пример 2 */\nfunction preOrder(root, path, res) {\n    if (root === null) {\n        return;\n    }\n    // Попытка\n    path.push(root);\n    if (root.val === 7) {\n        // Записать решение\n        res.push([...path]);\n    }\n    preOrder(root.left, path, res);\n    preOrder(root.right, path, res);\n    // Откат\n    path.pop();\n}\n
        preorder_traversal_ii_compact.ts
        /* Предварительный обход: пример 2 */\nfunction preOrder(\n    root: TreeNode | null,\n    path: TreeNode[],\n    res: TreeNode[][]\n): void {\n    if (root === null) {\n        return;\n    }\n    // Попытка\n    path.push(root);\n    if (root.val === 7) {\n        // Записать решение\n        res.push([...path]);\n    }\n    preOrder(root.left, path, res);\n    preOrder(root.right, path, res);\n    // Откат\n    path.pop();\n}\n
        preorder_traversal_ii_compact.dart
        /* Предварительный обход: пример 2 */\nvoid preOrder(\n  TreeNode? root,\n  List<TreeNode> path,\n  List<List<TreeNode>> res,\n) {\n  if (root == null) {\n    return;\n  }\n\n  // Попытка\n  path.add(root);\n  if (root.val == 7) {\n    // Записать решение\n    res.add(List.from(path));\n  }\n  preOrder(root.left, path, res);\n  preOrder(root.right, path, res);\n  // Откат\n  path.removeLast();\n}\n
        preorder_traversal_ii_compact.rs
        /* Предварительный обход: пример 2 */\nfn pre_order(\n    res: &mut Vec<Vec<Rc<RefCell<TreeNode>>>>,\n    path: &mut Vec<Rc<RefCell<TreeNode>>>,\n    root: Option<&Rc<RefCell<TreeNode>>>,\n) {\n    if root.is_none() {\n        return;\n    }\n    if let Some(node) = root {\n        // Попытка\n        path.push(node.clone());\n        if node.borrow().val == 7 {\n            // Записать решение\n            res.push(path.clone());\n        }\n        pre_order(res, path, node.borrow().left.as_ref());\n        pre_order(res, path, node.borrow().right.as_ref());\n        // Откат\n        path.pop();\n    }\n}\n
        preorder_traversal_ii_compact.c
        /* Предварительный обход: пример 2 */\nvoid preOrder(TreeNode *root) {\n    if (root == NULL) {\n        return;\n    }\n    // Попытка\n    path[pathSize++] = root;\n    if (root->val == 7) {\n        // Записать решение\n        for (int i = 0; i < pathSize; ++i) {\n            res[resSize][i] = path[i];\n        }\n        resSize++;\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n    // Откат\n    pathSize--;\n}\n
        preorder_traversal_ii_compact.kt
        /* Предварительный обход: пример 2 */\nfun preOrder(root: TreeNode?) {\n    if (root == null) {\n        return\n    }\n    // Попытка\n    path!!.add(root)\n    if (root._val == 7) {\n        // Записать решение\n        res!!.add(path!!.toMutableList())\n    }\n    preOrder(root.left)\n    preOrder(root.right)\n    // Откат\n    path!!.removeAt(path!!.size - 1)\n}\n
        preorder_traversal_ii_compact.rb
        ### Предварительный обход: пример 2 ###\ndef pre_order(root)\n  return unless root\n\n  # Попытка\n  $path << root\n\n  # Записать решение\n  $res << $path.dup if root.val == 7\n\n  pre_order(root.left)\n  pre_order(root.right)\n\n  # Откат\n  $path.pop\nend\n
        Визуализация кода

        Во весь экран >

        В каждой «попытке» мы добавляем текущий узел в path , чтобы записать путь. А перед «откатом» нам нужно удалить этот узел из path , чтобы восстановить состояние, существовавшее до текущей попытки.

        Если посмотреть на процесс, изображенный на рисунке 13-2, то попытку и откат можно понимать как «движение вперед» и «отмену»: это два взаимно противоположных действия.

        <1><2><3><4><5><6><7><8><9><10><11>

        Рисунок 13-2   Попытка и откат

        ","path":["Глава 13. Поиск с возвратом","13.1   Алгоритм поиска с возвратом"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1312","level":2,"title":"13.1.2   Обрезка","text":"

        Сложные задачи поиска с возвратом обычно содержат одно или несколько ограничений, которые часто можно использовать для «обрезки».

        Пример 3

        Найдите в двоичном дереве все узлы со значением \\(7\\) , верните пути от корня до этих узлов, причем путь не должен содержать узлы со значением \\(3\\).

        Чтобы выполнить это ограничение, нам нужно добавить операцию обрезки: во время поиска, если встречается узел со значением \\(3\\) , мы сразу возвращаемся и не продолжаем дальнейший поиск. Код выглядит так:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_iii_compact.py
        def pre_order(root: TreeNode):\n    \"\"\"Предварительный обход: пример 3\"\"\"\n    # Отсечение\n    if root is None or root.val == 3:\n        return\n    # Попытка\n    path.append(root)\n    if root.val == 7:\n        # Записать решение\n        res.append(list(path))\n    pre_order(root.left)\n    pre_order(root.right)\n    # Откат\n    path.pop()\n
        preorder_traversal_iii_compact.cpp
        /* Предварительный обход: пример 3 */\nvoid preOrder(TreeNode *root) {\n    // Отсечение\n    if (root == nullptr || root->val == 3) {\n        return;\n    }\n    // Попытка\n    path.push_back(root);\n    if (root->val == 7) {\n        // Записать решение\n        res.push_back(path);\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n    // Откат\n    path.pop_back();\n}\n
        preorder_traversal_iii_compact.java
        /* Предварительный обход: пример 3 */\nvoid preOrder(TreeNode root) {\n    // Отсечение\n    if (root == null || root.val == 3) {\n        return;\n    }\n    // Попытка\n    path.add(root);\n    if (root.val == 7) {\n        // Записать решение\n        res.add(new ArrayList<>(path));\n    }\n    preOrder(root.left);\n    preOrder(root.right);\n    // Откат\n    path.remove(path.size() - 1);\n}\n
        preorder_traversal_iii_compact.cs
        /* Предварительный обход: пример 3 */\nvoid PreOrder(TreeNode? root) {\n    // Отсечение\n    if (root == null || root.val == 3) {\n        return;\n    }\n    // Попытка\n    path.Add(root);\n    if (root.val == 7) {\n        // Записать решение\n        res.Add(new List<TreeNode>(path));\n    }\n    PreOrder(root.left);\n    PreOrder(root.right);\n    // Откат\n    path.RemoveAt(path.Count - 1);\n}\n
        preorder_traversal_iii_compact.go
        /* Предварительный обход: пример 3 */\nfunc preOrderIII(root *TreeNode, res *[][]*TreeNode, path *[]*TreeNode) {\n    // Отсечение\n    if root == nil || root.Val == 3 {\n        return\n    }\n    // Попытка\n    *path = append(*path, root)\n    if root.Val.(int) == 7 {\n        // Записать решение\n        *res = append(*res, append([]*TreeNode{}, *path...))\n    }\n    preOrderIII(root.Left, res, path)\n    preOrderIII(root.Right, res, path)\n    // Откат\n    *path = (*path)[:len(*path)-1]\n}\n
        preorder_traversal_iii_compact.swift
        /* Предварительный обход: пример 3 */\nfunc preOrder(root: TreeNode?) {\n    // Отсечение\n    guard let root = root, root.val != 3 else {\n        return\n    }\n    // Попытка\n    path.append(root)\n    if root.val == 7 {\n        // Записать решение\n        res.append(path)\n    }\n    preOrder(root: root.left)\n    preOrder(root: root.right)\n    // Откат\n    path.removeLast()\n}\n
        preorder_traversal_iii_compact.js
        /* Предварительный обход: пример 3 */\nfunction preOrder(root, path, res) {\n    // Отсечение\n    if (root === null || root.val === 3) {\n        return;\n    }\n    // Попытка\n    path.push(root);\n    if (root.val === 7) {\n        // Записать решение\n        res.push([...path]);\n    }\n    preOrder(root.left, path, res);\n    preOrder(root.right, path, res);\n    // Откат\n    path.pop();\n}\n
        preorder_traversal_iii_compact.ts
        /* Предварительный обход: пример 3 */\nfunction preOrder(\n    root: TreeNode | null,\n    path: TreeNode[],\n    res: TreeNode[][]\n): void {\n    // Отсечение\n    if (root === null || root.val === 3) {\n        return;\n    }\n    // Попытка\n    path.push(root);\n    if (root.val === 7) {\n        // Записать решение\n        res.push([...path]);\n    }\n    preOrder(root.left, path, res);\n    preOrder(root.right, path, res);\n    // Откат\n    path.pop();\n}\n
        preorder_traversal_iii_compact.dart
        /* Предварительный обход: пример 3 */\nvoid preOrder(\n  TreeNode? root,\n  List<TreeNode> path,\n  List<List<TreeNode>> res,\n) {\n  if (root == null || root.val == 3) {\n    return;\n  }\n\n  // Попытка\n  path.add(root);\n  if (root.val == 7) {\n    // Записать решение\n    res.add(List.from(path));\n  }\n  preOrder(root.left, path, res);\n  preOrder(root.right, path, res);\n  // Откат\n  path.removeLast();\n}\n
        preorder_traversal_iii_compact.rs
        /* Предварительный обход: пример 3 */\nfn pre_order(\n    res: &mut Vec<Vec<Rc<RefCell<TreeNode>>>>,\n    path: &mut Vec<Rc<RefCell<TreeNode>>>,\n    root: Option<&Rc<RefCell<TreeNode>>>,\n) {\n    // Отсечение\n    if root.is_none() || root.as_ref().unwrap().borrow().val == 3 {\n        return;\n    }\n    if let Some(node) = root {\n        // Попытка\n        path.push(node.clone());\n        if node.borrow().val == 7 {\n            // Записать решение\n            res.push(path.clone());\n        }\n        pre_order(res, path, node.borrow().left.as_ref());\n        pre_order(res, path, node.borrow().right.as_ref());\n        // Откат\n        path.pop();\n    }\n}\n
        preorder_traversal_iii_compact.c
        /* Предварительный обход: пример 3 */\nvoid preOrder(TreeNode *root) {\n    // Отсечение\n    if (root == NULL || root->val == 3) {\n        return;\n    }\n    // Попытка\n    path[pathSize++] = root;\n    if (root->val == 7) {\n        // Записать решение\n        for (int i = 0; i < pathSize; i++) {\n            res[resSize][i] = path[i];\n        }\n        resSize++;\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n    // Откат\n    pathSize--;\n}\n
        preorder_traversal_iii_compact.kt
        /* Предварительный обход: пример 3 */\nfun preOrder(root: TreeNode?) {\n    // Отсечение\n    if (root == null || root._val == 3) {\n        return\n    }\n    // Попытка\n    path!!.add(root)\n    if (root._val == 7) {\n        // Записать решение\n        res!!.add(path!!.toMutableList())\n    }\n    preOrder(root.left)\n    preOrder(root.right)\n    // Откат\n    path!!.removeAt(path!!.size - 1)\n}\n
        preorder_traversal_iii_compact.rb
        ### Предварительный обход: пример 3 ###\ndef pre_order(root)\n  # Отсечение\n  return if !root || root.val == 3\n\n  # Попытка\n  $path.append(root)\n\n  # Записать решение\n  $res << $path.dup if root.val == 7\n\n  pre_order(root.left)\n  pre_order(root.right)\n\n  # Откат\n  $path.pop\nend\n
        Визуализация кода

        Во весь экран >

        Термин «обрезка» очень нагляден. Как показано на рисунке 13-3, во время поиска мы отсекаем ветви, не удовлетворяющие ограничениям , тем самым избегая множества бессмысленных попыток и повышая эффективность поиска.

        Рисунок 13-3   Обрезка по условиям задачи

        ","path":["Глава 13. Поиск с возвратом","13.1   Алгоритм поиска с возвратом"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1313","level":2,"title":"13.1.3   Каркас кода","text":"

        Теперь попробуем извлечь общий каркас из действий «попытка», «откат» и «обрезка», чтобы сделать код более универсальным.

        В следующем каркасе кода state обозначает текущее состояние задачи, а choices - список выборов, доступных в текущем состоянии:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
        def backtrack(state: State, choices: list[choice], res: list[state]):\n    \"\"\"Каркас алгоритма поиска с возвратом\"\"\"\n    # Проверка, является ли текущее состояние решением\n    if is_solution(state):\n        # Запись решения\n        record_solution(state, res)\n        # Дальше не продолжаем поиск\n        return\n    # Перебор всех возможных выборов\n    for choice in choices:\n        # Обрезка: проверка допустимости выбора\n        if is_valid(state, choice):\n            # Попытка: сделать выбор и обновить состояние\n            make_choice(state, choice)\n            backtrack(state, choices, res)\n            # Откат: отменить выбор и восстановить предыдущее состояние\n            undo_choice(state, choice)\n
        /* Каркас алгоритма поиска с возвратом */\nvoid backtrack(State *state, vector<Choice *> &choices, vector<State *> &res) {\n    // Проверка, является ли текущее состояние решением\n    if (isSolution(state)) {\n        // Запись решения\n        recordSolution(state, res);\n        // Дальше не продолжаем поиск\n        return;\n    }\n    // Перебор всех возможных выборов\n    for (Choice choice : choices) {\n        // Обрезка: проверка допустимости выбора\n        if (isValid(state, choice)) {\n            // Попытка: сделать выбор и обновить состояние\n            makeChoice(state, choice);\n            backtrack(state, choices, res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            undoChoice(state, choice);\n        }\n    }\n}\n
        /* Каркас алгоритма поиска с возвратом */\nvoid backtrack(State state, List<Choice> choices, List<State> res) {\n    // Проверка, является ли текущее состояние решением\n    if (isSolution(state)) {\n        // Запись решения\n        recordSolution(state, res);\n        // Дальше не продолжаем поиск\n        return;\n    }\n    // Перебор всех возможных выборов\n    for (Choice choice : choices) {\n        // Обрезка: проверка допустимости выбора\n        if (isValid(state, choice)) {\n            // Попытка: сделать выбор и обновить состояние\n            makeChoice(state, choice);\n            backtrack(state, choices, res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            undoChoice(state, choice);\n        }\n    }\n}\n
        /* Каркас алгоритма поиска с возвратом */\nvoid Backtrack(State state, List<Choice> choices, List<State> res) {\n    // Проверка, является ли текущее состояние решением\n    if (IsSolution(state)) {\n        // Запись решения\n        RecordSolution(state, res);\n        // Дальше не продолжаем поиск\n        return;\n    }\n    // Перебор всех возможных выборов\n    foreach (Choice choice in choices) {\n        // Обрезка: проверка допустимости выбора\n        if (IsValid(state, choice)) {\n            // Попытка: сделать выбор и обновить состояние\n            MakeChoice(state, choice);\n            Backtrack(state, choices, res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            UndoChoice(state, choice);\n        }\n    }\n}\n
        /* Каркас алгоритма поиска с возвратом */\nfunc backtrack(state *State, choices []Choice, res *[]State) {\n    // Проверка, является ли текущее состояние решением\n    if isSolution(state) {\n        // Запись решения\n        recordSolution(state, res)\n        // Дальше не продолжаем поиск\n        return\n    }\n    // Перебор всех возможных выборов\n    for _, choice := range choices {\n        // Обрезка: проверка допустимости выбора\n        if isValid(state, choice) {\n            // Попытка: сделать выбор и обновить состояние\n            makeChoice(state, choice)\n            backtrack(state, choices, res)\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            undoChoice(state, choice)\n        }\n    }\n}\n
        /* Каркас алгоритма поиска с возвратом */\nfunc backtrack(state: inout State, choices: [Choice], res: inout [State]) {\n    // Проверка, является ли текущее состояние решением\n    if isSolution(state: state) {\n        // Запись решения\n        recordSolution(state: state, res: &res)\n        // Дальше не продолжаем поиск\n        return\n    }\n    // Перебор всех возможных выборов\n    for choice in choices {\n        // Обрезка: проверка допустимости выбора\n        if isValid(state: state, choice: choice) {\n            // Попытка: сделать выбор и обновить состояние\n            makeChoice(state: &state, choice: choice)\n            backtrack(state: &state, choices: choices, res: &res)\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            undoChoice(state: &state, choice: choice)\n        }\n    }\n}\n
        /* Каркас алгоритма поиска с возвратом */\nfunction backtrack(state, choices, res) {\n    // Проверка, является ли текущее состояние решением\n    if (isSolution(state)) {\n        // Запись решения\n        recordSolution(state, res);\n        // Дальше не продолжаем поиск\n        return;\n    }\n    // Перебор всех возможных выборов\n    for (let choice of choices) {\n        // Обрезка: проверка допустимости выбора\n        if (isValid(state, choice)) {\n            // Попытка: сделать выбор и обновить состояние\n            makeChoice(state, choice);\n            backtrack(state, choices, res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            undoChoice(state, choice);\n        }\n    }\n}\n
        /* Каркас алгоритма поиска с возвратом */\nfunction backtrack(state: State, choices: Choice[], res: State[]): void {\n    // Проверка, является ли текущее состояние решением\n    if (isSolution(state)) {\n        // Запись решения\n        recordSolution(state, res);\n        // Дальше не продолжаем поиск\n        return;\n    }\n    // Перебор всех возможных выборов\n    for (let choice of choices) {\n        // Обрезка: проверка допустимости выбора\n        if (isValid(state, choice)) {\n            // Попытка: сделать выбор и обновить состояние\n            makeChoice(state, choice);\n            backtrack(state, choices, res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            undoChoice(state, choice);\n        }\n    }\n}\n
        /* Каркас алгоритма поиска с возвратом */\nvoid backtrack(State state, List<Choice>, List<State> res) {\n  // Проверка, является ли текущее состояние решением\n  if (isSolution(state)) {\n    // Запись решения\n    recordSolution(state, res);\n    // Дальше не продолжаем поиск\n    return;\n  }\n  // Перебор всех возможных выборов\n  for (Choice choice in choices) {\n    // Обрезка: проверка допустимости выбора\n    if (isValid(state, choice)) {\n      // Попытка: сделать выбор и обновить состояние\n      makeChoice(state, choice);\n      backtrack(state, choices, res);\n      // Откат: отменить выбор и восстановить предыдущее состояние\n      undoChoice(state, choice);\n    }\n  }\n}\n
        /* Каркас алгоритма поиска с возвратом */\nfn backtrack(state: &mut State, choices: &Vec<Choice>, res: &mut Vec<State>) {\n    // Проверка, является ли текущее состояние решением\n    if is_solution(state) {\n        // Запись решения\n        record_solution(state, res);\n        // Дальше не продолжаем поиск\n        return;\n    }\n    // Перебор всех возможных выборов\n    for choice in choices {\n        // Обрезка: проверка допустимости выбора\n        if is_valid(state, choice) {\n            // Попытка: сделать выбор и обновить состояние\n            make_choice(state, choice);\n            backtrack(state, choices, res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            undo_choice(state, choice);\n        }\n    }\n}\n
        /* Каркас алгоритма поиска с возвратом */\nvoid backtrack(State *state, Choice *choices, int numChoices, State *res, int numRes) {\n    // Проверка, является ли текущее состояние решением\n    if (isSolution(state)) {\n        // Запись решения\n        recordSolution(state, res, numRes);\n        // Дальше не продолжаем поиск\n        return;\n    }\n    // Перебор всех возможных выборов\n    for (int i = 0; i < numChoices; i++) {\n        // Обрезка: проверка допустимости выбора\n        if (isValid(state, &choices[i])) {\n            // Попытка: сделать выбор и обновить состояние\n            makeChoice(state, &choices[i]);\n            backtrack(state, choices, numChoices, res, numRes);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            undoChoice(state, &choices[i]);\n        }\n    }\n}\n
        /* Каркас алгоритма поиска с возвратом */\nfun backtrack(state: State?, choices: List<Choice?>, res: List<State?>?) {\n    // Проверка, является ли текущее состояние решением\n    if (isSolution(state)) {\n        // Запись решения\n        recordSolution(state, res)\n        // Дальше не продолжаем поиск\n        return\n    }\n    // Перебор всех возможных выборов\n    for (choice in choices) {\n        // Обрезка: проверка допустимости выбора\n        if (isValid(state, choice)) {\n            // Попытка: сделать выбор и обновить состояние\n            makeChoice(state, choice)\n            backtrack(state, choices, res)\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            undoChoice(state, choice)\n        }\n    }\n}\n
        ### Каркас алгоритма поиска с возвратом ###\ndef backtrack(state, choices, res)\n    # Проверка, является ли текущее состояние решением\n    if is_solution?(state)\n        # Запись решения\n        record_solution(state, res)\n        return\n    end\n\n    # Перебор всех возможных выборов\n    for choice in choices\n        # Обрезка: проверка допустимости выбора\n        if is_valid?(state, choice)\n            # Попытка: сделать выбор и обновить состояние\n            make_choice(state, choice)\n            backtrack(state, choices, res)\n            # Откат: отменить выбор и восстановить предыдущее состояние\n            undo_choice(state, choice)\n        end\n    end\nend\n

        Теперь, опираясь на этот каркас, решим пример 3. Состояние state здесь - это путь обхода узлов, выбор choices - левый и правый потомки текущего узла, а результат res - список путей:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_iii_template.py
        def is_solution(state: list[TreeNode]) -> bool:\n    \"\"\"Проверить, является ли текущее состояние решением\"\"\"\n    return state and state[-1].val == 7\n\ndef record_solution(state: list[TreeNode], res: list[list[TreeNode]]):\n    \"\"\"Записать решение\"\"\"\n    res.append(list(state))\n\ndef is_valid(state: list[TreeNode], choice: TreeNode) -> bool:\n    \"\"\"Проверить, допустим ли этот выбор в текущем состоянии\"\"\"\n    return choice is not None and choice.val != 3\n\ndef make_choice(state: list[TreeNode], choice: TreeNode):\n    \"\"\"Обновить состояние\"\"\"\n    state.append(choice)\n\ndef undo_choice(state: list[TreeNode], choice: TreeNode):\n    \"\"\"Восстановить состояние\"\"\"\n    state.pop()\n\ndef backtrack(\n    state: list[TreeNode], choices: list[TreeNode], res: list[list[TreeNode]]\n):\n    \"\"\"Алгоритм бэктрекинга: пример 3\"\"\"\n    # Проверить, является ли текущее состояние решением\n    if is_solution(state):\n        # Записать решение\n        record_solution(state, res)\n    # Перебор всех вариантов выбора\n    for choice in choices:\n        # Отсечение: проверить допустимость выбора\n        if is_valid(state, choice):\n            # Попытка: сделать выбор и обновить состояние\n            make_choice(state, choice)\n            # Перейти к следующему выбору\n            backtrack(state, [choice.left, choice.right], res)\n            # Откат: отменить выбор и восстановить предыдущее состояние\n            undo_choice(state, choice)\n
        preorder_traversal_iii_template.cpp
        /* Проверить, является ли текущее состояние решением */\nbool isSolution(vector<TreeNode *> &state) {\n    return !state.empty() && state.back()->val == 7;\n}\n\n/* Записать решение */\nvoid recordSolution(vector<TreeNode *> &state, vector<vector<TreeNode *>> &res) {\n    res.push_back(state);\n}\n\n/* Проверить, допустим ли этот выбор в текущем состоянии */\nbool isValid(vector<TreeNode *> &state, TreeNode *choice) {\n    return choice != nullptr && choice->val != 3;\n}\n\n/* Обновить состояние */\nvoid makeChoice(vector<TreeNode *> &state, TreeNode *choice) {\n    state.push_back(choice);\n}\n\n/* Восстановить состояние */\nvoid undoChoice(vector<TreeNode *> &state, TreeNode *choice) {\n    state.pop_back();\n}\n\n/* Алгоритм бэктрекинга: пример 3 */\nvoid backtrack(vector<TreeNode *> &state, vector<TreeNode *> &choices, vector<vector<TreeNode *>> &res) {\n    // Проверить, является ли текущее состояние решением\n    if (isSolution(state)) {\n        // Записать решение\n        recordSolution(state, res);\n    }\n    // Перебор всех вариантов выбора\n    for (TreeNode *choice : choices) {\n        // Отсечение: проверить допустимость выбора\n        if (isValid(state, choice)) {\n            // Попытка: сделать выбор и обновить состояние\n            makeChoice(state, choice);\n            // Перейти к следующему выбору\n            vector<TreeNode *> nextChoices{choice->left, choice->right};\n            backtrack(state, nextChoices, res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            undoChoice(state, choice);\n        }\n    }\n}\n
        preorder_traversal_iii_template.java
        /* Проверить, является ли текущее состояние решением */\nboolean isSolution(List<TreeNode> state) {\n    return !state.isEmpty() && state.get(state.size() - 1).val == 7;\n}\n\n/* Записать решение */\nvoid recordSolution(List<TreeNode> state, List<List<TreeNode>> res) {\n    res.add(new ArrayList<>(state));\n}\n\n/* Проверить, допустим ли этот выбор в текущем состоянии */\nboolean isValid(List<TreeNode> state, TreeNode choice) {\n    return choice != null && choice.val != 3;\n}\n\n/* Обновить состояние */\nvoid makeChoice(List<TreeNode> state, TreeNode choice) {\n    state.add(choice);\n}\n\n/* Восстановить состояние */\nvoid undoChoice(List<TreeNode> state, TreeNode choice) {\n    state.remove(state.size() - 1);\n}\n\n/* Алгоритм бэктрекинга: пример 3 */\nvoid backtrack(List<TreeNode> state, List<TreeNode> choices, List<List<TreeNode>> res) {\n    // Проверить, является ли текущее состояние решением\n    if (isSolution(state)) {\n        // Записать решение\n        recordSolution(state, res);\n    }\n    // Перебор всех вариантов выбора\n    for (TreeNode choice : choices) {\n        // Отсечение: проверить допустимость выбора\n        if (isValid(state, choice)) {\n            // Попытка: сделать выбор и обновить состояние\n            makeChoice(state, choice);\n            // Перейти к следующему выбору\n            backtrack(state, Arrays.asList(choice.left, choice.right), res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            undoChoice(state, choice);\n        }\n    }\n}\n
        preorder_traversal_iii_template.cs
        /* Проверить, является ли текущее состояние решением */\nbool IsSolution(List<TreeNode> state) {\n    return state.Count != 0 && state[^1].val == 7;\n}\n\n/* Записать решение */\nvoid RecordSolution(List<TreeNode> state, List<List<TreeNode>> res) {\n    res.Add(new List<TreeNode>(state));\n}\n\n/* Проверить, допустим ли этот выбор в текущем состоянии */\nbool IsValid(List<TreeNode> state, TreeNode choice) {\n    return choice != null && choice.val != 3;\n}\n\n/* Обновить состояние */\nvoid MakeChoice(List<TreeNode> state, TreeNode choice) {\n    state.Add(choice);\n}\n\n/* Восстановить состояние */\nvoid UndoChoice(List<TreeNode> state, TreeNode choice) {\n    state.RemoveAt(state.Count - 1);\n}\n\n/* Алгоритм бэктрекинга: пример 3 */\nvoid Backtrack(List<TreeNode> state, List<TreeNode> choices, List<List<TreeNode>> res) {\n    // Проверить, является ли текущее состояние решением\n    if (IsSolution(state)) {\n        // Записать решение\n        RecordSolution(state, res);\n    }\n    // Перебор всех вариантов выбора\n    foreach (TreeNode choice in choices) {\n        // Отсечение: проверить допустимость выбора\n        if (IsValid(state, choice)) {\n            // Попытка: сделать выбор и обновить состояние\n            MakeChoice(state, choice);\n            // Перейти к следующему выбору\n            Backtrack(state, [choice.left!, choice.right!], res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            UndoChoice(state, choice);\n        }\n    }\n}\n
        preorder_traversal_iii_template.go
        /* Проверить, является ли текущее состояние решением */\nfunc isSolution(state *[]*TreeNode) bool {\n    return len(*state) != 0 && (*state)[len(*state)-1].Val == 7\n}\n\n/* Записать решение */\nfunc recordSolution(state *[]*TreeNode, res *[][]*TreeNode) {\n    *res = append(*res, append([]*TreeNode{}, *state...))\n}\n\n/* Проверить, допустим ли этот выбор в текущем состоянии */\nfunc isValid(state *[]*TreeNode, choice *TreeNode) bool {\n    return choice != nil && choice.Val != 3\n}\n\n/* Обновить состояние */\nfunc makeChoice(state *[]*TreeNode, choice *TreeNode) {\n    *state = append(*state, choice)\n}\n\n/* Восстановить состояние */\nfunc undoChoice(state *[]*TreeNode, choice *TreeNode) {\n    *state = (*state)[:len(*state)-1]\n}\n\n/* Алгоритм бэктрекинга: пример 3 */\nfunc backtrackIII(state *[]*TreeNode, choices *[]*TreeNode, res *[][]*TreeNode) {\n    // Проверить, является ли текущее состояние решением\n    if isSolution(state) {\n        // Записать решение\n        recordSolution(state, res)\n    }\n    // Перебор всех вариантов выбора\n    for _, choice := range *choices {\n        // Отсечение: проверить допустимость выбора\n        if isValid(state, choice) {\n            // Попытка: сделать выбор и обновить состояние\n            makeChoice(state, choice)\n            // Перейти к следующему выбору\n            temp := make([]*TreeNode, 0)\n            temp = append(temp, choice.Left, choice.Right)\n            backtrackIII(state, &temp, res)\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            undoChoice(state, choice)\n        }\n    }\n}\n
        preorder_traversal_iii_template.swift
        /* Проверить, является ли текущее состояние решением */\nfunc isSolution(state: [TreeNode]) -> Bool {\n    !state.isEmpty && state.last!.val == 7\n}\n\n/* Записать решение */\nfunc recordSolution(state: [TreeNode], res: inout [[TreeNode]]) {\n    res.append(state)\n}\n\n/* Проверить, допустим ли этот выбор в текущем состоянии */\nfunc isValid(state: [TreeNode], choice: TreeNode?) -> Bool {\n    choice != nil && choice!.val != 3\n}\n\n/* Обновить состояние */\nfunc makeChoice(state: inout [TreeNode], choice: TreeNode) {\n    state.append(choice)\n}\n\n/* Восстановить состояние */\nfunc undoChoice(state: inout [TreeNode], choice: TreeNode) {\n    state.removeLast()\n}\n\n/* Алгоритм бэктрекинга: пример 3 */\nfunc backtrack(state: inout [TreeNode], choices: [TreeNode], res: inout [[TreeNode]]) {\n    // Проверить, является ли текущее состояние решением\n    if isSolution(state: state) {\n        recordSolution(state: state, res: &res)\n    }\n    // Перебор всех вариантов выбора\n    for choice in choices {\n        // Отсечение: проверить допустимость выбора\n        if isValid(state: state, choice: choice) {\n            // Попытка: сделать выбор и обновить состояние\n            makeChoice(state: &state, choice: choice)\n            // Перейти к следующему выбору\n            backtrack(state: &state, choices: [choice.left, choice.right].compactMap { $0 }, res: &res)\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            undoChoice(state: &state, choice: choice)\n        }\n    }\n}\n
        preorder_traversal_iii_template.js
        /* Проверить, является ли текущее состояние решением */\nfunction isSolution(state) {\n    return state && state[state.length - 1]?.val === 7;\n}\n\n/* Записать решение */\nfunction recordSolution(state, res) {\n    res.push([...state]);\n}\n\n/* Проверить, допустим ли этот выбор в текущем состоянии */\nfunction isValid(state, choice) {\n    return choice !== null && choice.val !== 3;\n}\n\n/* Обновить состояние */\nfunction makeChoice(state, choice) {\n    state.push(choice);\n}\n\n/* Восстановить состояние */\nfunction undoChoice(state) {\n    state.pop();\n}\n\n/* Алгоритм бэктрекинга: пример 3 */\nfunction backtrack(state, choices, res) {\n    // Проверить, является ли текущее состояние решением\n    if (isSolution(state)) {\n        // Записать решение\n        recordSolution(state, res);\n    }\n    // Перебор всех вариантов выбора\n    for (const choice of choices) {\n        // Отсечение: проверить допустимость выбора\n        if (isValid(state, choice)) {\n            // Попытка: сделать выбор и обновить состояние\n            makeChoice(state, choice);\n            // Перейти к следующему выбору\n            backtrack(state, [choice.left, choice.right], res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            undoChoice(state);\n        }\n    }\n}\n
        preorder_traversal_iii_template.ts
        /* Проверить, является ли текущее состояние решением */\nfunction isSolution(state: TreeNode[]): boolean {\n    return state && state[state.length - 1]?.val === 7;\n}\n\n/* Записать решение */\nfunction recordSolution(state: TreeNode[], res: TreeNode[][]): void {\n    res.push([...state]);\n}\n\n/* Проверить, допустим ли этот выбор в текущем состоянии */\nfunction isValid(state: TreeNode[], choice: TreeNode): boolean {\n    return choice !== null && choice.val !== 3;\n}\n\n/* Обновить состояние */\nfunction makeChoice(state: TreeNode[], choice: TreeNode): void {\n    state.push(choice);\n}\n\n/* Восстановить состояние */\nfunction undoChoice(state: TreeNode[]): void {\n    state.pop();\n}\n\n/* Алгоритм бэктрекинга: пример 3 */\nfunction backtrack(\n    state: TreeNode[],\n    choices: TreeNode[],\n    res: TreeNode[][]\n): void {\n    // Проверить, является ли текущее состояние решением\n    if (isSolution(state)) {\n        // Записать решение\n        recordSolution(state, res);\n    }\n    // Перебор всех вариантов выбора\n    for (const choice of choices) {\n        // Отсечение: проверить допустимость выбора\n        if (isValid(state, choice)) {\n            // Попытка: сделать выбор и обновить состояние\n            makeChoice(state, choice);\n            // Перейти к следующему выбору\n            backtrack(state, [choice.left, choice.right], res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            undoChoice(state);\n        }\n    }\n}\n
        preorder_traversal_iii_template.dart
        /* Проверить, является ли текущее состояние решением */\nbool isSolution(List<TreeNode> state) {\n  return state.isNotEmpty && state.last.val == 7;\n}\n\n/* Записать решение */\nvoid recordSolution(List<TreeNode> state, List<List<TreeNode>> res) {\n  res.add(List.from(state));\n}\n\n/* Проверить, допустим ли этот выбор в текущем состоянии */\nbool isValid(List<TreeNode> state, TreeNode? choice) {\n  return choice != null && choice.val != 3;\n}\n\n/* Обновить состояние */\nvoid makeChoice(List<TreeNode> state, TreeNode? choice) {\n  state.add(choice!);\n}\n\n/* Восстановить состояние */\nvoid undoChoice(List<TreeNode> state, TreeNode? choice) {\n  state.removeLast();\n}\n\n/* Алгоритм бэктрекинга: пример 3 */\nvoid backtrack(\n  List<TreeNode> state,\n  List<TreeNode?> choices,\n  List<List<TreeNode>> res,\n) {\n  // Проверить, является ли текущее состояние решением\n  if (isSolution(state)) {\n    // Записать решение\n    recordSolution(state, res);\n  }\n  // Перебор всех вариантов выбора\n  for (TreeNode? choice in choices) {\n    // Отсечение: проверить допустимость выбора\n    if (isValid(state, choice)) {\n      // Попытка: сделать выбор и обновить состояние\n      makeChoice(state, choice);\n      // Перейти к следующему выбору\n      backtrack(state, [choice!.left, choice.right], res);\n      // Откат: отменить выбор и восстановить предыдущее состояние\n      undoChoice(state, choice);\n    }\n  }\n}\n
        preorder_traversal_iii_template.rs
        /* Проверить, является ли текущее состояние решением */\nfn is_solution(state: &mut Vec<Rc<RefCell<TreeNode>>>) -> bool {\n    return !state.is_empty() && state.last().unwrap().borrow().val == 7;\n}\n\n/* Записать решение */\nfn record_solution(\n    state: &mut Vec<Rc<RefCell<TreeNode>>>,\n    res: &mut Vec<Vec<Rc<RefCell<TreeNode>>>>,\n) {\n    res.push(state.clone());\n}\n\n/* Проверить, допустим ли этот выбор в текущем состоянии */\nfn is_valid(_: &mut Vec<Rc<RefCell<TreeNode>>>, choice: Option<&Rc<RefCell<TreeNode>>>) -> bool {\n    return choice.is_some() && choice.unwrap().borrow().val != 3;\n}\n\n/* Обновить состояние */\nfn make_choice(state: &mut Vec<Rc<RefCell<TreeNode>>>, choice: Rc<RefCell<TreeNode>>) {\n    state.push(choice);\n}\n\n/* Восстановить состояние */\nfn undo_choice(state: &mut Vec<Rc<RefCell<TreeNode>>>, _: Rc<RefCell<TreeNode>>) {\n    state.pop();\n}\n\n/* Алгоритм бэктрекинга: пример 3 */\nfn backtrack(\n    state: &mut Vec<Rc<RefCell<TreeNode>>>,\n    choices: &Vec<Option<&Rc<RefCell<TreeNode>>>>,\n    res: &mut Vec<Vec<Rc<RefCell<TreeNode>>>>,\n) {\n    // Проверить, является ли текущее состояние решением\n    if is_solution(state) {\n        // Записать решение\n        record_solution(state, res);\n    }\n    // Перебор всех вариантов выбора\n    for &choice in choices.iter() {\n        // Отсечение: проверить допустимость выбора\n        if is_valid(state, choice) {\n            // Попытка: сделать выбор и обновить состояние\n            make_choice(state, choice.unwrap().clone());\n            // Перейти к следующему выбору\n            backtrack(\n                state,\n                &vec![\n                    choice.unwrap().borrow().left.as_ref(),\n                    choice.unwrap().borrow().right.as_ref(),\n                ],\n                res,\n            );\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            undo_choice(state, choice.unwrap().clone());\n        }\n    }\n}\n
        preorder_traversal_iii_template.c
        /* Проверить, является ли текущее состояние решением */\nbool isSolution(void) {\n    return pathSize > 0 && path[pathSize - 1]->val == 7;\n}\n\n/* Записать решение */\nvoid recordSolution(void) {\n    for (int i = 0; i < pathSize; i++) {\n        res[resSize][i] = path[i];\n    }\n    resSize++;\n}\n\n/* Проверить, допустим ли этот выбор в текущем состоянии */\nbool isValid(TreeNode *choice) {\n    return choice != NULL && choice->val != 3;\n}\n\n/* Обновить состояние */\nvoid makeChoice(TreeNode *choice) {\n    path[pathSize++] = choice;\n}\n\n/* Восстановить состояние */\nvoid undoChoice(void) {\n    pathSize--;\n}\n\n/* Алгоритм бэктрекинга: пример 3 */\nvoid backtrack(TreeNode *choices[2]) {\n    // Проверить, является ли текущее состояние решением\n    if (isSolution()) {\n        // Записать решение\n        recordSolution();\n    }\n    // Перебор всех вариантов выбора\n    for (int i = 0; i < 2; i++) {\n        TreeNode *choice = choices[i];\n        // Отсечение: проверить допустимость выбора\n        if (isValid(choice)) {\n            // Попытка: сделать выбор и обновить состояние\n            makeChoice(choice);\n            // Перейти к следующему выбору\n            TreeNode *nextChoices[2] = {choice->left, choice->right};\n            backtrack(nextChoices);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            undoChoice();\n        }\n    }\n}\n
        preorder_traversal_iii_template.kt
        /* Проверить, является ли текущее состояние решением */\nfun isSolution(state: MutableList<TreeNode?>): Boolean {\n    return state.isNotEmpty() && state[state.size - 1]?._val == 7\n}\n\n/* Записать решение */\nfun recordSolution(state: MutableList<TreeNode?>?, res: MutableList<MutableList<TreeNode?>?>) {\n    res.add(state!!.toMutableList())\n}\n\n/* Проверить, допустим ли этот выбор в текущем состоянии */\nfun isValid(state: MutableList<TreeNode?>?, choice: TreeNode?): Boolean {\n    return choice != null && choice._val != 3\n}\n\n/* Обновить состояние */\nfun makeChoice(state: MutableList<TreeNode?>, choice: TreeNode?) {\n    state.add(choice)\n}\n\n/* Восстановить состояние */\nfun undoChoice(state: MutableList<TreeNode?>, choice: TreeNode?) {\n    state.removeLast()\n}\n\n/* Алгоритм бэктрекинга: пример 3 */\nfun backtrack(\n    state: MutableList<TreeNode?>,\n    choices: MutableList<TreeNode?>,\n    res: MutableList<MutableList<TreeNode?>?>\n) {\n    // Проверить, является ли текущее состояние решением\n    if (isSolution(state)) {\n        // Записать решение\n        recordSolution(state, res)\n    }\n    // Перебор всех вариантов выбора\n    for (choice in choices) {\n        // Отсечение: проверить допустимость выбора\n        if (isValid(state, choice)) {\n            // Попытка: сделать выбор и обновить состояние\n            makeChoice(state, choice)\n            // Перейти к следующему выбору\n            backtrack(state, mutableListOf(choice!!.left, choice.right), res)\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            undoChoice(state, choice)\n        }\n    }\n}\n
        preorder_traversal_iii_template.rb
        ### Проверка, является ли текущее состояние решением ###\ndef is_solution?(state)\n  !state.empty? && state.last.val == 7\nend\n\n### Записать решение ###\ndef record_solution(state, res)\n  res << state.dup\nend\n\n### Проверка допустимости этого выбора в текущем состоянии ###\ndef is_valid?(state, choice)\n  choice && choice.val != 3\nend\n\n### Обновить состояние ###\ndef make_choice(state, choice)\n  state << choice\nend\n\n### Восстановить состояние ###\ndef undo_choice(state, choice)\n  state.pop\nend\n\n### Алгоритм бэктрекинга: пример 3 ###\ndef backtrack(state, choices, res)\n  # Проверить, является ли текущее состояние решением\n  record_solution(state, res) if is_solution?(state)\n\n  # Перебор всех вариантов выбора\n  for choice in choices\n    # Отсечение: проверить допустимость выбора\n    if is_valid?(state, choice)\n      # Попытка: сделать выбор и обновить состояние\n      make_choice(state, choice)\n      # Перейти к следующему выбору\n      backtrack(state, [choice.left, choice.right], res)\n      # Откат: отменить выбор и восстановить предыдущее состояние\n      undo_choice(state, choice)\n    end\n  end\nend\n
        Визуализация кода

        Во весь экран >

        Согласно условию задачи, после нахождения узла со значением \\(7\\) мы должны продолжать поиск, поэтому оператор return после записи решения нужно удалить. На рисунке 13-4 сравниваются процессы поиска в случаях, когда return сохраняется и когда он удаляется.

        Рисунок 13-4   Сравнение поиска при сохранении и удалении return

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

        ","path":["Глава 13. Поиск с возвратом","13.1   Алгоритм поиска с возвратом"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1314","level":2,"title":"13.1.4   Часто используемые термины","text":"

        Чтобы яснее анализировать алгоритмические задачи, подытожим значения часто используемых терминов поиска с возвратом и сопоставим их с примером 3, как показано в таблице 13-1.

        Таблица 13-1   Часто используемые термины алгоритма поиска с возвратом

        Термин Определение Пример 3 Решение (solution) Решение - это ответ, удовлетворяющий условиям задачи. Решений может быть одно или несколько Все пути от корня до узла \\(7\\) , удовлетворяющие ограничениям Ограничение (constraint) Ограничение определяет допустимость решения и обычно используется для обрезки Путь не содержит узлы со значением \\(3\\) Состояние (state) Состояние описывает ситуацию задачи в некоторый момент времени, включая уже сделанные выборы Текущий путь посещенных узлов, то есть список узлов path Попытка (attempt) Попытка - это исследование пространства решений на основе доступных выборов, включая выбор, обновление состояния и проверку, является ли состояние решением Рекурсивный переход к левому или правому потомку, добавление узла в path и проверка, равно ли значение узла \\(7\\) Откат (backtracking) Откат означает отмену предыдущих выборов и возврат к более раннему состоянию при встрече состояния, не удовлетворяющего ограничениям Завершение поиска при проходе через лист, окончании посещения узла или встрече узла со значением \\(3\\) , то есть возврат из функции Обрезка (pruning) Обрезка - это способ избегать бессмысленных путей поиска на основе свойств задачи и ее ограничений, повышающий эффективность При встрече узла со значением \\(3\\) поиск по этой ветви прекращается

        Tip

        Такие понятия, как задача, решение и состояние, являются общими и встречаются не только в поиске с возвратом, но и в «разделяй и властвуй», динамическом программировании, жадных алгоритмах и других темах.

        ","path":["Глава 13. Поиск с возвратом","13.1   Алгоритм поиска с возвратом"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1315","level":2,"title":"13.1.5   Преимущества и ограничения","text":"

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

        Однако при работе с большими или сложными задачами эффективность поиска с возвратом может оказаться неприемлемой.

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

        Тем не менее поиск с возвратом по-прежнему остается лучшим решением для некоторых поисковых задач и задач удовлетворения ограничений. В таких задачах заранее невозможно предсказать, какие выборы приведут к эффективному решению, поэтому приходится перебирать все возможные варианты. В этой ситуации ключевым становится вопрос оптимизации эффективности , и для этого обычно используют две стратегии.

        • Обрезка: избегать поиска по тем путям, которые заведомо не приведут к решению, тем самым экономя время и память.
        • Эвристический поиск: вводить во время поиска дополнительные стратегии или оценки, чтобы в первую очередь исследовать пути, наиболее вероятно ведущие к эффективному решению.
        ","path":["Глава 13. Поиск с возвратом","13.1   Алгоритм поиска с возвратом"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1316","level":2,"title":"13.1.6   Типичные задачи поиска с возвратом","text":"

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

        Поисковые задачи: целью таких задач является поиск решений, удовлетворяющих определенным условиям.

        • Задача о перестановках: дано множество, требуется найти все возможные перестановки его элементов.
        • Задача о сумме подмножеств: даны множество и целевая сумма. Нужно найти все подмножества, сумма элементов которых равна целевой.
        • Задача о Ханойской башне: даны три стержня и набор дисков разного размера. Требуется перенести все диски с одного стержня на другой, перемещая за раз только один диск и не помещая больший диск на меньший.

        Задачи удовлетворения ограничений: целью таких задач является поиск решений, удовлетворяющих всем ограничениям.

        • Задача о \\(n\\) ферзях: разместить \\(n\\) ферзей на шахматной доске размера \\(n \\times n\\) так, чтобы они не атаковали друг друга.
        • Судоку: заполнить сетку \\(9 \\times 9\\) числами от \\(1\\) до \\(9\\) так, чтобы в каждой строке, каждом столбце и каждом блоке \\(3 \\times 3\\) числа не повторялись.
        • Задача раскраски графа: дан неориентированный граф. Требуется раскрасить его вершины минимальным числом цветов так, чтобы соседние вершины имели разные цвета.

        Задачи комбинаторной оптимизации: целью таких задач является поиск оптимального решения в некотором комбинаторном пространстве при заданных ограничениях.

        • Задача о рюкзаке 0-1: даны набор предметов и рюкзак. У каждого предмета есть ценность и вес, и нужно выбрать предметы так, чтобы при ограниченной вместимости рюкзака суммарная ценность была максимальной.
        • Задача коммивояжера: начиная из некоторой вершины графа, требуется посетить все остальные вершины ровно по одному разу и вернуться в исходную вершину, найдя при этом кратчайший путь.
        • Задача о максимальной клике: дан неориентированный граф. Требуется найти в нем максимальный полный подграф, то есть подграф, в котором любая пара вершин соединена ребром.

        Стоит отметить: для многих задач комбинаторной оптимизации поиск с возвратом не является оптимальным способом решения.

        • Задача о рюкзаке 0-1 обычно решается с помощью динамического программирования, что дает более высокую временную эффективность.
        • Задача коммивояжера является известной NP-Hard задачей. Для ее решения часто используют генетические алгоритмы, муравьиные алгоритмы и другие методы.
        • Задача о максимальной клике является классической задачей теории графов и может решаться жадными и другими эвристическими алгоритмами.
        ","path":["Глава 13. Поиск с возвратом","13.1   Алгоритм поиска с возвратом"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/","level":1,"title":"13.4   Задача о n ферзях","text":"

        Question

        Согласно правилам шахмат ферзь может атаковать фигуры, находящиеся с ним на одной строке, в одном столбце или на одной диагонали. Даны \\(n\\) ферзей и шахматная доска размера \\(n \\times n\\). Требуется найти такие расстановки, при которых ни одна пара ферзей не может атаковать друг друга.

        Как показано на рисунке 13-15, при \\(n = 4\\) существует два решения. С точки зрения поиска с возвратом доска размера \\(n \\times n\\) содержит \\(n^2\\) клеток, которые образуют все возможные выборы choices . По мере поочередного размещения ферзей состояние доски непрерывно меняется, и текущее содержимое доски образует состояние state .

        Рисунок 13-15   Решения задачи о 4 ферзях

        На рисунке 13-16 показаны три ограничения этой задачи: несколько ферзей не могут находиться на одной строке, в одном столбце или на одной диагонали. При этом нужно помнить, что диагонали бывают двух типов: главная \\ и побочная / .

        Рисунок 13-16   Ограничения задачи о n ферзях

        ","path":["Глава 13. Поиск с возвратом","13.4   Задача о n ферзях"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/#1","level":3,"title":"1.   Построчная стратегия размещения","text":"

        Число ферзей и число строк доски одинаково и равно \\(n\\) , поэтому легко получить следующий вывод: в каждой строке доски разрешено и нужно разместить ровно одного ферзя.

        Иначе говоря, можно использовать построчную стратегию: начиная с первой строки, размещать по одному ферзю в каждой строке, пока не будет достигнута последняя.

        На рисунке 13-17 показан процесс построчного размещения для задачи о 4 ферзях. Из-за ограничений размера изображения на рисунке 13-17 показана лишь одна ветвь поиска для первой строки, а все варианты, не удовлетворяющие ограничениям по столбцам и диагоналям, были отсечены.

        Рисунок 13-17   Построчная стратегия размещения

        По своей сути построчная стратегия сама по себе выполняет роль обрезки , потому что заранее исключает все ветви поиска, в которых в одной строке оказалось бы несколько ферзей.

        ","path":["Глава 13. Поиск с возвратом","13.4   Задача о n ферзях"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/#2","level":3,"title":"2.   Обрезка по столбцам и диагоналям","text":"

        Чтобы удовлетворить ограничению по столбцам, можно использовать булев массив cols длины \\(n\\) , который записывает, есть ли ферзь в каждом столбце. Перед каждым размещением мы используем cols для отсечения столбцов, уже занятых ферзями, а затем динамически обновляем состояние cols во время отката.

        Tip

        Обратите внимание: начало координат матрицы находится в левом верхнем углу, при этом индексы строк растут сверху вниз, а индексы столбцов - слева направо.

        Как теперь обработать ограничения по диагоналям? Пусть клетка на доске имеет координаты \\((row, col)\\) . Выбрав некоторую главную диагональ в матрице, можно заметить, что разность индексов строки и столбца одинакова для всех клеток этой диагонали, то есть для всех клеток главной диагонали значение \\(row - col\\) постоянно.

        Это означает, что если для двух клеток выполняется равенство \\(row_1 - col_1 = row_2 - col_2\\) , то они обязательно лежат на одной и той же главной диагонали. Используя это правило, можно с помощью массива diags1 , показанного на рисунке 13-18, отмечать наличие ферзя на каждой главной диагонали.

        Аналогично для всех клеток побочной диагонали значение \\(row + col\\) является постоянным. Поэтому для обработки ограничений по побочным диагоналям можно использовать еще один массив diags2 .

        Рисунок 13-18   Обработка ограничений по столбцам и диагоналям

        ","path":["Глава 13. Поиск с возвратом","13.4   Задача о n ферзях"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/#3","level":3,"title":"3.   Реализация кода","text":"

        Заметим, что в квадратной матрице размера \\(n\\) диапазон значений \\(row - col\\) равен \\([-n + 1, n - 1]\\) , а диапазон значений \\(row + col\\) равен \\([0, 2n - 2]\\) . Следовательно, число главных и побочных диагоналей равно \\(2n - 1\\) , а значит, длины массивов diags1 и diags2 тоже равны \\(2n - 1\\) .

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby n_queens.py
        def backtrack(\n    row: int,\n    n: int,\n    state: list[list[str]],\n    res: list[list[list[str]]],\n    cols: list[bool],\n    diags1: list[bool],\n    diags2: list[bool],\n):\n    \"\"\"Алгоритм бэктрекинга: n ферзей\"\"\"\n    # Когда все строки уже обработаны, записать решение\n    if row == n:\n        res.append([list(row) for row in state])\n        return\n    # Обойти все столбцы\n    for col in range(n):\n        # Вычислить главную и побочную диагонали, соответствующие этой клетке\n        diag1 = row - col + n - 1\n        diag2 = row + col\n        # Отсечение: в столбце, главной диагонали и побочной диагонали этой клетки не должно быть ферзей\n        if not cols[col] and not diags1[diag1] and not diags2[diag2]:\n            # Попытка: поставить ферзя в эту клетку\n            state[row][col] = \"Q\"\n            cols[col] = diags1[diag1] = diags2[diag2] = True\n            # Перейти к размещению следующей строки\n            backtrack(row + 1, n, state, res, cols, diags1, diags2)\n            # Откат: восстановить эту клетку как пустую\n            state[row][col] = \"#\"\n            cols[col] = diags1[diag1] = diags2[diag2] = False\n\ndef n_queens(n: int) -> list[list[list[str]]]:\n    \"\"\"Решить задачу о n ферзях\"\"\"\n    # Инициализировать доску размера n*n, где 'Q' обозначает ферзя, а '#' — пустую клетку\n    state = [[\"#\" for _ in range(n)] for _ in range(n)]\n    cols = [False] * n  # Отмечать, есть ли ферзь в столбце\n    diags1 = [False] * (2 * n - 1)  # Отмечать наличие ферзя на главной диагонали\n    diags2 = [False] * (2 * n - 1)  # Отмечать наличие ферзя на побочной диагонали\n    res = []\n    backtrack(0, n, state, res, cols, diags1, diags2)\n\n    return res\n
        n_queens.cpp
        /* Алгоритм бэктрекинга: n ферзей */\nvoid backtrack(int row, int n, vector<vector<string>> &state, vector<vector<vector<string>>> &res, vector<bool> &cols,\n               vector<bool> &diags1, vector<bool> &diags2) {\n    // Когда все строки уже обработаны, записать решение\n    if (row == n) {\n        res.push_back(state);\n        return;\n    }\n    // Обойти все столбцы\n    for (int col = 0; col < n; col++) {\n        // Вычислить главную и побочную диагонали, соответствующие этой клетке\n        int diag1 = row - col + n - 1;\n        int diag2 = row + col;\n        // Отсечение: в столбце, главной диагонали и побочной диагонали этой клетки не должно быть ферзей\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Попытка: поставить ферзя в эту клетку\n            state[row][col] = \"Q\";\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // Перейти к размещению следующей строки\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // Откат: восстановить эту клетку как пустую\n            state[row][col] = \"#\";\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* Решить задачу о n ферзях */\nvector<vector<vector<string>>> nQueens(int n) {\n    // Инициализировать доску размера n*n, где 'Q' обозначает ферзя, а '#' — пустую клетку\n    vector<vector<string>> state(n, vector<string>(n, \"#\"));\n    vector<bool> cols(n, false);           // Отмечать, есть ли ферзь в столбце\n    vector<bool> diags1(2 * n - 1, false); // Отмечать наличие ферзя на главной диагонали\n    vector<bool> diags2(2 * n - 1, false); // Отмечать наличие ферзя на побочной диагонали\n    vector<vector<vector<string>>> res;\n\n    backtrack(0, n, state, res, cols, diags1, diags2);\n\n    return res;\n}\n
        n_queens.java
        /* Алгоритм бэктрекинга: n ферзей */\nvoid backtrack(int row, int n, List<List<String>> state, List<List<List<String>>> res,\n        boolean[] cols, boolean[] diags1, boolean[] diags2) {\n    // Когда все строки уже обработаны, записать решение\n    if (row == n) {\n        List<List<String>> copyState = new ArrayList<>();\n        for (List<String> sRow : state) {\n            copyState.add(new ArrayList<>(sRow));\n        }\n        res.add(copyState);\n        return;\n    }\n    // Обойти все столбцы\n    for (int col = 0; col < n; col++) {\n        // Вычислить главную и побочную диагонали, соответствующие этой клетке\n        int diag1 = row - col + n - 1;\n        int diag2 = row + col;\n        // Отсечение: в столбце, главной диагонали и побочной диагонали этой клетки не должно быть ферзей\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Попытка: поставить ферзя в эту клетку\n            state.get(row).set(col, \"Q\");\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // Перейти к размещению следующей строки\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // Откат: восстановить эту клетку как пустую\n            state.get(row).set(col, \"#\");\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* Решить задачу о n ферзях */\nList<List<List<String>>> nQueens(int n) {\n    // Инициализировать доску размера n*n, где 'Q' обозначает ферзя, а '#' — пустую клетку\n    List<List<String>> state = new ArrayList<>();\n    for (int i = 0; i < n; i++) {\n        List<String> row = new ArrayList<>();\n        for (int j = 0; j < n; j++) {\n            row.add(\"#\");\n        }\n        state.add(row);\n    }\n    boolean[] cols = new boolean[n]; // Отмечать, есть ли ферзь в столбце\n    boolean[] diags1 = new boolean[2 * n - 1]; // Отмечать наличие ферзя на главной диагонали\n    boolean[] diags2 = new boolean[2 * n - 1]; // Отмечать наличие ферзя на побочной диагонали\n    List<List<List<String>>> res = new ArrayList<>();\n\n    backtrack(0, n, state, res, cols, diags1, diags2);\n\n    return res;\n}\n
        n_queens.cs
        /* Алгоритм бэктрекинга: n ферзей */\nvoid Backtrack(int row, int n, List<List<string>> state, List<List<List<string>>> res,\n        bool[] cols, bool[] diags1, bool[] diags2) {\n    // Когда все строки уже обработаны, записать решение\n    if (row == n) {\n        List<List<string>> copyState = [];\n        foreach (List<string> sRow in state) {\n            copyState.Add(new List<string>(sRow));\n        }\n        res.Add(copyState);\n        return;\n    }\n    // Обойти все столбцы\n    for (int col = 0; col < n; col++) {\n        // Вычислить главную и побочную диагонали, соответствующие этой клетке\n        int diag1 = row - col + n - 1;\n        int diag2 = row + col;\n        // Отсечение: в столбце, главной диагонали и побочной диагонали этой клетки не должно быть ферзей\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Попытка: поставить ферзя в эту клетку\n            state[row][col] = \"Q\";\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // Перейти к размещению следующей строки\n            Backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // Откат: восстановить эту клетку как пустую\n            state[row][col] = \"#\";\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* Решить задачу о n ферзях */\nList<List<List<string>>> NQueens(int n) {\n    // Инициализировать доску размера n*n, где 'Q' обозначает ферзя, а '#' — пустую клетку\n    List<List<string>> state = [];\n    for (int i = 0; i < n; i++) {\n        List<string> row = [];\n        for (int j = 0; j < n; j++) {\n            row.Add(\"#\");\n        }\n        state.Add(row);\n    }\n    bool[] cols = new bool[n]; // Отмечать, есть ли ферзь в столбце\n    bool[] diags1 = new bool[2 * n - 1]; // Отмечать наличие ферзя на главной диагонали\n    bool[] diags2 = new bool[2 * n - 1]; // Отмечать наличие ферзя на побочной диагонали\n    List<List<List<string>>> res = [];\n\n    Backtrack(0, n, state, res, cols, diags1, diags2);\n\n    return res;\n}\n
        n_queens.go
        /* Алгоритм бэктрекинга: n ферзей */\nfunc backtrack(row, n int, state *[][]string, res *[][][]string, cols, diags1, diags2 *[]bool) {\n    // Когда все строки уже обработаны, записать решение\n    if row == n {\n        newState := make([][]string, len(*state))\n        for i, _ := range newState {\n            newState[i] = make([]string, len((*state)[0]))\n            copy(newState[i], (*state)[i])\n\n        }\n        *res = append(*res, newState)\n        return\n    }\n    // Обойти все столбцы\n    for col := 0; col < n; col++ {\n        // Вычислить главную и побочную диагонали, соответствующие этой клетке\n        diag1 := row - col + n - 1\n        diag2 := row + col\n        // Отсечение: в столбце, главной диагонали и побочной диагонали этой клетки не должно быть ферзей\n        if !(*cols)[col] && !(*diags1)[diag1] && !(*diags2)[diag2] {\n            // Попытка: поставить ферзя в эту клетку\n            (*state)[row][col] = \"Q\"\n            (*cols)[col], (*diags1)[diag1], (*diags2)[diag2] = true, true, true\n            // Перейти к размещению следующей строки\n            backtrack(row+1, n, state, res, cols, diags1, diags2)\n            // Откат: восстановить эту клетку как пустую\n            (*state)[row][col] = \"#\"\n            (*cols)[col], (*diags1)[diag1], (*diags2)[diag2] = false, false, false\n        }\n    }\n}\n\n/* Решить задачу о n ферзях */\nfunc nQueens(n int) [][][]string {\n    // Инициализировать доску размера n*n, где 'Q' обозначает ферзя, а '#' — пустую клетку\n    state := make([][]string, n)\n    for i := 0; i < n; i++ {\n        row := make([]string, n)\n        for i := 0; i < n; i++ {\n            row[i] = \"#\"\n        }\n        state[i] = row\n    }\n    // Отмечать, есть ли ферзь в столбце\n    cols := make([]bool, n)\n    diags1 := make([]bool, 2*n-1)\n    diags2 := make([]bool, 2*n-1)\n    res := make([][][]string, 0)\n    backtrack(0, n, &state, &res, &cols, &diags1, &diags2)\n    return res\n}\n
        n_queens.swift
        /* Алгоритм бэктрекинга: n ферзей */\nfunc backtrack(row: Int, n: Int, state: inout [[String]], res: inout [[[String]]], cols: inout [Bool], diags1: inout [Bool], diags2: inout [Bool]) {\n    // Когда все строки уже обработаны, записать решение\n    if row == n {\n        res.append(state)\n        return\n    }\n    // Обойти все столбцы\n    for col in 0 ..< n {\n        // Вычислить главную и побочную диагонали, соответствующие этой клетке\n        let diag1 = row - col + n - 1\n        let diag2 = row + col\n        // Отсечение: в столбце, главной диагонали и побочной диагонали этой клетки не должно быть ферзей\n        if !cols[col] && !diags1[diag1] && !diags2[diag2] {\n            // Попытка: поставить ферзя в эту клетку\n            state[row][col] = \"Q\"\n            cols[col] = true\n            diags1[diag1] = true\n            diags2[diag2] = true\n            // Перейти к размещению следующей строки\n            backtrack(row: row + 1, n: n, state: &state, res: &res, cols: &cols, diags1: &diags1, diags2: &diags2)\n            // Откат: восстановить эту клетку как пустую\n            state[row][col] = \"#\"\n            cols[col] = false\n            diags1[diag1] = false\n            diags2[diag2] = false\n        }\n    }\n}\n\n/* Решить задачу о n ферзях */\nfunc nQueens(n: Int) -> [[[String]]] {\n    // Инициализировать доску размера n*n, где 'Q' обозначает ферзя, а '#' — пустую клетку\n    var state = Array(repeating: Array(repeating: \"#\", count: n), count: n)\n    var cols = Array(repeating: false, count: n) // Отмечать, есть ли ферзь в столбце\n    var diags1 = Array(repeating: false, count: 2 * n - 1) // Отмечать наличие ферзя на главной диагонали\n    var diags2 = Array(repeating: false, count: 2 * n - 1) // Отмечать наличие ферзя на побочной диагонали\n    var res: [[[String]]] = []\n\n    backtrack(row: 0, n: n, state: &state, res: &res, cols: &cols, diags1: &diags1, diags2: &diags2)\n\n    return res\n}\n
        n_queens.js
        /* Алгоритм бэктрекинга: n ферзей */\nfunction backtrack(row, n, state, res, cols, diags1, diags2) {\n    // Когда все строки уже обработаны, записать решение\n    if (row === n) {\n        res.push(state.map((row) => row.slice()));\n        return;\n    }\n    // Обойти все столбцы\n    for (let col = 0; col < n; col++) {\n        // Вычислить главную и побочную диагонали, соответствующие этой клетке\n        const diag1 = row - col + n - 1;\n        const diag2 = row + col;\n        // Отсечение: в столбце, главной диагонали и побочной диагонали этой клетки не должно быть ферзей\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Попытка: поставить ферзя в эту клетку\n            state[row][col] = 'Q';\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // Перейти к размещению следующей строки\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // Откат: восстановить эту клетку как пустую\n            state[row][col] = '#';\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* Решить задачу о n ферзях */\nfunction nQueens(n) {\n    // Инициализировать доску размера n*n, где 'Q' обозначает ферзя, а '#' — пустую клетку\n    const state = Array.from({ length: n }, () => Array(n).fill('#'));\n    const cols = Array(n).fill(false); // Отмечать, есть ли ферзь в столбце\n    const diags1 = Array(2 * n - 1).fill(false); // Отмечать наличие ферзя на главной диагонали\n    const diags2 = Array(2 * n - 1).fill(false); // Отмечать наличие ферзя на побочной диагонали\n    const res = [];\n\n    backtrack(0, n, state, res, cols, diags1, diags2);\n    return res;\n}\n
        n_queens.ts
        /* Алгоритм бэктрекинга: n ферзей */\nfunction backtrack(\n    row: number,\n    n: number,\n    state: string[][],\n    res: string[][][],\n    cols: boolean[],\n    diags1: boolean[],\n    diags2: boolean[]\n): void {\n    // Когда все строки уже обработаны, записать решение\n    if (row === n) {\n        res.push(state.map((row) => row.slice()));\n        return;\n    }\n    // Обойти все столбцы\n    for (let col = 0; col < n; col++) {\n        // Вычислить главную и побочную диагонали, соответствующие этой клетке\n        const diag1 = row - col + n - 1;\n        const diag2 = row + col;\n        // Отсечение: в столбце, главной диагонали и побочной диагонали этой клетки не должно быть ферзей\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Попытка: поставить ферзя в эту клетку\n            state[row][col] = 'Q';\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // Перейти к размещению следующей строки\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // Откат: восстановить эту клетку как пустую\n            state[row][col] = '#';\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* Решить задачу о n ферзях */\nfunction nQueens(n: number): string[][][] {\n    // Инициализировать доску размера n*n, где 'Q' обозначает ферзя, а '#' — пустую клетку\n    const state = Array.from({ length: n }, () => Array(n).fill('#'));\n    const cols = Array(n).fill(false); // Отмечать, есть ли ферзь в столбце\n    const diags1 = Array(2 * n - 1).fill(false); // Отмечать наличие ферзя на главной диагонали\n    const diags2 = Array(2 * n - 1).fill(false); // Отмечать наличие ферзя на побочной диагонали\n    const res: string[][][] = [];\n\n    backtrack(0, n, state, res, cols, diags1, diags2);\n    return res;\n}\n
        n_queens.dart
        /* Алгоритм бэктрекинга: n ферзей */\nvoid backtrack(\n  int row,\n  int n,\n  List<List<String>> state,\n  List<List<List<String>>> res,\n  List<bool> cols,\n  List<bool> diags1,\n  List<bool> diags2,\n) {\n  // Когда все строки уже обработаны, записать решение\n  if (row == n) {\n    List<List<String>> copyState = [];\n    for (List<String> sRow in state) {\n      copyState.add(List.from(sRow));\n    }\n    res.add(copyState);\n    return;\n  }\n  // Обойти все столбцы\n  for (int col = 0; col < n; col++) {\n    // Вычислить главную и побочную диагонали, соответствующие этой клетке\n    int diag1 = row - col + n - 1;\n    int diag2 = row + col;\n    // Отсечение: в столбце, главной диагонали и побочной диагонали этой клетки не должно быть ферзей\n    if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n      // Попытка: поставить ферзя в эту клетку\n      state[row][col] = \"Q\";\n      cols[col] = true;\n      diags1[diag1] = true;\n      diags2[diag2] = true;\n      // Перейти к размещению следующей строки\n      backtrack(row + 1, n, state, res, cols, diags1, diags2);\n      // Откат: восстановить эту клетку как пустую\n      state[row][col] = \"#\";\n      cols[col] = false;\n      diags1[diag1] = false;\n      diags2[diag2] = false;\n    }\n  }\n}\n\n/* Решить задачу о n ферзях */\nList<List<List<String>>> nQueens(int n) {\n  // Инициализировать доску размера n*n, где 'Q' обозначает ферзя, а '#' — пустую клетку\n  List<List<String>> state = List.generate(n, (index) => List.filled(n, \"#\"));\n  List<bool> cols = List.filled(n, false); // Отмечать, есть ли ферзь в столбце\n  List<bool> diags1 = List.filled(2 * n - 1, false); // Отмечать наличие ферзя на главной диагонали\n  List<bool> diags2 = List.filled(2 * n - 1, false); // Отмечать наличие ферзя на побочной диагонали\n  List<List<List<String>>> res = [];\n\n  backtrack(0, n, state, res, cols, diags1, diags2);\n\n  return res;\n}\n
        n_queens.rs
        /* Алгоритм бэктрекинга: n ферзей */\nfn backtrack(\n    row: usize,\n    n: usize,\n    state: &mut Vec<Vec<String>>,\n    res: &mut Vec<Vec<Vec<String>>>,\n    cols: &mut [bool],\n    diags1: &mut [bool],\n    diags2: &mut [bool],\n) {\n    // Когда все строки уже обработаны, записать решение\n    if row == n {\n        res.push(state.clone());\n        return;\n    }\n    // Обойти все столбцы\n    for col in 0..n {\n        // Вычислить главную и побочную диагонали, соответствующие этой клетке\n        let diag1 = row + n - 1 - col;\n        let diag2 = row + col;\n        // Отсечение: в столбце, главной диагонали и побочной диагонали этой клетки не должно быть ферзей\n        if !cols[col] && !diags1[diag1] && !diags2[diag2] {\n            // Попытка: поставить ферзя в эту клетку\n            state[row][col] = \"Q\".into();\n            (cols[col], diags1[diag1], diags2[diag2]) = (true, true, true);\n            // Перейти к размещению следующей строки\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // Откат: восстановить эту клетку как пустую\n            state[row][col] = \"#\".into();\n            (cols[col], diags1[diag1], diags2[diag2]) = (false, false, false);\n        }\n    }\n}\n\n/* Решить задачу о n ферзях */\nfn n_queens(n: usize) -> Vec<Vec<Vec<String>>> {\n    // Инициализировать доску размера n*n, где 'Q' обозначает ферзя, а '#' — пустую клетку\n    let mut state: Vec<Vec<String>> = vec![vec![\"#\".to_string(); n]; n];\n    let mut cols = vec![false; n]; // Отмечать, есть ли ферзь в столбце\n    let mut diags1 = vec![false; 2 * n - 1]; // Отмечать наличие ферзя на главной диагонали\n    let mut diags2 = vec![false; 2 * n - 1]; // Отмечать наличие ферзя на побочной диагонали\n    let mut res: Vec<Vec<Vec<String>>> = Vec::new();\n\n    backtrack(\n        0,\n        n,\n        &mut state,\n        &mut res,\n        &mut cols,\n        &mut diags1,\n        &mut diags2,\n    );\n\n    res\n}\n
        n_queens.c
        /* Алгоритм бэктрекинга: n ферзей */\nvoid backtrack(int row, int n, char state[MAX_SIZE][MAX_SIZE], char ***res, int *resSize, bool cols[MAX_SIZE],\n               bool diags1[2 * MAX_SIZE - 1], bool diags2[2 * MAX_SIZE - 1]) {\n    // Когда все строки уже обработаны, записать решение\n    if (row == n) {\n        res[*resSize] = (char **)malloc(sizeof(char *) * n);\n        for (int i = 0; i < n; ++i) {\n            res[*resSize][i] = (char *)malloc(sizeof(char) * (n + 1));\n            strcpy(res[*resSize][i], state[i]);\n        }\n        (*resSize)++;\n        return;\n    }\n    // Обойти все столбцы\n    for (int col = 0; col < n; col++) {\n        // Вычислить главную и побочную диагонали, соответствующие этой клетке\n        int diag1 = row - col + n - 1;\n        int diag2 = row + col;\n        // Отсечение: в столбце, главной диагонали и побочной диагонали этой клетки не должно быть ферзей\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Попытка: поставить ферзя в эту клетку\n            state[row][col] = 'Q';\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // Перейти к размещению следующей строки\n            backtrack(row + 1, n, state, res, resSize, cols, diags1, diags2);\n            // Откат: восстановить эту клетку как пустую\n            state[row][col] = '#';\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* Решить задачу о n ферзях */\nchar ***nQueens(int n, int *returnSize) {\n    char state[MAX_SIZE][MAX_SIZE];\n    // Инициализировать доску размера n*n, где 'Q' обозначает ферзя, а '#' — пустую клетку\n    for (int i = 0; i < n; ++i) {\n        for (int j = 0; j < n; ++j) {\n            state[i][j] = '#';\n        }\n        state[i][n] = '\\0';\n    }\n    bool cols[MAX_SIZE] = {false};           // Отмечать, есть ли ферзь в столбце\n    bool diags1[2 * MAX_SIZE - 1] = {false}; // Отмечать наличие ферзя на главной диагонали\n    bool diags2[2 * MAX_SIZE - 1] = {false}; // Отмечать наличие ферзя на побочной диагонали\n\n    char ***res = (char ***)malloc(sizeof(char **) * MAX_SIZE);\n    *returnSize = 0;\n    backtrack(0, n, state, res, returnSize, cols, diags1, diags2);\n    return res;\n}\n
        n_queens.kt
        /* Алгоритм бэктрекинга: n ферзей */\nfun backtrack(\n    row: Int,\n    n: Int,\n    state: MutableList<MutableList<String>>,\n    res: MutableList<MutableList<MutableList<String>>?>,\n    cols: BooleanArray,\n    diags1: BooleanArray,\n    diags2: BooleanArray\n) {\n    // Когда все строки уже обработаны, записать решение\n    if (row == n) {\n        val copyState = mutableListOf<MutableList<String>>()\n        for (sRow in state) {\n            copyState.add(sRow.toMutableList())\n        }\n        res.add(copyState)\n        return\n    }\n    // Обойти все столбцы\n    for (col in 0..<n) {\n        // Вычислить главную и побочную диагонали, соответствующие этой клетке\n        val diag1 = row - col + n - 1\n        val diag2 = row + col\n        // Отсечение: в столбце, главной диагонали и побочной диагонали этой клетки не должно быть ферзей\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Попытка: поставить ферзя в эту клетку\n            state[row][col] = \"Q\"\n            diags2[diag2] = true\n            diags1[diag1] = diags2[diag2]\n            cols[col] = diags1[diag1]\n            // Перейти к размещению следующей строки\n            backtrack(row + 1, n, state, res, cols, diags1, diags2)\n            // Откат: восстановить эту клетку как пустую\n            state[row][col] = \"#\"\n            diags2[diag2] = false\n            diags1[diag1] = diags2[diag2]\n            cols[col] = diags1[diag1]\n        }\n    }\n}\n\n/* Решить задачу о n ферзях */\nfun nQueens(n: Int): MutableList<MutableList<MutableList<String>>?> {\n    // Инициализировать доску размера n*n, где 'Q' обозначает ферзя, а '#' — пустую клетку\n    val state = mutableListOf<MutableList<String>>()\n    for (i in 0..<n) {\n        val row = mutableListOf<String>()\n        for (j in 0..<n) {\n            row.add(\"#\")\n        }\n        state.add(row)\n    }\n    val cols = BooleanArray(n) // Отмечать, есть ли ферзь в столбце\n    val diags1 = BooleanArray(2 * n - 1) // Отмечать наличие ферзя на главной диагонали\n    val diags2 = BooleanArray(2 * n - 1) // Отмечать наличие ферзя на побочной диагонали\n    val res = mutableListOf<MutableList<MutableList<String>>?>()\n\n    backtrack(0, n, state, res, cols, diags1, diags2)\n\n    return res\n}\n
        n_queens.rb
        ### Алгоритм бэктрекинга: n ферзей ###\ndef backtrack(row, n, state, res, cols, diags1, diags2)\n  # Когда все строки уже обработаны, записать решение\n  if row == n\n    res << state.map { |row| row.dup }\n    return\n  end\n\n  # Обойти все столбцы\n  for col in 0...n\n    # Вычислить главную и побочную диагонали, соответствующие этой клетке\n    diag1 = row - col + n - 1\n    diag2 = row + col\n    # Отсечение: в столбце, главной диагонали и побочной диагонали этой клетки не должно быть ферзей\n    if !cols[col] && !diags1[diag1] && !diags2[diag2]\n      # Попытка: поставить ферзя в эту клетку\n      state[row][col] = \"Q\"\n      cols[col] = diags1[diag1] = diags2[diag2] = true\n      # Перейти к размещению следующей строки\n      backtrack(row + 1, n, state, res, cols, diags1, diags2)\n      # Откат: восстановить эту клетку как пустую\n      state[row][col] = \"#\"\n      cols[col] = diags1[diag1] = diags2[diag2] = false\n    end\n  end\nend\n\n### Решить задачу о n ферзях ###\ndef n_queens(n)\n  # Инициализировать доску размера n*n, где 'Q' обозначает ферзя, а '#' — пустую клетку\n  state = Array.new(n) { Array.new(n, \"#\") }\n  cols = Array.new(n, false) # Отмечать, есть ли ферзь в столбце\n  diags1 = Array.new(2 * n - 1, false) # Отмечать наличие ферзя на главной диагонали\n  diags2 = Array.new(2 * n - 1, false) # Отмечать наличие ферзя на побочной диагонали\n  res = []\n  backtrack(0, n, state, res, cols, diags1, diags2)\n\n  res\nend\n
        Визуализация кода

        Во весь экран >

        Если размещать ферзей построчно \\(n\\) раз, учитывая ограничение по столбцам, то начиная с первой строки и заканчивая последней мы получаем соответственно \\(n\\), \\(n-1\\), \\(\\dots\\), \\(2\\), \\(1\\) вариантов выбора, что дает \\(O(n!)\\) времени. При записи решения нужно скопировать матрицу state и добавить ее в res , а копирование требует \\(O(n^2)\\) времени. Следовательно, общая временная сложность равна \\(O(n! \\cdot n^2)\\) . На практике обрезка по диагональным ограничениям дополнительно сильно уменьшает пространство поиска, поэтому фактическая эффективность часто лучше этой оценки.

        Массив state использует \\(O(n^2)\\) пространства, а массивы cols , diags1 и diags2 используют по \\(O(n)\\) пространства. Максимальная глубина рекурсии равна \\(n\\) , что требует \\(O(n)\\) памяти стека. Следовательно, пространственная сложность равна \\(O(n^2)\\) .

        ","path":["Глава 13. Поиск с возвратом","13.4   Задача о n ферзях"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/","level":1,"title":"13.2   Задача о перестановках","text":"

        Задача о перестановках является типичным применением алгоритма поиска с возвратом. Ее определение состоит в том, чтобы для данного множества элементов (например, массива или строки) найти все возможные перестановки этих элементов.

        В таблице 13-2 приведено несколько примеров входных массивов и соответствующих им перестановок.

        Таблица 13-2   Примеры перестановок

        Входной массив Все перестановки \\([1]\\) \\([1]\\) \\([1, 2]\\) \\([1, 2], [2, 1]\\) \\([1, 2, 3]\\) \\([1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]\\)","path":["Глава 13. Поиск с возвратом","13.2   Задача о перестановках"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1321","level":2,"title":"13.2.1   Случай без равных элементов","text":"

        Question

        Дан массив целых чисел, в котором нет повторяющихся элементов. Верните все возможные перестановки.

        С точки зрения поиска с возвратом процесс построения перестановок можно представить как результат последовательности выборов. Пусть входной массив равен \\([1, 2, 3]\\). Если мы сначала выберем \\(1\\) , затем \\(3\\) , а потом \\(2\\) , то получим перестановку \\([1, 3, 2]\\) . Откат здесь означает отмену одного из выборов с последующей попыткой других вариантов.

        С точки зрения кода поиска с возвратом множество кандидатов choices состоит из всех элементов входного массива, а состояние state - из элементов, уже выбранных к текущему моменту. Поскольку каждый элемент разрешено выбирать только один раз, все элементы в state должны быть уникальны.

        Как показано на рисунке 13-5, процесс поиска можно развернуть в дерево рекурсии, где каждый узел представляет текущее состояние state . Начиная от корня, после трех раундов выбора мы попадаем в листья, и каждый лист соответствует одной перестановке.

        Рисунок 13-5   Дерево рекурсии для перестановок

        ","path":["Глава 13. Поиск с возвратом","13.2   Задача о перестановках"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1","level":3,"title":"1.   Обрезка повторного выбора","text":"

        Чтобы гарантировать, что каждый элемент выбирается только один раз, введем булев массив selected , где selected[i] обозначает, был ли уже выбран choices[i] , и на его основе выполним следующую обрезку.

        • После того как сделан выбор choice[i] , мы присваиваем selected[i] значение \\(\\text{True}\\) , тем самым отмечая, что этот элемент уже выбран.
        • При обходе списка вариантов choices пропускаем все уже выбранные элементы, то есть выполняем обрезку.

        Как показано на рисунке 13-6, если в первом раунде мы выберем 1 , во втором - 3 , а в третьем - 2 , то во втором раунде нужно отсечь ветвь элемента 1 , а в третьем - ветви элементов 1 и 3 .

        Рисунок 13-6   Пример обрезки в задаче о перестановках

        Как видно на рисунке 13-6, такая обрезка уменьшает размер пространства поиска с \\(O(n^n)\\) до \\(O(n!)\\) .

        ","path":["Глава 13. Поиск с возвратом","13.2   Задача о перестановках"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#2","level":3,"title":"2.   Реализация кода","text":"

        После прояснения всей логики можно просто «заполнить пропуски» в шаблоне поиска с возвратом. Чтобы сократить общий объем кода, мы не будем отдельно реализовывать каждую функцию из каркаса, а раскроем их прямо внутри backtrack() :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby permutations_i.py
        def backtrack(\n    state: list[int], choices: list[int], selected: list[bool], res: list[list[int]]\n):\n    \"\"\"Алгоритм бэктрекинга: все перестановки I\"\"\"\n    # Когда длина состояния равна числу элементов, записать решение\n    if len(state) == len(choices):\n        res.append(list(state))\n        return\n    # Перебор всех вариантов выбора\n    for i, choice in enumerate(choices):\n        # Отсечение: нельзя выбирать один и тот же элемент повторно\n        if not selected[i]:\n            # Попытка: сделать выбор и обновить состояние\n            selected[i] = True\n            state.append(choice)\n            # Перейти к следующему выбору\n            backtrack(state, choices, selected, res)\n            # Откат: отменить выбор и восстановить предыдущее состояние\n            selected[i] = False\n            state.pop()\n\ndef permutations_i(nums: list[int]) -> list[list[int]]:\n    \"\"\"Все перестановки I\"\"\"\n    res = []\n    backtrack(state=[], choices=nums, selected=[False] * len(nums), res=res)\n    return res\n
        permutations_i.cpp
        /* Алгоритм бэктрекинга: все перестановки I */\nvoid backtrack(vector<int> &state, const vector<int> &choices, vector<bool> &selected, vector<vector<int>> &res) {\n    // Когда длина состояния равна числу элементов, записать решение\n    if (state.size() == choices.size()) {\n        res.push_back(state);\n        return;\n    }\n    // Перебор всех вариантов выбора\n    for (int i = 0; i < choices.size(); i++) {\n        int choice = choices[i];\n        // Отсечение: нельзя выбирать один и тот же элемент повторно\n        if (!selected[i]) {\n            // Попытка: сделать выбор и обновить состояние\n            selected[i] = true;\n            state.push_back(choice);\n            // Перейти к следующему выбору\n            backtrack(state, choices, selected, res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            selected[i] = false;\n            state.pop_back();\n        }\n    }\n}\n\n/* Все перестановки I */\nvector<vector<int>> permutationsI(vector<int> nums) {\n    vector<int> state;\n    vector<bool> selected(nums.size(), false);\n    vector<vector<int>> res;\n    backtrack(state, nums, selected, res);\n    return res;\n}\n
        permutations_i.java
        /* Алгоритм бэктрекинга: все перестановки I */\nvoid backtrack(List<Integer> state, int[] choices, boolean[] selected, List<List<Integer>> res) {\n    // Когда длина состояния равна числу элементов, записать решение\n    if (state.size() == choices.length) {\n        res.add(new ArrayList<Integer>(state));\n        return;\n    }\n    // Перебор всех вариантов выбора\n    for (int i = 0; i < choices.length; i++) {\n        int choice = choices[i];\n        // Отсечение: нельзя выбирать один и тот же элемент повторно\n        if (!selected[i]) {\n            // Попытка: сделать выбор и обновить состояние\n            selected[i] = true;\n            state.add(choice);\n            // Перейти к следующему выбору\n            backtrack(state, choices, selected, res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            selected[i] = false;\n            state.remove(state.size() - 1);\n        }\n    }\n}\n\n/* Все перестановки I */\nList<List<Integer>> permutationsI(int[] nums) {\n    List<List<Integer>> res = new ArrayList<List<Integer>>();\n    backtrack(new ArrayList<Integer>(), nums, new boolean[nums.length], res);\n    return res;\n}\n
        permutations_i.cs
        /* Алгоритм бэктрекинга: все перестановки I */\nvoid Backtrack(List<int> state, int[] choices, bool[] selected, List<List<int>> res) {\n    // Когда длина состояния равна числу элементов, записать решение\n    if (state.Count == choices.Length) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // Перебор всех вариантов выбора\n    for (int i = 0; i < choices.Length; i++) {\n        int choice = choices[i];\n        // Отсечение: нельзя выбирать один и тот же элемент повторно\n        if (!selected[i]) {\n            // Попытка: сделать выбор и обновить состояние\n            selected[i] = true;\n            state.Add(choice);\n            // Перейти к следующему выбору\n            Backtrack(state, choices, selected, res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            selected[i] = false;\n            state.RemoveAt(state.Count - 1);\n        }\n    }\n}\n\n/* Все перестановки I */\nList<List<int>> PermutationsI(int[] nums) {\n    List<List<int>> res = [];\n    Backtrack([], nums, new bool[nums.Length], res);\n    return res;\n}\n
        permutations_i.go
        /* Алгоритм бэктрекинга: все перестановки I */\nfunc backtrackI(state *[]int, choices *[]int, selected *[]bool, res *[][]int) {\n    // Когда длина состояния равна числу элементов, записать решение\n    if len(*state) == len(*choices) {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n    }\n    // Перебор всех вариантов выбора\n    for i := 0; i < len(*choices); i++ {\n        choice := (*choices)[i]\n        // Отсечение: нельзя выбирать один и тот же элемент повторно\n        if !(*selected)[i] {\n            // Попытка: сделать выбор и обновить состояние\n            (*selected)[i] = true\n            *state = append(*state, choice)\n            // Перейти к следующему выбору\n            backtrackI(state, choices, selected, res)\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            (*selected)[i] = false\n            *state = (*state)[:len(*state)-1]\n        }\n    }\n}\n\n/* Все перестановки I */\nfunc permutationsI(nums []int) [][]int {\n    res := make([][]int, 0)\n    state := make([]int, 0)\n    selected := make([]bool, len(nums))\n    backtrackI(&state, &nums, &selected, &res)\n    return res\n}\n
        permutations_i.swift
        /* Алгоритм бэктрекинга: все перестановки I */\nfunc backtrack(state: inout [Int], choices: [Int], selected: inout [Bool], res: inout [[Int]]) {\n    // Когда длина состояния равна числу элементов, записать решение\n    if state.count == choices.count {\n        res.append(state)\n        return\n    }\n    // Перебор всех вариантов выбора\n    for (i, choice) in choices.enumerated() {\n        // Отсечение: нельзя выбирать один и тот же элемент повторно\n        if !selected[i] {\n            // Попытка: сделать выбор и обновить состояние\n            selected[i] = true\n            state.append(choice)\n            // Перейти к следующему выбору\n            backtrack(state: &state, choices: choices, selected: &selected, res: &res)\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            selected[i] = false\n            state.removeLast()\n        }\n    }\n}\n\n/* Все перестановки I */\nfunc permutationsI(nums: [Int]) -> [[Int]] {\n    var state: [Int] = []\n    var selected = Array(repeating: false, count: nums.count)\n    var res: [[Int]] = []\n    backtrack(state: &state, choices: nums, selected: &selected, res: &res)\n    return res\n}\n
        permutations_i.js
        /* Алгоритм бэктрекинга: все перестановки I */\nfunction backtrack(state, choices, selected, res) {\n    // Когда длина состояния равна числу элементов, записать решение\n    if (state.length === choices.length) {\n        res.push([...state]);\n        return;\n    }\n    // Перебор всех вариантов выбора\n    choices.forEach((choice, i) => {\n        // Отсечение: нельзя выбирать один и тот же элемент повторно\n        if (!selected[i]) {\n            // Попытка: сделать выбор и обновить состояние\n            selected[i] = true;\n            state.push(choice);\n            // Перейти к следующему выбору\n            backtrack(state, choices, selected, res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            selected[i] = false;\n            state.pop();\n        }\n    });\n}\n\n/* Все перестановки I */\nfunction permutationsI(nums) {\n    const res = [];\n    backtrack([], nums, Array(nums.length).fill(false), res);\n    return res;\n}\n
        permutations_i.ts
        /* Алгоритм бэктрекинга: все перестановки I */\nfunction backtrack(\n    state: number[],\n    choices: number[],\n    selected: boolean[],\n    res: number[][]\n): void {\n    // Когда длина состояния равна числу элементов, записать решение\n    if (state.length === choices.length) {\n        res.push([...state]);\n        return;\n    }\n    // Перебор всех вариантов выбора\n    choices.forEach((choice, i) => {\n        // Отсечение: нельзя выбирать один и тот же элемент повторно\n        if (!selected[i]) {\n            // Попытка: сделать выбор и обновить состояние\n            selected[i] = true;\n            state.push(choice);\n            // Перейти к следующему выбору\n            backtrack(state, choices, selected, res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            selected[i] = false;\n            state.pop();\n        }\n    });\n}\n\n/* Все перестановки I */\nfunction permutationsI(nums: number[]): number[][] {\n    const res: number[][] = [];\n    backtrack([], nums, Array(nums.length).fill(false), res);\n    return res;\n}\n
        permutations_i.dart
        /* Алгоритм бэктрекинга: все перестановки I */\nvoid backtrack(\n  List<int> state,\n  List<int> choices,\n  List<bool> selected,\n  List<List<int>> res,\n) {\n  // Когда длина состояния равна числу элементов, записать решение\n  if (state.length == choices.length) {\n    res.add(List.from(state));\n    return;\n  }\n  // Перебор всех вариантов выбора\n  for (int i = 0; i < choices.length; i++) {\n    int choice = choices[i];\n    // Отсечение: нельзя выбирать один и тот же элемент повторно\n    if (!selected[i]) {\n      // Попытка: сделать выбор и обновить состояние\n      selected[i] = true;\n      state.add(choice);\n      // Перейти к следующему выбору\n      backtrack(state, choices, selected, res);\n      // Откат: отменить выбор и восстановить предыдущее состояние\n      selected[i] = false;\n      state.removeLast();\n    }\n  }\n}\n\n/* Все перестановки I */\nList<List<int>> permutationsI(List<int> nums) {\n  List<List<int>> res = [];\n  backtrack([], nums, List.filled(nums.length, false), res);\n  return res;\n}\n
        permutations_i.rs
        /* Алгоритм бэктрекинга: все перестановки I */\nfn backtrack(mut state: Vec<i32>, choices: &[i32], selected: &mut [bool], res: &mut Vec<Vec<i32>>) {\n    // Когда длина состояния равна числу элементов, записать решение\n    if state.len() == choices.len() {\n        res.push(state);\n        return;\n    }\n    // Перебор всех вариантов выбора\n    for i in 0..choices.len() {\n        let choice = choices[i];\n        // Отсечение: нельзя выбирать один и тот же элемент повторно\n        if !selected[i] {\n            // Попытка: сделать выбор и обновить состояние\n            selected[i] = true;\n            state.push(choice);\n            // Перейти к следующему выбору\n            backtrack(state.clone(), choices, selected, res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            selected[i] = false;\n            state.pop();\n        }\n    }\n}\n\n/* Все перестановки I */\nfn permutations_i(nums: &mut [i32]) -> Vec<Vec<i32>> {\n    let mut res = Vec::new(); // Состояние (подмножество)\n    backtrack(Vec::new(), nums, &mut vec![false; nums.len()], &mut res);\n    res\n}\n
        permutations_i.c
        /* Алгоритм бэктрекинга: все перестановки I */\nvoid backtrack(int *state, int stateSize, int *choices, int choicesSize, bool *selected, int **res, int *resSize) {\n    // Когда длина состояния равна числу элементов, записать решение\n    if (stateSize == choicesSize) {\n        res[*resSize] = (int *)malloc(choicesSize * sizeof(int));\n        for (int i = 0; i < choicesSize; i++) {\n            res[*resSize][i] = state[i];\n        }\n        (*resSize)++;\n        return;\n    }\n    // Перебор всех вариантов выбора\n    for (int i = 0; i < choicesSize; i++) {\n        int choice = choices[i];\n        // Отсечение: нельзя выбирать один и тот же элемент повторно\n        if (!selected[i]) {\n            // Попытка: сделать выбор и обновить состояние\n            selected[i] = true;\n            state[stateSize] = choice;\n            // Перейти к следующему выбору\n            backtrack(state, stateSize + 1, choices, choicesSize, selected, res, resSize);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            selected[i] = false;\n        }\n    }\n}\n\n/* Все перестановки I */\nint **permutationsI(int *nums, int numsSize, int *returnSize) {\n    int *state = (int *)malloc(numsSize * sizeof(int));\n    bool *selected = (bool *)malloc(numsSize * sizeof(bool));\n    for (int i = 0; i < numsSize; i++) {\n        selected[i] = false;\n    }\n    int **res = (int **)malloc(MAX_SIZE * sizeof(int *));\n    *returnSize = 0;\n\n    backtrack(state, 0, nums, numsSize, selected, res, returnSize);\n\n    free(state);\n    free(selected);\n\n    return res;\n}\n
        permutations_i.kt
        /* Алгоритм бэктрекинга: все перестановки I */\nfun backtrack(\n    state: MutableList<Int>,\n    choices: IntArray,\n    selected: BooleanArray,\n    res: MutableList<MutableList<Int>?>\n) {\n    // Когда длина состояния равна числу элементов, записать решение\n    if (state.size == choices.size) {\n        res.add(state.toMutableList())\n        return\n    }\n    // Перебор всех вариантов выбора\n    for (i in choices.indices) {\n        val choice = choices[i]\n        // Отсечение: нельзя выбирать один и тот же элемент повторно\n        if (!selected[i]) {\n            // Попытка: сделать выбор и обновить состояние\n            selected[i] = true\n            state.add(choice)\n            // Перейти к следующему выбору\n            backtrack(state, choices, selected, res)\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            selected[i] = false\n            state.removeAt(state.size - 1)\n        }\n    }\n}\n\n/* Все перестановки I */\nfun permutationsI(nums: IntArray): MutableList<MutableList<Int>?> {\n    val res = mutableListOf<MutableList<Int>?>()\n    backtrack(mutableListOf(), nums, BooleanArray(nums.size), res)\n    return res\n}\n
        permutations_i.rb
        ### Алгоритм бэктрекинга: все перестановки I ###\ndef backtrack(state, choices, selected, res)\n  # Когда длина состояния равна числу элементов, записать решение\n  if state.length == choices.length\n    res << state.dup\n    return\n  end\n\n  # Перебор всех вариантов выбора\n  choices.each_with_index do |choice, i|\n    # Отсечение: нельзя выбирать один и тот же элемент повторно\n    unless selected[i]\n      # Попытка: сделать выбор и обновить состояние\n      selected[i] = true\n      state << choice\n      # Перейти к следующему выбору\n      backtrack(state, choices, selected, res)\n      # Откат: отменить выбор и восстановить предыдущее состояние\n      selected[i] = false\n      state.pop\n    end\n  end\nend\n\n### Все перестановки I ###\ndef permutations_i(nums)\n  res = []\n  backtrack([], nums, Array.new(nums.length, false), res)\n  res\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 13. Поиск с возвратом","13.2   Задача о перестановках"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1322","level":2,"title":"13.2.2   Учет равных элементов","text":"

        Question

        Дан массив целых чисел, который может содержать повторяющиеся элементы. Верните все неповторяющиеся перестановки.

        Пусть входной массив равен \\([1, 1, 2]\\) . Чтобы различать два одинаковых элемента \\(1\\) , будем обозначать второй из них как \\(\\hat{1}\\) .

        Как показано на рисунке 13-7, описанный выше метод создаст результат, половина которого окажется дублирующейся.

        Рисунок 13-7   Повторяющиеся перестановки

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

        ","path":["Глава 13. Поиск с возвратом","13.2   Задача о перестановках"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1_1","level":3,"title":"1.   Обрезка равных элементов","text":"

        Как видно на рисунке 13-8, в первом раунде выбрать \\(1\\) или выбрать \\(\\hat{1}\\) - это одно и то же, а значит, все перестановки, полученные из этих двух выборов, будут дублироваться. Поэтому ветвь \\(\\hat{1}\\) нужно отсечь.

        Точно так же, если в первом раунде выбрать \\(2\\) , то во втором раунде выборы \\(1\\) и \\(\\hat{1}\\) снова создадут дублирующиеся ветви, поэтому и в этом случае ветвь \\(\\hat{1}\\) нужно отсечь.

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

        Рисунок 13-8   Обрезка повторяющихся перестановок

        ","path":["Глава 13. Поиск с возвратом","13.2   Задача о перестановках"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#2_1","level":3,"title":"2.   Реализация кода","text":"

        На основе решения из предыдущей задачи можно на каждом раунде выбора заводить хеш-множество duplicated , которое будет записывать элементы, уже встречавшиеся в этом раунде, и отсекать повторы:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby permutations_ii.py
        def backtrack(\n    state: list[int], choices: list[int], selected: list[bool], res: list[list[int]]\n):\n    \"\"\"Алгоритм бэктрекинга: все перестановки II\"\"\"\n    # Когда длина состояния равна числу элементов, записать решение\n    if len(state) == len(choices):\n        res.append(list(state))\n        return\n    # Перебор всех вариантов выбора\n    duplicated = set[int]()\n    for i, choice in enumerate(choices):\n        # Отсечение: нельзя выбирать один и тот же элемент повторно и нельзя повторно выбирать равные элементы\n        if not selected[i] and choice not in duplicated:\n            # Попытка: сделать выбор и обновить состояние\n            duplicated.add(choice)  # Записать значения уже выбранных элементов\n            selected[i] = True\n            state.append(choice)\n            # Перейти к следующему выбору\n            backtrack(state, choices, selected, res)\n            # Откат: отменить выбор и восстановить предыдущее состояние\n            selected[i] = False\n            state.pop()\n\ndef permutations_ii(nums: list[int]) -> list[list[int]]:\n    \"\"\"Все перестановки II\"\"\"\n    res = []\n    backtrack(state=[], choices=nums, selected=[False] * len(nums), res=res)\n    return res\n
        permutations_ii.cpp
        /* Алгоритм бэктрекинга: все перестановки II */\nvoid backtrack(vector<int> &state, const vector<int> &choices, vector<bool> &selected, vector<vector<int>> &res) {\n    // Когда длина состояния равна числу элементов, записать решение\n    if (state.size() == choices.size()) {\n        res.push_back(state);\n        return;\n    }\n    // Перебор всех вариантов выбора\n    unordered_set<int> duplicated;\n    for (int i = 0; i < choices.size(); i++) {\n        int choice = choices[i];\n        // Отсечение: нельзя выбирать один и тот же элемент повторно и нельзя повторно выбирать равные элементы\n        if (!selected[i] && duplicated.find(choice) == duplicated.end()) {\n            // Попытка: сделать выбор и обновить состояние\n            duplicated.emplace(choice); // Записать значения уже выбранных элементов\n            selected[i] = true;\n            state.push_back(choice);\n            // Перейти к следующему выбору\n            backtrack(state, choices, selected, res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            selected[i] = false;\n            state.pop_back();\n        }\n    }\n}\n\n/* Все перестановки II */\nvector<vector<int>> permutationsII(vector<int> nums) {\n    vector<int> state;\n    vector<bool> selected(nums.size(), false);\n    vector<vector<int>> res;\n    backtrack(state, nums, selected, res);\n    return res;\n}\n
        permutations_ii.java
        /* Алгоритм бэктрекинга: все перестановки II */\nvoid backtrack(List<Integer> state, int[] choices, boolean[] selected, List<List<Integer>> res) {\n    // Когда длина состояния равна числу элементов, записать решение\n    if (state.size() == choices.length) {\n        res.add(new ArrayList<Integer>(state));\n        return;\n    }\n    // Перебор всех вариантов выбора\n    Set<Integer> duplicated = new HashSet<Integer>();\n    for (int i = 0; i < choices.length; i++) {\n        int choice = choices[i];\n        // Отсечение: нельзя выбирать один и тот же элемент повторно и нельзя повторно выбирать равные элементы\n        if (!selected[i] && !duplicated.contains(choice)) {\n            // Попытка: сделать выбор и обновить состояние\n            duplicated.add(choice); // Записать значения уже выбранных элементов\n            selected[i] = true;\n            state.add(choice);\n            // Перейти к следующему выбору\n            backtrack(state, choices, selected, res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            selected[i] = false;\n            state.remove(state.size() - 1);\n        }\n    }\n}\n\n/* Все перестановки II */\nList<List<Integer>> permutationsII(int[] nums) {\n    List<List<Integer>> res = new ArrayList<List<Integer>>();\n    backtrack(new ArrayList<Integer>(), nums, new boolean[nums.length], res);\n    return res;\n}\n
        permutations_ii.cs
        /* Алгоритм бэктрекинга: все перестановки II */\nvoid Backtrack(List<int> state, int[] choices, bool[] selected, List<List<int>> res) {\n    // Когда длина состояния равна числу элементов, записать решение\n    if (state.Count == choices.Length) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // Перебор всех вариантов выбора\n    HashSet<int> duplicated = [];\n    for (int i = 0; i < choices.Length; i++) {\n        int choice = choices[i];\n        // Отсечение: нельзя выбирать один и тот же элемент повторно и нельзя повторно выбирать равные элементы\n        if (!selected[i] && !duplicated.Contains(choice)) {\n            // Попытка: сделать выбор и обновить состояние\n            duplicated.Add(choice); // Записать значения уже выбранных элементов\n            selected[i] = true;\n            state.Add(choice);\n            // Перейти к следующему выбору\n            Backtrack(state, choices, selected, res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            selected[i] = false;\n            state.RemoveAt(state.Count - 1);\n        }\n    }\n}\n\n/* Все перестановки II */\nList<List<int>> PermutationsII(int[] nums) {\n    List<List<int>> res = [];\n    Backtrack([], nums, new bool[nums.Length], res);\n    return res;\n}\n
        permutations_ii.go
        /* Алгоритм бэктрекинга: все перестановки II */\nfunc backtrackII(state *[]int, choices *[]int, selected *[]bool, res *[][]int) {\n    // Когда длина состояния равна числу элементов, записать решение\n    if len(*state) == len(*choices) {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n    }\n    // Перебор всех вариантов выбора\n    duplicated := make(map[int]struct{}, 0)\n    for i := 0; i < len(*choices); i++ {\n        choice := (*choices)[i]\n        // Отсечение: нельзя выбирать один и тот же элемент повторно и нельзя повторно выбирать равные элементы\n        if _, ok := duplicated[choice]; !ok && !(*selected)[i] {\n            // Попробовать: сделать выбор, обновить состояние\n            // Записать значение уже выбранного элемента\n            duplicated[choice] = struct{}{}\n            (*selected)[i] = true\n            *state = append(*state, choice)\n            // Перейти к следующему выбору\n            backtrackII(state, choices, selected, res)\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            (*selected)[i] = false\n            *state = (*state)[:len(*state)-1]\n        }\n    }\n}\n\n/* Все перестановки II */\nfunc permutationsII(nums []int) [][]int {\n    res := make([][]int, 0)\n    state := make([]int, 0)\n    selected := make([]bool, len(nums))\n    backtrackII(&state, &nums, &selected, &res)\n    return res\n}\n
        permutations_ii.swift
        /* Алгоритм бэктрекинга: все перестановки II */\nfunc backtrack(state: inout [Int], choices: [Int], selected: inout [Bool], res: inout [[Int]]) {\n    // Когда длина состояния равна числу элементов, записать решение\n    if state.count == choices.count {\n        res.append(state)\n        return\n    }\n    // Перебор всех вариантов выбора\n    var duplicated: Set<Int> = []\n    for (i, choice) in choices.enumerated() {\n        // Отсечение: нельзя выбирать один и тот же элемент повторно и нельзя повторно выбирать равные элементы\n        if !selected[i], !duplicated.contains(choice) {\n            // Попытка: сделать выбор и обновить состояние\n            duplicated.insert(choice) // Записать значения уже выбранных элементов\n            selected[i] = true\n            state.append(choice)\n            // Перейти к следующему выбору\n            backtrack(state: &state, choices: choices, selected: &selected, res: &res)\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            selected[i] = false\n            state.removeLast()\n        }\n    }\n}\n\n/* Все перестановки II */\nfunc permutationsII(nums: [Int]) -> [[Int]] {\n    var state: [Int] = []\n    var selected = Array(repeating: false, count: nums.count)\n    var res: [[Int]] = []\n    backtrack(state: &state, choices: nums, selected: &selected, res: &res)\n    return res\n}\n
        permutations_ii.js
        /* Алгоритм бэктрекинга: все перестановки II */\nfunction backtrack(state, choices, selected, res) {\n    // Когда длина состояния равна числу элементов, записать решение\n    if (state.length === choices.length) {\n        res.push([...state]);\n        return;\n    }\n    // Перебор всех вариантов выбора\n    const duplicated = new Set();\n    choices.forEach((choice, i) => {\n        // Отсечение: нельзя выбирать один и тот же элемент повторно и нельзя повторно выбирать равные элементы\n        if (!selected[i] && !duplicated.has(choice)) {\n            // Попытка: сделать выбор и обновить состояние\n            duplicated.add(choice); // Записать значения уже выбранных элементов\n            selected[i] = true;\n            state.push(choice);\n            // Перейти к следующему выбору\n            backtrack(state, choices, selected, res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            selected[i] = false;\n            state.pop();\n        }\n    });\n}\n\n/* Все перестановки II */\nfunction permutationsII(nums) {\n    const res = [];\n    backtrack([], nums, Array(nums.length).fill(false), res);\n    return res;\n}\n
        permutations_ii.ts
        /* Алгоритм бэктрекинга: все перестановки II */\nfunction backtrack(\n    state: number[],\n    choices: number[],\n    selected: boolean[],\n    res: number[][]\n): void {\n    // Когда длина состояния равна числу элементов, записать решение\n    if (state.length === choices.length) {\n        res.push([...state]);\n        return;\n    }\n    // Перебор всех вариантов выбора\n    const duplicated = new Set();\n    choices.forEach((choice, i) => {\n        // Отсечение: нельзя выбирать один и тот же элемент повторно и нельзя повторно выбирать равные элементы\n        if (!selected[i] && !duplicated.has(choice)) {\n            // Попытка: сделать выбор и обновить состояние\n            duplicated.add(choice); // Записать значения уже выбранных элементов\n            selected[i] = true;\n            state.push(choice);\n            // Перейти к следующему выбору\n            backtrack(state, choices, selected, res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            selected[i] = false;\n            state.pop();\n        }\n    });\n}\n\n/* Все перестановки II */\nfunction permutationsII(nums: number[]): number[][] {\n    const res: number[][] = [];\n    backtrack([], nums, Array(nums.length).fill(false), res);\n    return res;\n}\n
        permutations_ii.dart
        /* Алгоритм бэктрекинга: все перестановки II */\nvoid backtrack(\n  List<int> state,\n  List<int> choices,\n  List<bool> selected,\n  List<List<int>> res,\n) {\n  // Когда длина состояния равна числу элементов, записать решение\n  if (state.length == choices.length) {\n    res.add(List.from(state));\n    return;\n  }\n  // Перебор всех вариантов выбора\n  Set<int> duplicated = {};\n  for (int i = 0; i < choices.length; i++) {\n    int choice = choices[i];\n    // Отсечение: нельзя выбирать один и тот же элемент повторно и нельзя повторно выбирать равные элементы\n    if (!selected[i] && !duplicated.contains(choice)) {\n      // Попытка: сделать выбор и обновить состояние\n      duplicated.add(choice); // Записать значения уже выбранных элементов\n      selected[i] = true;\n      state.add(choice);\n      // Перейти к следующему выбору\n      backtrack(state, choices, selected, res);\n      // Откат: отменить выбор и восстановить предыдущее состояние\n      selected[i] = false;\n      state.removeLast();\n    }\n  }\n}\n\n/* Все перестановки II */\nList<List<int>> permutationsII(List<int> nums) {\n  List<List<int>> res = [];\n  backtrack([], nums, List.filled(nums.length, false), res);\n  return res;\n}\n
        permutations_ii.rs
        /* Алгоритм бэктрекинга: все перестановки II */\nfn backtrack(mut state: Vec<i32>, choices: &[i32], selected: &mut [bool], res: &mut Vec<Vec<i32>>) {\n    // Когда длина состояния равна числу элементов, записать решение\n    if state.len() == choices.len() {\n        res.push(state);\n        return;\n    }\n    // Перебор всех вариантов выбора\n    let mut duplicated = HashSet::<i32>::new();\n    for i in 0..choices.len() {\n        let choice = choices[i];\n        // Отсечение: нельзя выбирать один и тот же элемент повторно и нельзя повторно выбирать равные элементы\n        if !selected[i] && !duplicated.contains(&choice) {\n            // Попытка: сделать выбор и обновить состояние\n            duplicated.insert(choice); // Записать значения уже выбранных элементов\n            selected[i] = true;\n            state.push(choice);\n            // Перейти к следующему выбору\n            backtrack(state.clone(), choices, selected, res);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            selected[i] = false;\n            state.pop();\n        }\n    }\n}\n\n/* Все перестановки II */\nfn permutations_ii(nums: &mut [i32]) -> Vec<Vec<i32>> {\n    let mut res = Vec::new();\n    backtrack(Vec::new(), nums, &mut vec![false; nums.len()], &mut res);\n    res\n}\n
        permutations_ii.c
        /* Алгоритм бэктрекинга: все перестановки II */\nvoid backtrack(int *state, int stateSize, int *choices, int choicesSize, bool *selected, int **res, int *resSize) {\n    // Когда длина состояния равна числу элементов, записать решение\n    if (stateSize == choicesSize) {\n        res[*resSize] = (int *)malloc(choicesSize * sizeof(int));\n        for (int i = 0; i < choicesSize; i++) {\n            res[*resSize][i] = state[i];\n        }\n        (*resSize)++;\n        return;\n    }\n    // Перебор всех вариантов выбора\n    bool duplicated[MAX_SIZE] = {false};\n    for (int i = 0; i < choicesSize; i++) {\n        int choice = choices[i];\n        // Отсечение: нельзя выбирать один и тот же элемент повторно и нельзя повторно выбирать равные элементы\n        if (!selected[i] && !duplicated[choice]) {\n            // Попытка: сделать выбор и обновить состояние\n            duplicated[choice] = true; // Записать значения уже выбранных элементов\n            selected[i] = true;\n            state[stateSize] = choice;\n            // Перейти к следующему выбору\n            backtrack(state, stateSize + 1, choices, choicesSize, selected, res, resSize);\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            selected[i] = false;\n        }\n    }\n}\n\n/* Все перестановки II */\nint **permutationsII(int *nums, int numsSize, int *returnSize) {\n    int *state = (int *)malloc(numsSize * sizeof(int));\n    bool *selected = (bool *)malloc(numsSize * sizeof(bool));\n    for (int i = 0; i < numsSize; i++) {\n        selected[i] = false;\n    }\n    int **res = (int **)malloc(MAX_SIZE * sizeof(int *));\n    *returnSize = 0;\n\n    backtrack(state, 0, nums, numsSize, selected, res, returnSize);\n\n    free(state);\n    free(selected);\n\n    return res;\n}\n
        permutations_ii.kt
        /* Алгоритм бэктрекинга: все перестановки II */\nfun backtrack(\n    state: MutableList<Int>,\n    choices: IntArray,\n    selected: BooleanArray,\n    res: MutableList<MutableList<Int>?>\n) {\n    // Когда длина состояния равна числу элементов, записать решение\n    if (state.size == choices.size) {\n        res.add(state.toMutableList())\n        return\n    }\n    // Перебор всех вариантов выбора\n    val duplicated = HashSet<Int>()\n    for (i in choices.indices) {\n        val choice = choices[i]\n        // Отсечение: нельзя выбирать один и тот же элемент повторно и нельзя повторно выбирать равные элементы\n        if (!selected[i] && !duplicated.contains(choice)) {\n            // Попытка: сделать выбор и обновить состояние\n            duplicated.add(choice) // Записать значения уже выбранных элементов\n            selected[i] = true\n            state.add(choice)\n            // Перейти к следующему выбору\n            backtrack(state, choices, selected, res)\n            // Откат: отменить выбор и восстановить предыдущее состояние\n            selected[i] = false\n            state.removeAt(state.size - 1)\n        }\n    }\n}\n\n/* Все перестановки II */\nfun permutationsII(nums: IntArray): MutableList<MutableList<Int>?> {\n    val res = mutableListOf<MutableList<Int>?>()\n    backtrack(mutableListOf(), nums, BooleanArray(nums.size), res)\n    return res\n}\n
        permutations_ii.rb
        ### Алгоритм бэктрекинга: все перестановки II ###\ndef backtrack(state, choices, selected, res)\n  # Когда длина состояния равна числу элементов, записать решение\n  if state.length == choices.length\n    res << state.dup\n    return\n  end\n\n  # Перебор всех вариантов выбора\n  duplicated = Set.new\n  choices.each_with_index do |choice, i|\n    # Отсечение: нельзя выбирать один и тот же элемент повторно и нельзя повторно выбирать равные элементы\n    if !selected[i] && !duplicated.include?(choice)\n      # Попытка: сделать выбор и обновить состояние\n      duplicated.add(choice)\n      selected[i] = true\n      state << choice\n      # Перейти к следующему выбору\n      backtrack(state, choices, selected, res)\n      # Откат: отменить выбор и восстановить предыдущее состояние\n      selected[i] = false\n      state.pop\n    end\n  end\nend\n\n### Все перестановки II ###\ndef permutations_ii(nums)\n  res = []\n  backtrack([], nums, Array.new(nums.length, false), res)\n  res\nend\n
        Визуализация кода

        Во весь экран >

        Если предположить, что все элементы попарно различны, то из \\(n\\) элементов можно получить \\(n!\\) перестановок. При записи результата требуется копировать список длины \\(n\\) , что занимает \\(O(n)\\) времени. Следовательно, временная сложность равна \\(O(n!n)\\) .

        Максимальная глубина рекурсии равна \\(n\\) , что требует \\(O(n)\\) стековой памяти. Массив selected занимает \\(O(n)\\) пространства. Одновременно может существовать до \\(n\\) хеш-множеств duplicated , что дает \\(O(n^2)\\) памяти. Следовательно, пространственная сложность равна \\(O(n^2)\\) .

        ","path":["Глава 13. Поиск с возвратом","13.2   Задача о перестановках"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#3","level":3,"title":"3.   Сравнение двух видов обрезки","text":"

        Обратите внимание: хотя и selected , и duplicated используются для обрезки, их цели различаются.

        • Обрезка повторного выбора: во всем процессе поиска существует только один selected . Он записывает, какие элементы уже входят в текущее состояние, и нужен для того, чтобы один и тот же элемент не появлялся в state дважды.
        • Обрезка равных элементов: каждый раунд выбора (каждый вызов backtrack) содержит собственный duplicated . Он записывает, какие элементы уже выбирались в текущем раунде (for цикле), и нужен для того, чтобы равные элементы выбирались только один раз.

        На рисунке 13-9 показана область действия двух условий обрезки. Помните, что каждый узел дерева соответствует одному выбору, а путь от корня до листа образует одну перестановку.

        Рисунок 13-9   Область действия двух условий обрезки

        ","path":["Глава 13. Поиск с возвратом","13.2   Задача о перестановках"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/","level":1,"title":"13.3   Задача о сумме подмножеств","text":"","path":["Глава 13. Поиск с возвратом","13.3   Задача о сумме подмножеств"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1331","level":2,"title":"13.3.1   Случай без повторяющихся элементов","text":"

        Question

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

        Например, для входного множества \\(\\{3, 4, 5\\}\\) и целевого значения \\(9\\) решениями будут \\(\\{3, 3, 3\\}\\) и \\(\\{4, 5\\}\\) . При этом важно учитывать два обстоятельства.

        • Элементы входного множества можно выбирать повторно неограниченное число раз.
        • Подмножество не различает порядок элементов, поэтому \\(\\{4, 5\\}\\) и \\(\\{5, 4\\}\\) считаются одним и тем же подмножеством.
        ","path":["Глава 13. Поиск с возвратом","13.3   Задача о сумме подмножеств"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1","level":3,"title":"1.   Отталкиваемся от решения задачи о перестановках","text":"

        Как и в задаче о перестановках, можно представлять построение подмножеств как результат последовательности выборов и во время выбора динамически обновлять «сумму элементов». Когда эта сумма становится равной target , соответствующее подмножество записывается в список результатов.

        Однако в отличие от задачи о перестановках в этой задаче элементы множества можно выбирать неограниченное число раз, поэтому нам не нужен булев список selected для записи того, был ли выбран элемент. Можно слегка изменить код для перестановок и получить первоначальную версию решения:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby subset_sum_i_naive.py
        def backtrack(\n    state: list[int],\n    target: int,\n    total: int,\n    choices: list[int],\n    res: list[list[int]],\n):\n    \"\"\"Алгоритм бэктрекинга: сумма подмножеств I\"\"\"\n    # Если сумма подмножества равна target, записать решение\n    if total == target:\n        res.append(list(state))\n        return\n    # Перебор всех вариантов выбора\n    for i in range(len(choices)):\n        # Отсечение: если сумма подмножества превышает target, пропустить этот выбор\n        if total + choices[i] > target:\n            continue\n        # Попытка: сделать выбор и обновить элемент и total\n        state.append(choices[i])\n        # Перейти к следующему выбору\n        backtrack(state, target, total + choices[i], choices, res)\n        # Откат: отменить выбор и восстановить предыдущее состояние\n        state.pop()\n\ndef subset_sum_i_naive(nums: list[int], target: int) -> list[list[int]]:\n    \"\"\"Решить задачу суммы подмножеств I (с повторяющимися подмножествами)\"\"\"\n    state = []  # Состояние (подмножество)\n    total = 0  # Сумма подмножеств\n    res = []  # Список результатов (список подмножеств)\n    backtrack(state, target, total, nums, res)\n    return res\n
        subset_sum_i_naive.cpp
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nvoid backtrack(vector<int> &state, int target, int total, vector<int> &choices, vector<vector<int>> &res) {\n    // Если сумма подмножества равна target, записать решение\n    if (total == target) {\n        res.push_back(state);\n        return;\n    }\n    // Перебор всех вариантов выбора\n    for (size_t i = 0; i < choices.size(); i++) {\n        // Отсечение: если сумма подмножества превышает target, пропустить этот выбор\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // Попытка: сделать выбор и обновить элемент и total\n        state.push_back(choices[i]);\n        // Перейти к следующему выбору\n        backtrack(state, target, total + choices[i], choices, res);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.pop_back();\n    }\n}\n\n/* Решить задачу суммы подмножеств I (с повторяющимися подмножествами) */\nvector<vector<int>> subsetSumINaive(vector<int> &nums, int target) {\n    vector<int> state;       // Состояние (подмножество)\n    int total = 0;           // Сумма подмножеств\n    vector<vector<int>> res; // Список результатов (список подмножеств)\n    backtrack(state, target, total, nums, res);\n    return res;\n}\n
        subset_sum_i_naive.java
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nvoid backtrack(List<Integer> state, int target, int total, int[] choices, List<List<Integer>> res) {\n    // Если сумма подмножества равна target, записать решение\n    if (total == target) {\n        res.add(new ArrayList<>(state));\n        return;\n    }\n    // Перебор всех вариантов выбора\n    for (int i = 0; i < choices.length; i++) {\n        // Отсечение: если сумма подмножества превышает target, пропустить этот выбор\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // Попытка: сделать выбор и обновить элемент и total\n        state.add(choices[i]);\n        // Перейти к следующему выбору\n        backtrack(state, target, total + choices[i], choices, res);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.remove(state.size() - 1);\n    }\n}\n\n/* Решить задачу суммы подмножеств I (с повторяющимися подмножествами) */\nList<List<Integer>> subsetSumINaive(int[] nums, int target) {\n    List<Integer> state = new ArrayList<>(); // Состояние (подмножество)\n    int total = 0; // Сумма подмножеств\n    List<List<Integer>> res = new ArrayList<>(); // Список результатов (список подмножеств)\n    backtrack(state, target, total, nums, res);\n    return res;\n}\n
        subset_sum_i_naive.cs
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nvoid Backtrack(List<int> state, int target, int total, int[] choices, List<List<int>> res) {\n    // Если сумма подмножества равна target, записать решение\n    if (total == target) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // Перебор всех вариантов выбора\n    for (int i = 0; i < choices.Length; i++) {\n        // Отсечение: если сумма подмножества превышает target, пропустить этот выбор\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // Попытка: сделать выбор и обновить элемент и total\n        state.Add(choices[i]);\n        // Перейти к следующему выбору\n        Backtrack(state, target, total + choices[i], choices, res);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.RemoveAt(state.Count - 1);\n    }\n}\n\n/* Решить задачу суммы подмножеств I (с повторяющимися подмножествами) */\nList<List<int>> SubsetSumINaive(int[] nums, int target) {\n    List<int> state = []; // Состояние (подмножество)\n    int total = 0; // Сумма подмножеств\n    List<List<int>> res = []; // Список результатов (список подмножеств)\n    Backtrack(state, target, total, nums, res);\n    return res;\n}\n
        subset_sum_i_naive.go
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nfunc backtrackSubsetSumINaive(total, target int, state, choices *[]int, res *[][]int) {\n    // Если сумма подмножества равна target, записать решение\n    if target == total {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n        return\n    }\n    // Перебор всех вариантов выбора\n    for i := 0; i < len(*choices); i++ {\n        // Отсечение: если сумма подмножества превышает target, пропустить этот выбор\n        if total+(*choices)[i] > target {\n            continue\n        }\n        // Попытка: сделать выбор и обновить элемент и total\n        *state = append(*state, (*choices)[i])\n        // Перейти к следующему выбору\n        backtrackSubsetSumINaive(total+(*choices)[i], target, state, choices, res)\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        *state = (*state)[:len(*state)-1]\n    }\n}\n\n/* Решить задачу суммы подмножеств I (с повторяющимися подмножествами) */\nfunc subsetSumINaive(nums []int, target int) [][]int {\n    state := make([]int, 0) // Состояние (подмножество)\n    total := 0              // Сумма подмножеств\n    res := make([][]int, 0) // Список результатов (список подмножеств)\n    backtrackSubsetSumINaive(total, target, &state, &nums, &res)\n    return res\n}\n
        subset_sum_i_naive.swift
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nfunc backtrack(state: inout [Int], target: Int, total: Int, choices: [Int], res: inout [[Int]]) {\n    // Если сумма подмножества равна target, записать решение\n    if total == target {\n        res.append(state)\n        return\n    }\n    // Перебор всех вариантов выбора\n    for i in choices.indices {\n        // Отсечение: если сумма подмножества превышает target, пропустить этот выбор\n        if total + choices[i] > target {\n            continue\n        }\n        // Попытка: сделать выбор и обновить элемент и total\n        state.append(choices[i])\n        // Перейти к следующему выбору\n        backtrack(state: &state, target: target, total: total + choices[i], choices: choices, res: &res)\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.removeLast()\n    }\n}\n\n/* Решить задачу суммы подмножеств I (с повторяющимися подмножествами) */\nfunc subsetSumINaive(nums: [Int], target: Int) -> [[Int]] {\n    var state: [Int] = [] // Состояние (подмножество)\n    let total = 0 // Сумма подмножеств\n    var res: [[Int]] = [] // Список результатов (список подмножеств)\n    backtrack(state: &state, target: target, total: total, choices: nums, res: &res)\n    return res\n}\n
        subset_sum_i_naive.js
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nfunction backtrack(state, target, total, choices, res) {\n    // Если сумма подмножества равна target, записать решение\n    if (total === target) {\n        res.push([...state]);\n        return;\n    }\n    // Перебор всех вариантов выбора\n    for (let i = 0; i < choices.length; i++) {\n        // Отсечение: если сумма подмножества превышает target, пропустить этот выбор\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // Попытка: сделать выбор и обновить элемент и total\n        state.push(choices[i]);\n        // Перейти к следующему выбору\n        backtrack(state, target, total + choices[i], choices, res);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.pop();\n    }\n}\n\n/* Решить задачу суммы подмножеств I (с повторяющимися подмножествами) */\nfunction subsetSumINaive(nums, target) {\n    const state = []; // Состояние (подмножество)\n    const total = 0; // Сумма подмножеств\n    const res = []; // Список результатов (список подмножеств)\n    backtrack(state, target, total, nums, res);\n    return res;\n}\n
        subset_sum_i_naive.ts
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nfunction backtrack(\n    state: number[],\n    target: number,\n    total: number,\n    choices: number[],\n    res: number[][]\n): void {\n    // Если сумма подмножества равна target, записать решение\n    if (total === target) {\n        res.push([...state]);\n        return;\n    }\n    // Перебор всех вариантов выбора\n    for (let i = 0; i < choices.length; i++) {\n        // Отсечение: если сумма подмножества превышает target, пропустить этот выбор\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // Попытка: сделать выбор и обновить элемент и total\n        state.push(choices[i]);\n        // Перейти к следующему выбору\n        backtrack(state, target, total + choices[i], choices, res);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.pop();\n    }\n}\n\n/* Решить задачу суммы подмножеств I (с повторяющимися подмножествами) */\nfunction subsetSumINaive(nums: number[], target: number): number[][] {\n    const state = []; // Состояние (подмножество)\n    const total = 0; // Сумма подмножеств\n    const res = []; // Список результатов (список подмножеств)\n    backtrack(state, target, total, nums, res);\n    return res;\n}\n
        subset_sum_i_naive.dart
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nvoid backtrack(\n  List<int> state,\n  int target,\n  int total,\n  List<int> choices,\n  List<List<int>> res,\n) {\n  // Если сумма подмножества равна target, записать решение\n  if (total == target) {\n    res.add(List.from(state));\n    return;\n  }\n  // Перебор всех вариантов выбора\n  for (int i = 0; i < choices.length; i++) {\n    // Отсечение: если сумма подмножества превышает target, пропустить этот выбор\n    if (total + choices[i] > target) {\n      continue;\n    }\n    // Попытка: сделать выбор и обновить элемент и total\n    state.add(choices[i]);\n    // Перейти к следующему выбору\n    backtrack(state, target, total + choices[i], choices, res);\n    // Откат: отменить выбор и восстановить предыдущее состояние\n    state.removeLast();\n  }\n}\n\n/* Решить задачу суммы подмножеств I (с повторяющимися подмножествами) */\nList<List<int>> subsetSumINaive(List<int> nums, int target) {\n  List<int> state = []; // Состояние (подмножество)\n  int total = 0; // Сумма элементов\n  List<List<int>> res = []; // Список результатов (список подмножеств)\n  backtrack(state, target, total, nums, res);\n  return res;\n}\n
        subset_sum_i_naive.rs
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nfn backtrack(\n    state: &mut Vec<i32>,\n    target: i32,\n    total: i32,\n    choices: &[i32],\n    res: &mut Vec<Vec<i32>>,\n) {\n    // Если сумма подмножества равна target, записать решение\n    if total == target {\n        res.push(state.clone());\n        return;\n    }\n    // Перебор всех вариантов выбора\n    for i in 0..choices.len() {\n        // Отсечение: если сумма подмножества превышает target, пропустить этот выбор\n        if total + choices[i] > target {\n            continue;\n        }\n        // Попытка: сделать выбор и обновить элемент и total\n        state.push(choices[i]);\n        // Перейти к следующему выбору\n        backtrack(state, target, total + choices[i], choices, res);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.pop();\n    }\n}\n\n/* Решить задачу суммы подмножеств I (с повторяющимися подмножествами) */\nfn subset_sum_i_naive(nums: &[i32], target: i32) -> Vec<Vec<i32>> {\n    let mut state = Vec::new(); // Состояние (подмножество)\n    let total = 0; // Сумма подмножеств\n    let mut res = Vec::new(); // Список результатов (список подмножеств)\n    backtrack(&mut state, target, total, nums, &mut res);\n    res\n}\n
        subset_sum_i_naive.c
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nvoid backtrack(int target, int total, int *choices, int choicesSize) {\n    // Если сумма подмножества равна target, записать решение\n    if (total == target) {\n        for (int i = 0; i < stateSize; i++) {\n            res[resSize][i] = state[i];\n        }\n        resColSizes[resSize++] = stateSize;\n        return;\n    }\n    // Перебор всех вариантов выбора\n    for (int i = 0; i < choicesSize; i++) {\n        // Отсечение: если сумма подмножества превышает target, пропустить этот выбор\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // Попытка: сделать выбор и обновить элемент и total\n        state[stateSize++] = choices[i];\n        // Перейти к следующему выбору\n        backtrack(target, total + choices[i], choices, choicesSize);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        stateSize--;\n    }\n}\n\n/* Решить задачу суммы подмножеств I (с повторяющимися подмножествами) */\nvoid subsetSumINaive(int *nums, int numsSize, int target) {\n    resSize = 0; // Инициализировать число решений нулем\n    backtrack(target, 0, nums, numsSize);\n}\n
        subset_sum_i_naive.kt
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nfun backtrack(\n    state: MutableList<Int>,\n    target: Int,\n    total: Int,\n    choices: IntArray,\n    res: MutableList<MutableList<Int>?>\n) {\n    // Если сумма подмножества равна target, записать решение\n    if (total == target) {\n        res.add(state.toMutableList())\n        return\n    }\n    // Перебор всех вариантов выбора\n    for (i in choices.indices) {\n        // Отсечение: если сумма подмножества превышает target, пропустить этот выбор\n        if (total + choices[i] > target) {\n            continue\n        }\n        // Попытка: сделать выбор и обновить элемент и total\n        state.add(choices[i])\n        // Перейти к следующему выбору\n        backtrack(state, target, total + choices[i], choices, res)\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.removeAt(state.size - 1)\n    }\n}\n\n/* Решить задачу суммы подмножеств I (с повторяющимися подмножествами) */\nfun subsetSumINaive(nums: IntArray, target: Int): MutableList<MutableList<Int>?> {\n    val state = mutableListOf<Int>() // Состояние (подмножество)\n    val total = 0 // Сумма подмножеств\n    val res = mutableListOf<MutableList<Int>?>() // Список результатов (список подмножеств)\n    backtrack(state, target, total, nums, res)\n    return res\n}\n
        subset_sum_i_naive.rb
        ### Алгоритм бэктрекинга: сумма подмножеств I ###\ndef backtrack(state, target, total, choices, res)\n  # Если сумма подмножества равна target, записать решение\n  if total == target\n    res << state.dup\n    return\n  end\n\n  # Перебор всех вариантов выбора\n  for i in 0...choices.length\n    # Отсечение: если сумма подмножества превышает target, пропустить этот выбор\n    next if total + choices[i] > target\n    # Попытка: сделать выбор и обновить элемент и total\n    state << choices[i]\n    # Перейти к следующему выбору\n    backtrack(state, target, total + choices[i], choices, res)\n    # Откат: отменить выбор и восстановить предыдущее состояние\n    state.pop\n  end\nend\n\n### Алгоритм бэктрекинга: сумма подмножеств I ###\ndef backtrack(state, target, total, choices, res)\n  # Если сумма подмножества равна target, записать решение\n  if total == target\n    res << state.dup\n    return\n  end\n\n  # Перебор всех вариантов выбора\n  for i in 0...choices.length\n    # Отсечение: если сумма подмножества превышает target, пропустить этот выбор\n    next if total + choices[i] > target\n    # Попытка: сделать выбор и обновить элемент и total\n    state << choices[i]\n    # Перейти к следующему выбору\n    backtrack(state, target, total + choices[i], choices, res)\n    # Откат: отменить выбор и восстановить предыдущее состояние\n    state.pop\n  end\nend\n\n# ## Решить задачу суммы подмножеств I (с повторяющимися подмножествами) ###\ndef subset_sum_i_naive(nums, target)\n  state = [] # Состояние (подмножество)\n  total = 0 # Сумма подмножеств\n  res = [] # Список результатов (список подмножеств)\n  backtrack(state, target, total, nums, res)\n  res\nend\n
        Визуализация кода

        Во весь экран >

        Если подать на этот код массив \\([3, 4, 5]\\) и целевое значение \\(9\\) , то на выходе мы получим \\([3, 3, 3], [4, 5], [5, 4]\\) . Хотя все подмножества с суммой \\(9\\) успешно найдены, среди них все же присутствуют дубликаты: \\([4, 5]\\) и \\([5, 4]\\) .

        Причина в том, что процесс поиска различает порядок выбора, тогда как для подмножеств порядок не важен. Как показано на рисунке 13-10, сначала выбрать \\(4\\) , а затем \\(5\\) , и сначала выбрать \\(5\\) , а затем \\(4\\) - это разные ветви поиска, но им соответствует одно и то же подмножество.

        Рисунок 13-10   Поиск подмножеств и обрезка по выходу за границу

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

        • Когда массив содержит много элементов, а особенно когда target велик, процесс поиска порождает огромное число повторяющихся подмножеств.
        • Сравнение подмножеств (то есть массивов) само по себе довольно затратно: сначала приходится сортировать массивы, а затем поэлементно сравнивать их.
        ","path":["Глава 13. Поиск с возвратом","13.3   Задача о сумме подмножеств"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#2","level":3,"title":"2.   Обрезка повторяющихся подмножеств","text":"

        Поэтому стоит выполнять устранение дубликатов прямо во время поиска, с помощью обрезки. Как видно на рисунке 13-11, повторяющиеся подмножества возникают тогда, когда элементы массива выбираются в разном порядке, например так.

        1. Если в первом и втором раундах выбрать соответственно \\(3\\) и \\(4\\) , то будут сгенерированы все подмножества, содержащие эти два элемента, и их можно обозначить как \\([3, 4, \\dots]\\) .
        2. После этого, если в первом раунде выбрать \\(4\\) , то во втором раунде нужно пропустить \\(3\\) , потому что подмножества \\([4, 3, \\dots]\\) полностью дублируют подмножества, уже построенные на шаге 1. .

        Во время поиска варианты на каждом уровне пробуются по одному слева направо, поэтому чем правее ветвь, тем больше ветвей оказывается отсечено.

        1. В первых двух раундах выбираются \\(3\\) и \\(5\\) , что дает подмножества \\([3, 5, \\dots]\\) .
        2. В первых двух раундах выбираются \\(4\\) и \\(5\\) , что дает подмножества \\([4, 5, \\dots]\\) .
        3. Если же в первом раунде выбрать \\(5\\) , то во втором раунде нужно пропустить \\(3\\) и \\(4\\) , потому что подмножества \\([5, 3, \\dots]\\) и \\([5, 4, \\dots]\\) полностью дублируют случаи, описанные в шагах 1. и 2. .

        Рисунок 13-11   Повторяющиеся подмножества из-за разного порядка выбора

        В общем виде, если входной массив имеет вид \\([x_1, x_2, \\dots, x_n]\\) , а последовательность выборов в ходе поиска равна \\([x_{i_1}, x_{i_2}, \\dots, x_{i_m}]\\) , то она должна удовлетворять условию \\(i_1 \\leq i_2 \\leq \\dots \\leq i_m\\). Все последовательности выборов, не удовлетворяющие этому условию, приводят к дубликатам и должны отсекаться.

        ","path":["Глава 13. Поиск с возвратом","13.3   Задача о сумме подмножеств"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#3","level":3,"title":"3.   Реализация кода","text":"

        Чтобы реализовать такую обрезку, инициализируем переменную start , которая будет указывать начальную точку обхода. После выбора элемента \\(x_i\\) следующий раунд начинается с индекса \\(i\\). Благодаря этому последовательность выборов всегда удовлетворяет условию \\(i_1 \\leq i_2 \\leq \\dots \\leq i_m\\) , а значит, каждое подмножество создается только один раз.

        Помимо этого, мы внесем в код еще два улучшения.

        • Перед началом поиска отсортируем массив nums . Тогда при обходе всех вариантов можно сразу прервать цикл, как только сумма подмножества превысит target , потому что все последующие элементы будут еще больше и их сумма тоже превысит target .
        • Откажемся от отдельной переменной суммы total и будем учитывать сумму через вычитание из target. Когда target станет равным \\(0\\) , решение фиксируется.
        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby subset_sum_i.py
        def backtrack(\n    state: list[int], target: int, choices: list[int], start: int, res: list[list[int]]\n):\n    \"\"\"Алгоритм бэктрекинга: сумма подмножеств I\"\"\"\n    # Если сумма подмножества равна target, записать решение\n    if target == 0:\n        res.append(list(state))\n        return\n    # Обойти все варианты выбора\n    # Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    for i in range(start, len(choices)):\n        # Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        # Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if target - choices[i] < 0:\n            break\n        # Попытка: сделать выбор и обновить target и start\n        state.append(choices[i])\n        # Перейти к следующему выбору\n        backtrack(state, target - choices[i], choices, i, res)\n        # Откат: отменить выбор и восстановить предыдущее состояние\n        state.pop()\n\ndef subset_sum_i(nums: list[int], target: int) -> list[list[int]]:\n    \"\"\"Решить задачу суммы подмножеств I\"\"\"\n    state = []  # Состояние (подмножество)\n    nums.sort()  # Отсортировать nums\n    start = 0  # Стартовая вершина обхода\n    res = []  # Список результатов (список подмножеств)\n    backtrack(state, target, nums, start, res)\n    return res\n
        subset_sum_i.cpp
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nvoid backtrack(vector<int> &state, int target, vector<int> &choices, int start, vector<vector<int>> &res) {\n    // Если сумма подмножества равна target, записать решение\n    if (target == 0) {\n        res.push_back(state);\n        return;\n    }\n    // Обойти все варианты выбора\n    // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    for (int i = start; i < choices.size(); i++) {\n        // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Попытка: сделать выбор и обновить target и start\n        state.push_back(choices[i]);\n        // Перейти к следующему выбору\n        backtrack(state, target - choices[i], choices, i, res);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.pop_back();\n    }\n}\n\n/* Решить задачу суммы подмножеств I */\nvector<vector<int>> subsetSumI(vector<int> &nums, int target) {\n    vector<int> state;              // Состояние (подмножество)\n    sort(nums.begin(), nums.end()); // Отсортировать nums\n    int start = 0;                  // Стартовая вершина обхода\n    vector<vector<int>> res;        // Список результатов (список подмножеств)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
        subset_sum_i.java
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nvoid backtrack(List<Integer> state, int target, int[] choices, int start, List<List<Integer>> res) {\n    // Если сумма подмножества равна target, записать решение\n    if (target == 0) {\n        res.add(new ArrayList<>(state));\n        return;\n    }\n    // Обойти все варианты выбора\n    // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    for (int i = start; i < choices.length; i++) {\n        // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Попытка: сделать выбор и обновить target и start\n        state.add(choices[i]);\n        // Перейти к следующему выбору\n        backtrack(state, target - choices[i], choices, i, res);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.remove(state.size() - 1);\n    }\n}\n\n/* Решить задачу суммы подмножеств I */\nList<List<Integer>> subsetSumI(int[] nums, int target) {\n    List<Integer> state = new ArrayList<>(); // Состояние (подмножество)\n    Arrays.sort(nums); // Отсортировать nums\n    int start = 0; // Стартовая вершина обхода\n    List<List<Integer>> res = new ArrayList<>(); // Список результатов (список подмножеств)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
        subset_sum_i.cs
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nvoid Backtrack(List<int> state, int target, int[] choices, int start, List<List<int>> res) {\n    // Если сумма подмножества равна target, записать решение\n    if (target == 0) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // Обойти все варианты выбора\n    // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    for (int i = start; i < choices.Length; i++) {\n        // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Попытка: сделать выбор и обновить target и start\n        state.Add(choices[i]);\n        // Перейти к следующему выбору\n        Backtrack(state, target - choices[i], choices, i, res);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.RemoveAt(state.Count - 1);\n    }\n}\n\n/* Решить задачу суммы подмножеств I */\nList<List<int>> SubsetSumI(int[] nums, int target) {\n    List<int> state = []; // Состояние (подмножество)\n    Array.Sort(nums); // Отсортировать nums\n    int start = 0; // Стартовая вершина обхода\n    List<List<int>> res = []; // Список результатов (список подмножеств)\n    Backtrack(state, target, nums, start, res);\n    return res;\n}\n
        subset_sum_i.go
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nfunc backtrackSubsetSumI(start, target int, state, choices *[]int, res *[][]int) {\n    // Если сумма подмножества равна target, записать решение\n    if target == 0 {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n        return\n    }\n    // Обойти все варианты выбора\n    // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    for i := start; i < len(*choices); i++ {\n        // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if target-(*choices)[i] < 0 {\n            break\n        }\n        // Попытка: сделать выбор и обновить target и start\n        *state = append(*state, (*choices)[i])\n        // Перейти к следующему выбору\n        backtrackSubsetSumI(i, target-(*choices)[i], state, choices, res)\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        *state = (*state)[:len(*state)-1]\n    }\n}\n\n/* Решить задачу суммы подмножеств I */\nfunc subsetSumI(nums []int, target int) [][]int {\n    state := make([]int, 0) // Состояние (подмножество)\n    sort.Ints(nums)         // Отсортировать nums\n    start := 0              // Стартовая вершина обхода\n    res := make([][]int, 0) // Список результатов (список подмножеств)\n    backtrackSubsetSumI(start, target, &state, &nums, &res)\n    return res\n}\n
        subset_sum_i.swift
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nfunc backtrack(state: inout [Int], target: Int, choices: [Int], start: Int, res: inout [[Int]]) {\n    // Если сумма подмножества равна target, записать решение\n    if target == 0 {\n        res.append(state)\n        return\n    }\n    // Обойти все варианты выбора\n    // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    for i in choices.indices.dropFirst(start) {\n        // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if target - choices[i] < 0 {\n            break\n        }\n        // Попытка: сделать выбор и обновить target и start\n        state.append(choices[i])\n        // Перейти к следующему выбору\n        backtrack(state: &state, target: target - choices[i], choices: choices, start: i, res: &res)\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.removeLast()\n    }\n}\n\n/* Решить задачу суммы подмножеств I */\nfunc subsetSumI(nums: [Int], target: Int) -> [[Int]] {\n    var state: [Int] = [] // Состояние (подмножество)\n    let nums = nums.sorted() // Отсортировать nums\n    let start = 0 // Стартовая вершина обхода\n    var res: [[Int]] = [] // Список результатов (список подмножеств)\n    backtrack(state: &state, target: target, choices: nums, start: start, res: &res)\n    return res\n}\n
        subset_sum_i.js
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nfunction backtrack(state, target, choices, start, res) {\n    // Если сумма подмножества равна target, записать решение\n    if (target === 0) {\n        res.push([...state]);\n        return;\n    }\n    // Обойти все варианты выбора\n    // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    for (let i = start; i < choices.length; i++) {\n        // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Попытка: сделать выбор и обновить target и start\n        state.push(choices[i]);\n        // Перейти к следующему выбору\n        backtrack(state, target - choices[i], choices, i, res);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.pop();\n    }\n}\n\n/* Решить задачу суммы подмножеств I */\nfunction subsetSumI(nums, target) {\n    const state = []; // Состояние (подмножество)\n    nums.sort((a, b) => a - b); // Отсортировать nums\n    const start = 0; // Стартовая вершина обхода\n    const res = []; // Список результатов (список подмножеств)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
        subset_sum_i.ts
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nfunction backtrack(\n    state: number[],\n    target: number,\n    choices: number[],\n    start: number,\n    res: number[][]\n): void {\n    // Если сумма подмножества равна target, записать решение\n    if (target === 0) {\n        res.push([...state]);\n        return;\n    }\n    // Обойти все варианты выбора\n    // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    for (let i = start; i < choices.length; i++) {\n        // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Попытка: сделать выбор и обновить target и start\n        state.push(choices[i]);\n        // Перейти к следующему выбору\n        backtrack(state, target - choices[i], choices, i, res);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.pop();\n    }\n}\n\n/* Решить задачу суммы подмножеств I */\nfunction subsetSumI(nums: number[], target: number): number[][] {\n    const state = []; // Состояние (подмножество)\n    nums.sort((a, b) => a - b); // Отсортировать nums\n    const start = 0; // Стартовая вершина обхода\n    const res = []; // Список результатов (список подмножеств)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
        subset_sum_i.dart
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nvoid backtrack(\n  List<int> state,\n  int target,\n  List<int> choices,\n  int start,\n  List<List<int>> res,\n) {\n  // Если сумма подмножества равна target, записать решение\n  if (target == 0) {\n    res.add(List.from(state));\n    return;\n  }\n  // Обойти все варианты выбора\n  // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n  for (int i = start; i < choices.length; i++) {\n    // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n    // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n    if (target - choices[i] < 0) {\n      break;\n    }\n    // Попытка: сделать выбор и обновить target и start\n    state.add(choices[i]);\n    // Перейти к следующему выбору\n    backtrack(state, target - choices[i], choices, i, res);\n    // Откат: отменить выбор и восстановить предыдущее состояние\n    state.removeLast();\n  }\n}\n\n/* Решить задачу суммы подмножеств I */\nList<List<int>> subsetSumI(List<int> nums, int target) {\n  List<int> state = []; // Состояние (подмножество)\n  nums.sort(); // Отсортировать nums\n  int start = 0; // Стартовая вершина обхода\n  List<List<int>> res = []; // Список результатов (список подмножеств)\n  backtrack(state, target, nums, start, res);\n  return res;\n}\n
        subset_sum_i.rs
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nfn backtrack(\n    state: &mut Vec<i32>,\n    target: i32,\n    choices: &[i32],\n    start: usize,\n    res: &mut Vec<Vec<i32>>,\n) {\n    // Если сумма подмножества равна target, записать решение\n    if target == 0 {\n        res.push(state.clone());\n        return;\n    }\n    // Обойти все варианты выбора\n    // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    for i in start..choices.len() {\n        // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if target - choices[i] < 0 {\n            break;\n        }\n        // Попытка: сделать выбор и обновить target и start\n        state.push(choices[i]);\n        // Перейти к следующему выбору\n        backtrack(state, target - choices[i], choices, i, res);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.pop();\n    }\n}\n\n/* Решить задачу суммы подмножеств I */\nfn subset_sum_i(nums: &mut [i32], target: i32) -> Vec<Vec<i32>> {\n    let mut state = Vec::new(); // Состояние (подмножество)\n    nums.sort(); // Отсортировать nums\n    let start = 0; // Стартовая вершина обхода\n    let mut res = Vec::new(); // Список результатов (список подмножеств)\n    backtrack(&mut state, target, nums, start, &mut res);\n    res\n}\n
        subset_sum_i.c
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nvoid backtrack(int target, int *choices, int choicesSize, int start) {\n    // Если сумма подмножества равна target, записать решение\n    if (target == 0) {\n        for (int i = 0; i < stateSize; ++i) {\n            res[resSize][i] = state[i];\n        }\n        resColSizes[resSize++] = stateSize;\n        return;\n    }\n    // Обойти все варианты выбора\n    // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    for (int i = start; i < choicesSize; i++) {\n        // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Попытка: сделать выбор и обновить target и start\n        state[stateSize] = choices[i];\n        stateSize++;\n        // Перейти к следующему выбору\n        backtrack(target - choices[i], choices, choicesSize, i);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        stateSize--;\n    }\n}\n\n/* Решить задачу суммы подмножеств I */\nvoid subsetSumI(int *nums, int numsSize, int target) {\n    qsort(nums, numsSize, sizeof(int), cmp); // Отсортировать nums\n    int start = 0;                           // Стартовая вершина обхода\n    backtrack(target, nums, numsSize, start);\n}\n
        subset_sum_i.kt
        /* Алгоритм бэктрекинга: сумма подмножеств I */\nfun backtrack(\n    state: MutableList<Int>,\n    target: Int,\n    choices: IntArray,\n    start: Int,\n    res: MutableList<MutableList<Int>?>\n) {\n    // Если сумма подмножества равна target, записать решение\n    if (target == 0) {\n        res.add(state.toMutableList())\n        return\n    }\n    // Обойти все варианты выбора\n    // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    for (i in start..<choices.size) {\n        // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if (target - choices[i] < 0) {\n            break\n        }\n        // Попытка: сделать выбор и обновить target и start\n        state.add(choices[i])\n        // Перейти к следующему выбору\n        backtrack(state, target - choices[i], choices, i, res)\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.removeAt(state.size - 1)\n    }\n}\n\n/* Решить задачу суммы подмножеств I */\nfun subsetSumI(nums: IntArray, target: Int): MutableList<MutableList<Int>?> {\n    val state = mutableListOf<Int>() // Состояние (подмножество)\n    nums.sort() // Отсортировать nums\n    val start = 0 // Стартовая вершина обхода\n    val res = mutableListOf<MutableList<Int>?>() // Список результатов (список подмножеств)\n    backtrack(state, target, nums, start, res)\n    return res\n}\n
        subset_sum_i.rb
        ### Алгоритм бэктрекинга: сумма подмножеств I ###\ndef backtrack(state, target, choices, start, res)\n  # Если сумма подмножества равна target, записать решение\n  if target.zero?\n    res << state.dup\n    return\n  end\n  # Обойти все варианты выбора\n  # Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n  for i in start...choices.length\n    # Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n    # Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n    break if target - choices[i] < 0\n    # Попытка: сделать выбор и обновить target и start\n    state << choices[i]\n    # Перейти к следующему выбору\n    backtrack(state, target - choices[i], choices, i, res)\n    # Откат: отменить выбор и восстановить предыдущее состояние\n    state.pop\n  end\nend\n\n### Решить задачу суммы подмножеств I ###\ndef subset_sum_i(nums, target)\n  state = [] # Состояние (подмножество)\n  nums.sort! # Отсортировать nums\n  start = 0 # Стартовая вершина обхода\n  res = [] # Список результатов (список подмножеств)\n  backtrack(state, target, nums, start, res)\n  res\nend\n
        Визуализация кода

        Во весь экран >

        На рисунке 13-12 показан полный процесс поиска с возвратом для массива \\([3, 4, 5]\\) и целевого значения \\(9\\) .

        Рисунок 13-12   Процесс поиска с возвратом для задачи о сумме подмножеств I

        ","path":["Глава 13. Поиск с возвратом","13.3   Задача о сумме подмножеств"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1332","level":2,"title":"13.3.2   Учет повторяющихся элементов","text":"

        Question

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

        По сравнению с предыдущей задачей во входном массиве теперь могут присутствовать повторяющиеся элементы, и это создает новую проблему. Например, если дан массив \\([4, \\hat{4}, 5]\\) и целевое значение \\(9\\) , то существующий код вернет результат \\([4, 5], [\\hat{4}, 5]\\) , то есть с повторяющимся подмножеством.

        Причина появления дублей в том, что равные элементы выбираются несколько раз в одном и том же раунде. На рисунке 13-13 в первом раунде существует три варианта выбора, и два из них равны \\(4\\). Из-за этого появляются две дублирующиеся ветви поиска и, соответственно, повторяющиеся подмножества. Точно так же два элемента \\(4\\) во втором раунде тоже порождают дубликаты.

        Рисунок 13-13   Повторяющиеся подмножества из-за равных элементов

        ","path":["Глава 13. Поиск с возвратом","13.3   Задача о сумме подмножеств"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1_1","level":3,"title":"1.   Обрезка равных элементов","text":"

        Чтобы решить эту проблему, нужно ограничить выбор равных элементов так, чтобы в каждом раунде каждый из них выбирался только один раз. Реализуется это довольно естественно: поскольку массив отсортирован, равные элементы стоят рядом. Значит, если в текущем раунде текущий элемент равен соседнему слева, то этот вариант уже был рассмотрен, и текущий элемент нужно пропустить.

        Одновременно по условию этой задачи каждый элемент массива можно выбрать только один раз. К счастью, это ограничение тоже можно реализовать через переменную start : после выбора элемента \\(x_i\\) следующий раунд начинается с индекса \\(i + 1\\) . Так мы одновременно убираем повторяющиеся подмножества и исключаем повторный выбор одного и того же элемента.

        ","path":["Глава 13. Поиск с возвратом","13.3   Задача о сумме подмножеств"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#2_1","level":3,"title":"2.   Реализация кода","text":"PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby subset_sum_ii.py
        def backtrack(\n    state: list[int], target: int, choices: list[int], start: int, res: list[list[int]]\n):\n    \"\"\"Алгоритм бэктрекинга: сумма подмножеств II\"\"\"\n    # Если сумма подмножества равна target, записать решение\n    if target == 0:\n        res.append(list(state))\n        return\n    # Обойти все варианты выбора\n    # Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    # Отсечение 3: начинать обход с start, чтобы избежать повторного выбора одного и того же элемента\n    for i in range(start, len(choices)):\n        # Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        # Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if target - choices[i] < 0:\n            break\n        # Отсечение 4: если этот элемент равен элементу слева, значит ветвь поиска повторяется, ее нужно сразу пропустить\n        if i > start and choices[i] == choices[i - 1]:\n            continue\n        # Попытка: сделать выбор и обновить target и start\n        state.append(choices[i])\n        # Перейти к следующему выбору\n        backtrack(state, target - choices[i], choices, i + 1, res)\n        # Откат: отменить выбор и восстановить предыдущее состояние\n        state.pop()\n\ndef subset_sum_ii(nums: list[int], target: int) -> list[list[int]]:\n    \"\"\"Решить задачу суммы подмножеств II\"\"\"\n    state = []  # Состояние (подмножество)\n    nums.sort()  # Отсортировать nums\n    start = 0  # Стартовая вершина обхода\n    res = []  # Список результатов (список подмножеств)\n    backtrack(state, target, nums, start, res)\n    return res\n
        subset_sum_ii.cpp
        /* Алгоритм бэктрекинга: сумма подмножеств II */\nvoid backtrack(vector<int> &state, int target, vector<int> &choices, int start, vector<vector<int>> &res) {\n    // Если сумма подмножества равна target, записать решение\n    if (target == 0) {\n        res.push_back(state);\n        return;\n    }\n    // Обойти все варианты выбора\n    // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    // Отсечение 3: начинать обход с start, чтобы избежать повторного выбора одного и того же элемента\n    for (int i = start; i < choices.size(); i++) {\n        // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Отсечение 4: если этот элемент равен элементу слева, значит ветвь поиска повторяется, ее нужно сразу пропустить\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue;\n        }\n        // Попытка: сделать выбор и обновить target и start\n        state.push_back(choices[i]);\n        // Перейти к следующему выбору\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.pop_back();\n    }\n}\n\n/* Решить задачу суммы подмножеств II */\nvector<vector<int>> subsetSumII(vector<int> &nums, int target) {\n    vector<int> state;              // Состояние (подмножество)\n    sort(nums.begin(), nums.end()); // Отсортировать nums\n    int start = 0;                  // Стартовая вершина обхода\n    vector<vector<int>> res;        // Список результатов (список подмножеств)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
        subset_sum_ii.java
        /* Алгоритм бэктрекинга: сумма подмножеств II */\nvoid backtrack(List<Integer> state, int target, int[] choices, int start, List<List<Integer>> res) {\n    // Если сумма подмножества равна target, записать решение\n    if (target == 0) {\n        res.add(new ArrayList<>(state));\n        return;\n    }\n    // Обойти все варианты выбора\n    // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    // Отсечение 3: начинать обход с start, чтобы избежать повторного выбора одного и того же элемента\n    for (int i = start; i < choices.length; i++) {\n        // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Отсечение 4: если этот элемент равен элементу слева, значит ветвь поиска повторяется, ее нужно сразу пропустить\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue;\n        }\n        // Попытка: сделать выбор и обновить target и start\n        state.add(choices[i]);\n        // Перейти к следующему выбору\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.remove(state.size() - 1);\n    }\n}\n\n/* Решить задачу суммы подмножеств II */\nList<List<Integer>> subsetSumII(int[] nums, int target) {\n    List<Integer> state = new ArrayList<>(); // Состояние (подмножество)\n    Arrays.sort(nums); // Отсортировать nums\n    int start = 0; // Стартовая вершина обхода\n    List<List<Integer>> res = new ArrayList<>(); // Список результатов (список подмножеств)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
        subset_sum_ii.cs
        /* Алгоритм бэктрекинга: сумма подмножеств II */\nvoid Backtrack(List<int> state, int target, int[] choices, int start, List<List<int>> res) {\n    // Если сумма подмножества равна target, записать решение\n    if (target == 0) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // Обойти все варианты выбора\n    // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    // Отсечение 3: начинать обход с start, чтобы избежать повторного выбора одного и того же элемента\n    for (int i = start; i < choices.Length; i++) {\n        // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Отсечение 4: если этот элемент равен элементу слева, значит ветвь поиска повторяется, ее нужно сразу пропустить\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue;\n        }\n        // Попытка: сделать выбор и обновить target и start\n        state.Add(choices[i]);\n        // Перейти к следующему выбору\n        Backtrack(state, target - choices[i], choices, i + 1, res);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.RemoveAt(state.Count - 1);\n    }\n}\n\n/* Решить задачу суммы подмножеств II */\nList<List<int>> SubsetSumII(int[] nums, int target) {\n    List<int> state = []; // Состояние (подмножество)\n    Array.Sort(nums); // Отсортировать nums\n    int start = 0; // Стартовая вершина обхода\n    List<List<int>> res = []; // Список результатов (список подмножеств)\n    Backtrack(state, target, nums, start, res);\n    return res;\n}\n
        subset_sum_ii.go
        /* Алгоритм бэктрекинга: сумма подмножеств II */\nfunc backtrackSubsetSumII(start, target int, state, choices *[]int, res *[][]int) {\n    // Если сумма подмножества равна target, записать решение\n    if target == 0 {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n        return\n    }\n    // Обойти все варианты выбора\n    // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    // Отсечение 3: начинать обход с start, чтобы избежать повторного выбора одного и того же элемента\n    for i := start; i < len(*choices); i++ {\n        // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if target-(*choices)[i] < 0 {\n            break\n        }\n        // Отсечение 4: если этот элемент равен элементу слева, значит ветвь поиска повторяется, ее нужно сразу пропустить\n        if i > start && (*choices)[i] == (*choices)[i-1] {\n            continue\n        }\n        // Попытка: сделать выбор и обновить target и start\n        *state = append(*state, (*choices)[i])\n        // Перейти к следующему выбору\n        backtrackSubsetSumII(i+1, target-(*choices)[i], state, choices, res)\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        *state = (*state)[:len(*state)-1]\n    }\n}\n\n/* Решить задачу суммы подмножеств II */\nfunc subsetSumII(nums []int, target int) [][]int {\n    state := make([]int, 0) // Состояние (подмножество)\n    sort.Ints(nums)         // Отсортировать nums\n    start := 0              // Стартовая вершина обхода\n    res := make([][]int, 0) // Список результатов (список подмножеств)\n    backtrackSubsetSumII(start, target, &state, &nums, &res)\n    return res\n}\n
        subset_sum_ii.swift
        /* Алгоритм бэктрекинга: сумма подмножеств II */\nfunc backtrack(state: inout [Int], target: Int, choices: [Int], start: Int, res: inout [[Int]]) {\n    // Если сумма подмножества равна target, записать решение\n    if target == 0 {\n        res.append(state)\n        return\n    }\n    // Обойти все варианты выбора\n    // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    // Отсечение 3: начинать обход с start, чтобы избежать повторного выбора одного и того же элемента\n    for i in choices.indices.dropFirst(start) {\n        // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if target - choices[i] < 0 {\n            break\n        }\n        // Отсечение 4: если этот элемент равен элементу слева, значит ветвь поиска повторяется, ее нужно сразу пропустить\n        if i > start, choices[i] == choices[i - 1] {\n            continue\n        }\n        // Попытка: сделать выбор и обновить target и start\n        state.append(choices[i])\n        // Перейти к следующему выбору\n        backtrack(state: &state, target: target - choices[i], choices: choices, start: i + 1, res: &res)\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.removeLast()\n    }\n}\n\n/* Решить задачу суммы подмножеств II */\nfunc subsetSumII(nums: [Int], target: Int) -> [[Int]] {\n    var state: [Int] = [] // Состояние (подмножество)\n    let nums = nums.sorted() // Отсортировать nums\n    let start = 0 // Стартовая вершина обхода\n    var res: [[Int]] = [] // Список результатов (список подмножеств)\n    backtrack(state: &state, target: target, choices: nums, start: start, res: &res)\n    return res\n}\n
        subset_sum_ii.js
        /* Алгоритм бэктрекинга: сумма подмножеств II */\nfunction backtrack(state, target, choices, start, res) {\n    // Если сумма подмножества равна target, записать решение\n    if (target === 0) {\n        res.push([...state]);\n        return;\n    }\n    // Обойти все варианты выбора\n    // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    // Отсечение 3: начинать обход с start, чтобы избежать повторного выбора одного и того же элемента\n    for (let i = start; i < choices.length; i++) {\n        // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Отсечение 4: если этот элемент равен элементу слева, значит ветвь поиска повторяется, ее нужно сразу пропустить\n        if (i > start && choices[i] === choices[i - 1]) {\n            continue;\n        }\n        // Попытка: сделать выбор и обновить target и start\n        state.push(choices[i]);\n        // Перейти к следующему выбору\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.pop();\n    }\n}\n\n/* Решить задачу суммы подмножеств II */\nfunction subsetSumII(nums, target) {\n    const state = []; // Состояние (подмножество)\n    nums.sort((a, b) => a - b); // Отсортировать nums\n    const start = 0; // Стартовая вершина обхода\n    const res = []; // Список результатов (список подмножеств)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
        subset_sum_ii.ts
        /* Алгоритм бэктрекинга: сумма подмножеств II */\nfunction backtrack(\n    state: number[],\n    target: number,\n    choices: number[],\n    start: number,\n    res: number[][]\n): void {\n    // Если сумма подмножества равна target, записать решение\n    if (target === 0) {\n        res.push([...state]);\n        return;\n    }\n    // Обойти все варианты выбора\n    // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    // Отсечение 3: начинать обход с start, чтобы избежать повторного выбора одного и того же элемента\n    for (let i = start; i < choices.length; i++) {\n        // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Отсечение 4: если этот элемент равен элементу слева, значит ветвь поиска повторяется, ее нужно сразу пропустить\n        if (i > start && choices[i] === choices[i - 1]) {\n            continue;\n        }\n        // Попытка: сделать выбор и обновить target и start\n        state.push(choices[i]);\n        // Перейти к следующему выбору\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.pop();\n    }\n}\n\n/* Решить задачу суммы подмножеств II */\nfunction subsetSumII(nums: number[], target: number): number[][] {\n    const state = []; // Состояние (подмножество)\n    nums.sort((a, b) => a - b); // Отсортировать nums\n    const start = 0; // Стартовая вершина обхода\n    const res = []; // Список результатов (список подмножеств)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
        subset_sum_ii.dart
        /* Алгоритм бэктрекинга: сумма подмножеств II */\nvoid backtrack(\n  List<int> state,\n  int target,\n  List<int> choices,\n  int start,\n  List<List<int>> res,\n) {\n  // Если сумма подмножества равна target, записать решение\n  if (target == 0) {\n    res.add(List.from(state));\n    return;\n  }\n  // Обойти все варианты выбора\n  // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n  // Отсечение 3: начинать обход с start, чтобы избежать повторного выбора одного и того же элемента\n  for (int i = start; i < choices.length; i++) {\n    // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n    // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n    if (target - choices[i] < 0) {\n      break;\n    }\n    // Отсечение 4: если этот элемент равен элементу слева, значит ветвь поиска повторяется, ее нужно сразу пропустить\n    if (i > start && choices[i] == choices[i - 1]) {\n      continue;\n    }\n    // Попытка: сделать выбор и обновить target и start\n    state.add(choices[i]);\n    // Перейти к следующему выбору\n    backtrack(state, target - choices[i], choices, i + 1, res);\n    // Откат: отменить выбор и восстановить предыдущее состояние\n    state.removeLast();\n  }\n}\n\n/* Решить задачу суммы подмножеств II */\nList<List<int>> subsetSumII(List<int> nums, int target) {\n  List<int> state = []; // Состояние (подмножество)\n  nums.sort(); // Отсортировать nums\n  int start = 0; // Стартовая вершина обхода\n  List<List<int>> res = []; // Список результатов (список подмножеств)\n  backtrack(state, target, nums, start, res);\n  return res;\n}\n
        subset_sum_ii.rs
        /* Алгоритм бэктрекинга: сумма подмножеств II */\nfn backtrack(\n    state: &mut Vec<i32>,\n    target: i32,\n    choices: &[i32],\n    start: usize,\n    res: &mut Vec<Vec<i32>>,\n) {\n    // Если сумма подмножества равна target, записать решение\n    if target == 0 {\n        res.push(state.clone());\n        return;\n    }\n    // Обойти все варианты выбора\n    // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    // Отсечение 3: начинать обход с start, чтобы избежать повторного выбора одного и того же элемента\n    for i in start..choices.len() {\n        // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if target - choices[i] < 0 {\n            break;\n        }\n        // Отсечение 4: если этот элемент равен элементу слева, значит ветвь поиска повторяется, ее нужно сразу пропустить\n        if i > start && choices[i] == choices[i - 1] {\n            continue;\n        }\n        // Попытка: сделать выбор и обновить target и start\n        state.push(choices[i]);\n        // Перейти к следующему выбору\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.pop();\n    }\n}\n\n/* Решить задачу суммы подмножеств II */\nfn subset_sum_ii(nums: &mut [i32], target: i32) -> Vec<Vec<i32>> {\n    let mut state = Vec::new(); // Состояние (подмножество)\n    nums.sort(); // Отсортировать nums\n    let start = 0; // Стартовая вершина обхода\n    let mut res = Vec::new(); // Список результатов (список подмножеств)\n    backtrack(&mut state, target, nums, start, &mut res);\n    res\n}\n
        subset_sum_ii.c
        /* Алгоритм бэктрекинга: сумма подмножеств II */\nvoid backtrack(int target, int *choices, int choicesSize, int start) {\n    // Если сумма подмножества равна target, записать решение\n    if (target == 0) {\n        for (int i = 0; i < stateSize; i++) {\n            res[resSize][i] = state[i];\n        }\n        resColSizes[resSize++] = stateSize;\n        return;\n    }\n    // Обойти все варианты выбора\n    // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    // Отсечение 3: начинать обход с start, чтобы избежать повторного выбора одного и того же элемента\n    for (int i = start; i < choicesSize; i++) {\n        // Отсечение 1: если сумма подмножества превышает target, сразу пропустить\n        if (target - choices[i] < 0) {\n            continue;\n        }\n        // Отсечение 4: если этот элемент равен элементу слева, значит ветвь поиска повторяется, ее нужно сразу пропустить\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue;\n        }\n        // Попытка: сделать выбор и обновить target и start\n        state[stateSize] = choices[i];\n        stateSize++;\n        // Перейти к следующему выбору\n        backtrack(target - choices[i], choices, choicesSize, i + 1);\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        stateSize--;\n    }\n}\n\n/* Решить задачу суммы подмножеств II */\nvoid subsetSumII(int *nums, int numsSize, int target) {\n    // Отсортировать nums\n    qsort(nums, numsSize, sizeof(int), cmp);\n    // Начать бэктрекинг\n    backtrack(target, nums, numsSize, 0);\n}\n
        subset_sum_ii.kt
        /* Алгоритм бэктрекинга: сумма подмножеств II */\nfun backtrack(\n    state: MutableList<Int>,\n    target: Int,\n    choices: IntArray,\n    start: Int,\n    res: MutableList<MutableList<Int>?>\n) {\n    // Если сумма подмножества равна target, записать решение\n    if (target == 0) {\n        res.add(state.toMutableList())\n        return\n    }\n    // Обойти все варианты выбора\n    // Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n    // Отсечение 3: начинать обход с start, чтобы избежать повторного выбора одного и того же элемента\n    for (i in start..<choices.size) {\n        // Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n        // Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n        if (target - choices[i] < 0) {\n            break\n        }\n        // Отсечение 4: если этот элемент равен элементу слева, значит ветвь поиска повторяется, ее нужно сразу пропустить\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue\n        }\n        // Попытка: сделать выбор и обновить target и start\n        state.add(choices[i])\n        // Перейти к следующему выбору\n        backtrack(state, target - choices[i], choices, i + 1, res)\n        // Откат: отменить выбор и восстановить предыдущее состояние\n        state.removeAt(state.size - 1)\n    }\n}\n\n/* Решить задачу суммы подмножеств II */\nfun subsetSumII(nums: IntArray, target: Int): MutableList<MutableList<Int>?> {\n    val state = mutableListOf<Int>() // Состояние (подмножество)\n    nums.sort() // Отсортировать nums\n    val start = 0 // Стартовая вершина обхода\n    val res = mutableListOf<MutableList<Int>?>() // Список результатов (список подмножеств)\n    backtrack(state, target, nums, start, res)\n    return res\n}\n
        subset_sum_ii.rb
        ### Алгоритм бэктрекинга: сумма подмножеств II ###\ndef backtrack(state, target, choices, start, res)\n  # Если сумма подмножества равна target, записать решение\n  if target.zero?\n    res << state.dup\n    return\n  end\n\n  # Обойти все варианты выбора\n  # Отсечение 2: начинать обход с start, чтобы избежать генерации повторяющихся подмножеств\n  # Отсечение 3: начинать обход с start, чтобы избежать повторного выбора одного и того же элемента\n  for i in start...choices.length\n    # Отсечение 1: если сумма подмножества превышает target, немедленно завершить цикл\n    # Это связано с тем, что массив уже отсортирован, следующие элементы больше, и сумма подмножества точно превысит target\n    break if target - choices[i] < 0\n    # Отсечение 4: если этот элемент равен элементу слева, значит ветвь поиска повторяется, ее нужно сразу пропустить\n    next if i > start && choices[i] == choices[i - 1]\n    # Попытка: сделать выбор и обновить target и start\n    state << choices[i]\n    # Перейти к следующему выбору\n    backtrack(state, target - choices[i], choices, i + 1, res)\n    # Откат: отменить выбор и восстановить предыдущее состояние\n    state.pop\n  end\nend\n\n### Решить задачу суммы подмножеств II ###\ndef subset_sum_ii(nums, target)\n  state = [] # Состояние (подмножество)\n  nums.sort! # Отсортировать nums\n  start = 0 # Стартовая вершина обхода\n  res = [] # Список результатов (список подмножеств)\n  backtrack(state, target, nums, start, res)\n  res\nend\n
        Визуализация кода

        Во весь экран >

        На рисунке 13-14 показан процесс поиска с возвратом для массива \\([4, 4, 5]\\) и целевого значения \\(9\\) . В нем используются четыре вида обрезки. Попробуйте сопоставить рисунок с комментариями в коде, чтобы понять полный процесс поиска и то, как работает каждый тип обрезки.

        Рисунок 13-14   Процесс поиска с возвратом для задачи о сумме подмножеств II

        ","path":["Глава 13. Поиск с возвратом","13.3   Задача о сумме подмножеств"],"tags":[]},{"location":"chapter_backtracking/summary/","level":1,"title":"13.5   Резюме","text":"","path":["Глава 13. Поиск с возвратом","13.5   Резюме"],"tags":[]},{"location":"chapter_backtracking/summary/#1","level":3,"title":"1.   Ключевые выводы","text":"
        • Алгоритм поиска с возвратом по своей сути является методом полного перебора: он ищет решения путем обхода пространства решений в глубину. Во время поиска он фиксирует решения, удовлетворяющие условиям, пока не найдет все такие решения или пока обход не завершится.
        • Процесс поиска с возвратом состоит из двух частей: попытки и отката. Он с помощью поиска в глубину пробует разные варианты выбора. Когда встречается состояние, не удовлетворяющее ограничениям, алгоритм отменяет предыдущий выбор, возвращается к прошлому состоянию и продолжает пробовать другие варианты. Попытка и откат являются двумя противоположными по направлению действиями.
        • Задачи поиска с возвратом обычно содержат несколько ограничений, которые можно использовать для обрезки. Обрезка позволяет заранее завершать ненужные ветви поиска и тем самым значительно повышать эффективность.
        • Алгоритм поиска с возвратом в первую очередь применяется для решения поисковых задач и задач с ограничениями. Задачи комбинаторной оптимизации тоже можно решать с его помощью, но для них часто существуют более эффективные или более подходящие методы.
        • Задача о перестановках нацелена на поиск всех возможных перестановок элементов данного множества. Мы используем массив для записи того, был ли выбран каждый элемент, и отсекаем ветви, где один и тот же элемент выбирается повторно, чтобы гарантировать однократный выбор каждого элемента.
        • В задаче о перестановках, если во множестве присутствуют повторяющиеся элементы, в итоговом результате возникнут повторяющиеся перестановки. Поэтому нужно ограничить выбор равных элементов так, чтобы в каждом раунде каждый из них выбирался только один раз. Обычно это реализуется с помощью хеш-множества.
        • Цель задачи о сумме подмножеств - найти все подмножества данного множества, сумма которых равна целевому значению. В множестве порядок элементов не важен, однако процесс поиска порождает результаты во всех возможных порядках, из-за чего появляются повторяющиеся подмножества. Поэтому перед запуском поиска с возвратом мы сортируем данные и вводим переменную, указывающую начальную точку обхода в каждом раунде, чтобы отсечь ветви, создающие дубликаты.
        • В задаче о сумме подмножеств равные элементы массива также порождают повторяющиеся множества. При наличии предварительной сортировки их можно отсекать, проверяя равенство соседних элементов, и тем самым гарантировать, что в каждом раунде равные элементы будут выбираться только один раз.
        • Задача о \\(n\\) ферзях состоит в поиске способов разместить \\(n\\) ферзей на доске размера \\(n \\times n\\) так, чтобы никакие два ферзя не атаковали друг друга. Ограничения этой задачи включают строки, столбцы, главные диагонали и побочные диагонали. Чтобы выполнить ограничение по строкам, используется построчная стратегия размещения, гарантирующая по одному ферзю в каждой строке.
        • Обработка ограничений по столбцам и диагоналям устроена похожим образом. Для ограничения по столбцам используется массив, фиксирующий наличие ферзя в каждом столбце. Для диагоналей используются два массива, записывающие наличие ферзей на главных и побочных диагоналях. Основная сложность здесь состоит в том, чтобы найти закономерность индексов строк и столбцов клеток, лежащих на одной и той же главной или побочной диагонали.
        ","path":["Глава 13. Поиск с возвратом","13.5   Резюме"],"tags":[]},{"location":"chapter_backtracking/summary/#2","level":3,"title":"2.   Вопросы и ответы","text":"

        Q: Как понять связь между поиском с возвратом и рекурсией?

        В целом поиск с возвратом - это скорее «алгоритмическая стратегия», а рекурсия больше похожа на «инструмент».

        • Алгоритмы поиска с возвратом обычно реализуются на основе рекурсии. Однако поиск с возвратом - это лишь один из вариантов применения рекурсии, а именно ее использование в поисковых задачах.
        • Структура рекурсии отражает парадигму разбиения на подзадачи и часто применяется для решения задач «разделяй и властвуй», поиска с возвратом, динамического программирования (мемоизированной рекурсии) и других подобных задач.
        ","path":["Глава 13. Поиск с возвратом","13.5   Резюме"],"tags":[]},{"location":"chapter_computational_complexity/","level":1,"title":"Глава 2.   Анализ сложности","text":"

        Abstract

        Анализ сложности подобен пространственно-временному проводнику в необъятной вселенной алгоритмов.

        Он ведет нас в глубину двух измерений - времени и пространства, помогая искать более изящные решения.

        ","path":["Глава 2. Анализ сложности","Глава 2.   Анализ сложности"],"tags":[]},{"location":"chapter_computational_complexity/#_1","level":2,"title":"Содержание главы","text":"
        • 2.1   Оценка эффективности алгоритмов
        • 2.2   Итерация и рекурсия
        • 2.3   Временная сложность
        • 2.4   Пространственная сложность
        • 2.5   Резюме
        ","path":["Глава 2. Анализ сложности","Глава 2.   Анализ сложности"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/","level":1,"title":"2.2   Итерация и рекурсия","text":"

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

        ","path":["Глава 2. Анализ сложности","2.2   Итерация и рекурсия"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#221","level":2,"title":"2.2.1   Итерация","text":"

        Итерация (iteration) - это структура управления, которая позволяет повторно выполнять определенную задачу. В итерации программа повторяет выполнение определенного участка кода, пока выполняется определенное условие.

        ","path":["Глава 2. Анализ сложности","2.2   Итерация и рекурсия"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#1-for","level":3,"title":"1.   Цикл for","text":"

        Цикл for - одна из наиболее распространенных форм итерации, которая подходит для использования, когда количество итераций известно заранее.

        Следующая функция реализует суммирование \\(1 + 2 + \\dots + n\\) с использованием цикла for , а результат суммирования сохраняется в переменной res . Следует отметить, что в Python диапазон range(a, b) соответствует левому закрытому, правому открытому интервалу, то есть перебираются значения \\(a, a + 1, \\dots, b-1\\) :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py
        def for_loop(n: int) -> int:\n    \"\"\"Цикл for\"\"\"\n    res = 0\n    # Циклическое суммирование 1, 2, ..., n-1, n\n    for i in range(1, n + 1):\n        res += i\n    return res\n
        iteration.cpp
        /* Цикл for */\nint forLoop(int n) {\n    int res = 0;\n    // Циклическое суммирование 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; ++i) {\n        res += i;\n    }\n    return res;\n}\n
        iteration.java
        /* Цикл for */\nint forLoop(int n) {\n    int res = 0;\n    // Циклическое суммирование 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; i++) {\n        res += i;\n    }\n    return res;\n}\n
        iteration.cs
        /* Цикл for */\nint ForLoop(int n) {\n    int res = 0;\n    // Циклическое суммирование 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; i++) {\n        res += i;\n    }\n    return res;\n}\n
        iteration.go
        /* Цикл for */\nfunc forLoop(n int) int {\n    res := 0\n    // Циклическое суммирование 1, 2, ..., n-1, n\n    for i := 1; i <= n; i++ {\n        res += i\n    }\n    return res\n}\n
        iteration.swift
        /* Цикл for */\nfunc forLoop(n: Int) -> Int {\n    var res = 0\n    // Циклическое суммирование 1, 2, ..., n-1, n\n    for i in 1 ... n {\n        res += i\n    }\n    return res\n}\n
        iteration.js
        /* Цикл for */\nfunction forLoop(n) {\n    let res = 0;\n    // Циклическое суммирование 1, 2, ..., n-1, n\n    for (let i = 1; i <= n; i++) {\n        res += i;\n    }\n    return res;\n}\n
        iteration.ts
        /* Цикл for */\nfunction forLoop(n: number): number {\n    let res = 0;\n    // Циклическое суммирование 1, 2, ..., n-1, n\n    for (let i = 1; i <= n; i++) {\n        res += i;\n    }\n    return res;\n}\n
        iteration.dart
        /* Цикл for */\nint forLoop(int n) {\n  int res = 0;\n  // Циклическое суммирование 1, 2, ..., n-1, n\n  for (int i = 1; i <= n; i++) {\n    res += i;\n  }\n  return res;\n}\n
        iteration.rs
        /* Цикл for */\nfn for_loop(n: i32) -> i32 {\n    let mut res = 0;\n    // Циклическое суммирование 1, 2, ..., n-1, n\n    for i in 1..=n {\n        res += i;\n    }\n    res\n}\n
        iteration.c
        /* Цикл for */\nint forLoop(int n) {\n    int res = 0;\n    // Циклическое суммирование 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; i++) {\n        res += i;\n    }\n    return res;\n}\n
        iteration.kt
        /* Цикл for */\nfun forLoop(n: Int): Int {\n    var res = 0\n    // Циклическое суммирование 1, 2, ..., n-1, n\n    for (i in 1..n) {\n        res += i\n    }\n    return res\n}\n
        iteration.rb
        ### Цикл for ###\ndef for_loop(n)\n  res = 0\n\n  # Циклическое суммирование 1, 2, ..., n-1, n\n  for i in 1..n\n    res += i\n  end\n\n  res\nend\n
        Визуализация кода

        Во весь экран >

        На рисунке 2-1 представлена блок-схема этой функции суммирования.

        Рисунок 2-1   Блок-схема функции суммирования

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

        ","path":["Глава 2. Анализ сложности","2.2   Итерация и рекурсия"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#2-while","level":3,"title":"2.   Цикл while","text":"

        Подобно циклу for , цикл while также представляет собой метод реализации итерации. В цикле while программа перед каждой итерацией проверяет условие: если условие истинно, то выполнение продолжается, иначе цикл завершается.

        Ниже приведен пример реализации суммирования \\(1 + 2 + \\dots + n\\) с использованием цикла while :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py
        def while_loop(n: int) -> int:\n    \"\"\"Цикл while\"\"\"\n    res = 0\n    i = 1  # Инициализация условной переменной\n    # Циклическое суммирование 1, 2, ..., n-1, n\n    while i <= n:\n        res += i\n        i += 1  # Обновить условную переменную\n    return res\n
        iteration.cpp
        /* Цикл while */\nint whileLoop(int n) {\n    int res = 0;\n    int i = 1; // Инициализация условной переменной\n    // Циклическое суммирование 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i++; // Обновить условную переменную\n    }\n    return res;\n}\n
        iteration.java
        /* Цикл while */\nint whileLoop(int n) {\n    int res = 0;\n    int i = 1; // Инициализация условной переменной\n    // Циклическое суммирование 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i++; // Обновить условную переменную\n    }\n    return res;\n}\n
        iteration.cs
        /* Цикл while */\nint WhileLoop(int n) {\n    int res = 0;\n    int i = 1; // Инициализация условной переменной\n    // Циклическое суммирование 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i += 1; // Обновить условную переменную\n    }\n    return res;\n}\n
        iteration.go
        /* Цикл while */\nfunc whileLoop(n int) int {\n    res := 0\n    // Инициализация условной переменной\n    i := 1\n    // Циклическое суммирование 1, 2, ..., n-1, n\n    for i <= n {\n        res += i\n        // Обновить условную переменную\n        i++\n    }\n    return res\n}\n
        iteration.swift
        /* Цикл while */\nfunc whileLoop(n: Int) -> Int {\n    var res = 0\n    var i = 1 // Инициализация условной переменной\n    // Циклическое суммирование 1, 2, ..., n-1, n\n    while i <= n {\n        res += i\n        i += 1 // Обновить условную переменную\n    }\n    return res\n}\n
        iteration.js
        /* Цикл while */\nfunction whileLoop(n) {\n    let res = 0;\n    let i = 1; // Инициализация условной переменной\n    // Циклическое суммирование 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i++; // Обновить условную переменную\n    }\n    return res;\n}\n
        iteration.ts
        /* Цикл while */\nfunction whileLoop(n: number): number {\n    let res = 0;\n    let i = 1; // Инициализация условной переменной\n    // Циклическое суммирование 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i++; // Обновить условную переменную\n    }\n    return res;\n}\n
        iteration.dart
        /* Цикл while */\nint whileLoop(int n) {\n  int res = 0;\n  int i = 1; // Инициализация условной переменной\n  // Циклическое суммирование 1, 2, ..., n-1, n\n  while (i <= n) {\n    res += i;\n    i++; // Обновить условную переменную\n  }\n  return res;\n}\n
        iteration.rs
        /* Цикл while */\nfn while_loop(n: i32) -> i32 {\n    let mut res = 0;\n    let mut i = 1; // Инициализация условной переменной\n\n    // Циклическое суммирование 1, 2, ..., n-1, n\n    while i <= n {\n        res += i;\n        i += 1; // Обновить условную переменную\n    }\n    res\n}\n
        iteration.c
        /* Цикл while */\nint whileLoop(int n) {\n    int res = 0;\n    int i = 1; // Инициализация условной переменной\n    // Циклическое суммирование 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i++; // Обновить условную переменную\n    }\n    return res;\n}\n
        iteration.kt
        /* Цикл while */\nfun whileLoop(n: Int): Int {\n    var res = 0\n    var i = 1 // Инициализация условной переменной\n    // Циклическое суммирование 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i\n        i++ // Обновить условную переменную\n    }\n    return res\n}\n
        iteration.rb
        ### Цикл while ###\ndef while_loop(n)\n  res = 0\n  i = 1 # Инициализация условной переменной\n\n  # Циклическое суммирование 1, 2, ..., n-1, n\n  while i <= n\n    res += i\n    i += 1 # Обновить условную переменную\n  end\n\n  res\nend\n
        Визуализация кода

        Во весь экран >

        **Цикл while обладает большей степенью свободы по сравнению с циклом for **. В цикле while можно свободно управлять инициализацией и обновлением условной переменной.

        Например, в следующем коде условная переменная \\(i\\) обновляется дважды на каждой итерации, что затруднительно сделать с использованием цикла for :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py
        def while_loop_ii(n: int) -> int:\n    \"\"\"Цикл while (двойное обновление)\"\"\"\n    res = 0\n    i = 1  # Инициализация условной переменной\n    # Циклическое суммирование 1, 4, 10, ...\n    while i <= n:\n        res += i\n        # Обновить условную переменную\n        i += 1\n        i *= 2\n    return res\n
        iteration.cpp
        /* Цикл while (двойное обновление) */\nint whileLoopII(int n) {\n    int res = 0;\n    int i = 1; // Инициализация условной переменной\n    // Циклическое суммирование 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // Обновить условную переменную\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
        iteration.java
        /* Цикл while (двойное обновление) */\nint whileLoopII(int n) {\n    int res = 0;\n    int i = 1; // Инициализация условной переменной\n    // Циклическое суммирование 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // Обновить условную переменную\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
        iteration.cs
        /* Цикл while (двойное обновление) */\nint WhileLoopII(int n) {\n    int res = 0;\n    int i = 1; // Инициализация условной переменной\n    // Циклическое суммирование 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // Обновить условную переменную\n        i += 1; \n        i *= 2;\n    }\n    return res;\n}\n
        iteration.go
        /* Цикл while (двойное обновление) */\nfunc whileLoopII(n int) int {\n    res := 0\n    // Инициализация условной переменной\n    i := 1\n    // Циклическое суммирование 1, 4, 10, ...\n    for i <= n {\n        res += i\n        // Обновить условную переменную\n        i++\n        i *= 2\n    }\n    return res\n}\n
        iteration.swift
        /* Цикл while (двойное обновление) */\nfunc whileLoopII(n: Int) -> Int {\n    var res = 0\n    var i = 1 // Инициализация условной переменной\n    // Циклическое суммирование 1, 4, 10, ...\n    while i <= n {\n        res += i\n        // Обновить условную переменную\n        i += 1\n        i *= 2\n    }\n    return res\n}\n
        iteration.js
        /* Цикл while (двойное обновление) */\nfunction whileLoopII(n) {\n    let res = 0;\n    let i = 1; // Инициализация условной переменной\n    // Циклическое суммирование 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // Обновить условную переменную\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
        iteration.ts
        /* Цикл while (двойное обновление) */\nfunction whileLoopII(n: number): number {\n    let res = 0;\n    let i = 1; // Инициализация условной переменной\n    // Циклическое суммирование 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // Обновить условную переменную\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
        iteration.dart
        /* Цикл while (двойное обновление) */\nint whileLoopII(int n) {\n  int res = 0;\n  int i = 1; // Инициализация условной переменной\n  // Циклическое суммирование 1, 4, 10, ...\n  while (i <= n) {\n    res += i;\n    // Обновить условную переменную\n    i++;\n    i *= 2;\n  }\n  return res;\n}\n
        iteration.rs
        /* Цикл while (двойное обновление) */\nfn while_loop_ii(n: i32) -> i32 {\n    let mut res = 0;\n    let mut i = 1; // Инициализация условной переменной\n\n    // Циклическое суммирование 1, 4, 10, ...\n    while i <= n {\n        res += i;\n        // Обновить условную переменную\n        i += 1;\n        i *= 2;\n    }\n    res\n}\n
        iteration.c
        /* Цикл while (двойное обновление) */\nint whileLoopII(int n) {\n    int res = 0;\n    int i = 1; // Инициализация условной переменной\n    // Циклическое суммирование 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // Обновить условную переменную\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
        iteration.kt
        /* Цикл while (двойное обновление) */\nfun whileLoopII(n: Int): Int {\n    var res = 0\n    var i = 1 // Инициализация условной переменной\n    // Циклическое суммирование 1, 4, 10, ...\n    while (i <= n) {\n        res += i\n        // Обновить условную переменную\n        i++\n        i *= 2\n    }\n    return res\n}\n
        iteration.rb
        ### Цикл while ###\ndef while_loop(n)\n  res = 0\n  i = 1 # Инициализация условной переменной\n\n  # Циклическое суммирование 1, 2, ..., n-1, n\n  while i <= n\n    res += i\n    i += 1 # Обновить условную переменную\n  end\n\n  res\nend\n\n# ## Цикл while (двойное обновление) ###\ndef while_loop_ii(n)\n  res = 0\n  i = 1 # Инициализация условной переменной\n\n  # Циклическое суммирование 1, 4, 10, ...\n  while i <= n\n    res += i\n    # Обновить условную переменную\n    i += 1\n    i *= 2\n  end\n\n  res\nend\n
        Визуализация кода

        Во весь экран >

        В целом код с использованием цикла for более компактный, а цикл while более гибкий. Но они оба могут реализовать итерационную структуру. Выбор между ними определяется требованиями конкретной задачи.

        ","path":["Глава 2. Анализ сложности","2.2   Итерация и рекурсия"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#3","level":3,"title":"3.   Вложенные циклы","text":"

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

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py
        def nested_for_loop(n: int) -> str:\n    \"\"\"Двойной цикл for\"\"\"\n    res = \"\"\n    # Цикл по i = 1, 2, ..., n-1, n\n    for i in range(1, n + 1):\n        # Цикл по j = 1, 2, ..., n-1, n\n        for j in range(1, n + 1):\n            res += f\"({i}, {j}), \"\n    return res\n
        iteration.cpp
        /* Двойной цикл for */\nstring nestedForLoop(int n) {\n    ostringstream res;\n    // Цикл по i = 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; ++i) {\n        // Цикл по j = 1, 2, ..., n-1, n\n        for (int j = 1; j <= n; ++j) {\n            res << \"(\" << i << \", \" << j << \"), \";\n        }\n    }\n    return res.str();\n}\n
        iteration.java
        /* Двойной цикл for */\nString nestedForLoop(int n) {\n    StringBuilder res = new StringBuilder();\n    // Цикл по i = 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; i++) {\n        // Цикл по j = 1, 2, ..., n-1, n\n        for (int j = 1; j <= n; j++) {\n            res.append(\"(\" + i + \", \" + j + \"), \");\n        }\n    }\n    return res.toString();\n}\n
        iteration.cs
        /* Двойной цикл for */\nstring NestedForLoop(int n) {\n    StringBuilder res = new();\n    // Цикл по i = 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; i++) {\n        // Цикл по j = 1, 2, ..., n-1, n\n        for (int j = 1; j <= n; j++) {\n            res.Append($\"({i}, {j}), \");\n        }\n    }\n    return res.ToString();\n}\n
        iteration.go
        /* Двойной цикл for */\nfunc nestedForLoop(n int) string {\n    res := \"\"\n    // Цикл по i = 1, 2, ..., n-1, n\n    for i := 1; i <= n; i++ {\n        for j := 1; j <= n; j++ {\n            // Цикл по j = 1, 2, ..., n-1, n\n            res += fmt.Sprintf(\"(%d, %d), \", i, j)\n        }\n    }\n    return res\n}\n
        iteration.swift
        /* Двойной цикл for */\nfunc nestedForLoop(n: Int) -> String {\n    var res = \"\"\n    // Цикл по i = 1, 2, ..., n-1, n\n    for i in 1 ... n {\n        // Цикл по j = 1, 2, ..., n-1, n\n        for j in 1 ... n {\n            res.append(\"(\\(i), \\(j)), \")\n        }\n    }\n    return res\n}\n
        iteration.js
        /* Двойной цикл for */\nfunction nestedForLoop(n) {\n    let res = '';\n    // Цикл по i = 1, 2, ..., n-1, n\n    for (let i = 1; i <= n; i++) {\n        // Цикл по j = 1, 2, ..., n-1, n\n        for (let j = 1; j <= n; j++) {\n            res += `(${i}, ${j}), `;\n        }\n    }\n    return res;\n}\n
        iteration.ts
        /* Двойной цикл for */\nfunction nestedForLoop(n: number): string {\n    let res = '';\n    // Цикл по i = 1, 2, ..., n-1, n\n    for (let i = 1; i <= n; i++) {\n        // Цикл по j = 1, 2, ..., n-1, n\n        for (let j = 1; j <= n; j++) {\n            res += `(${i}, ${j}), `;\n        }\n    }\n    return res;\n}\n
        iteration.dart
        /* Двойной цикл for */\nString nestedForLoop(int n) {\n  String res = \"\";\n  // Цикл по i = 1, 2, ..., n-1, n\n  for (int i = 1; i <= n; i++) {\n    // Цикл по j = 1, 2, ..., n-1, n\n    for (int j = 1; j <= n; j++) {\n      res += \"($i, $j), \";\n    }\n  }\n  return res;\n}\n
        iteration.rs
        /* Двойной цикл for */\nfn nested_for_loop(n: i32) -> String {\n    let mut res = vec![];\n    // Цикл по i = 1, 2, ..., n-1, n\n    for i in 1..=n {\n        // Цикл по j = 1, 2, ..., n-1, n\n        for j in 1..=n {\n            res.push(format!(\"({}, {}), \", i, j));\n        }\n    }\n    res.join(\"\")\n}\n
        iteration.c
        /* Двойной цикл for */\nchar *nestedForLoop(int n) {\n    // n * n — это число соответствующих точек, а максимальная длина строки \"(i, j), \" равна 6+10*2, плюс дополнительное место для завершающего нулевого символа \\0\n    int size = n * n * 26 + 1;\n    char *res = malloc(size * sizeof(char));\n    // Цикл по i = 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; i++) {\n        // Цикл по j = 1, 2, ..., n-1, n\n        for (int j = 1; j <= n; j++) {\n            char tmp[26];\n            snprintf(tmp, sizeof(tmp), \"(%d, %d), \", i, j);\n            strncat(res, tmp, size - strlen(res) - 1);\n        }\n    }\n    return res;\n}\n
        iteration.kt
        /* Двойной цикл for */\nfun nestedForLoop(n: Int): String {\n    val res = StringBuilder()\n    // Цикл по i = 1, 2, ..., n-1, n\n    for (i in 1..n) {\n        // Цикл по j = 1, 2, ..., n-1, n\n        for (j in 1..n) {\n            res.append(\" ($i, $j), \")\n        }\n    }\n    return res.toString()\n}\n
        iteration.rb
        ### Двойной цикл for ###\ndef nested_for_loop(n)\n  res = \"\"\n\n  # Цикл по i = 1, 2, ..., n-1, n\n  for i in 1..n\n    # Цикл по j = 1, 2, ..., n-1, n\n    for j in 1..n\n      res += \"(#{i}, #{j}), \"\n    end\n  end\n\n  res\nend\n
        Визуализация кода

        Во весь экран >

        На рисунке 2-2 приведена блок-схема такого вложенного цикла.

        Рисунок 2-2   Блок-схема вложенного цикла

        В этом случае количество выполненных действий пропорционально \\(n^2\\) , или, другими словами, время выполнения алгоритма и размер входных данных \\(n\\) находятся в квадратичной зависимости.

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

        ","path":["Глава 2. Анализ сложности","2.2   Итерация и рекурсия"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#222","level":2,"title":"2.2.2   Рекурсия","text":"

        Рекурсия (recursion) - это стратегия алгоритма, при которой функция вызывает саму себя для решения задачи. Она включает два основных этапа.

        1. Вызов: программа постоянно вызывает саму себя, обычно передавая меньшие или более упрощенные параметры, пока не будет достигнуто условие завершения.
        2. Возврат: после срабатывания условия завершения программа начинает возвращаться из самой глубокой рекурсивной функции, объединяя результаты каждого уровня.

        С точки зрения реализации рекурсивный код включает три основных элемента.

        1. Условие завершения: используется для определения момента перехода от вызова к возврату.
        2. Рекурсивный вызов: соответствует вызову, функция вызывает саму себя, обычно с меньшими или упрощенными параметрами.
        3. Возврат результата: соответствует возврату, возвращает результат текущего уровня рекурсии на предыдущий уровень.

        Рассмотрим следующий код: вызов функции recur(n) позволяет вычислить сумму \\(1 + 2 + \\dots + n\\) :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py
        def recur(n: int) -> int:\n    \"\"\"Рекурсия\"\"\"\n    # Условие завершения\n    if n == 1:\n        return 1\n    # Рекурсия: рекурсивный вызов\n    res = recur(n - 1)\n    # Возврат: вернуть результат\n    return n + res\n
        recursion.cpp
        /* Рекурсия */\nint recur(int n) {\n    // Условие завершения\n    if (n == 1)\n        return 1;\n    // Рекурсия: рекурсивный вызов\n    int res = recur(n - 1);\n    // Возврат: вернуть результат\n    return n + res;\n}\n
        recursion.java
        /* Рекурсия */\nint recur(int n) {\n    // Условие завершения\n    if (n == 1)\n        return 1;\n    // Рекурсия: рекурсивный вызов\n    int res = recur(n - 1);\n    // Возврат: вернуть результат\n    return n + res;\n}\n
        recursion.cs
        /* Рекурсия */\nint Recur(int n) {\n    // Условие завершения\n    if (n == 1)\n        return 1;\n    // Рекурсия: рекурсивный вызов\n    int res = Recur(n - 1);\n    // Возврат: вернуть результат\n    return n + res;\n}\n
        recursion.go
        /* Рекурсия */\nfunc recur(n int) int {\n    // Условие завершения\n    if n == 1 {\n        return 1\n    }\n    // Рекурсия: рекурсивный вызов\n    res := recur(n - 1)\n    // Возврат: вернуть результат\n    return n + res\n}\n
        recursion.swift
        /* Рекурсия */\nfunc recur(n: Int) -> Int {\n    // Условие завершения\n    if n == 1 {\n        return 1\n    }\n    // Рекурсия: рекурсивный вызов\n    let res = recur(n: n - 1)\n    // Возврат: вернуть результат\n    return n + res\n}\n
        recursion.js
        /* Рекурсия */\nfunction recur(n) {\n    // Условие завершения\n    if (n === 1) return 1;\n    // Рекурсия: рекурсивный вызов\n    const res = recur(n - 1);\n    // Возврат: вернуть результат\n    return n + res;\n}\n
        recursion.ts
        /* Рекурсия */\nfunction recur(n: number): number {\n    // Условие завершения\n    if (n === 1) return 1;\n    // Рекурсия: рекурсивный вызов\n    const res = recur(n - 1);\n    // Возврат: вернуть результат\n    return n + res;\n}\n
        recursion.dart
        /* Рекурсия */\nint recur(int n) {\n  // Условие завершения\n  if (n == 1) return 1;\n  // Рекурсия: рекурсивный вызов\n  int res = recur(n - 1);\n  // Возврат: вернуть результат\n  return n + res;\n}\n
        recursion.rs
        /* Рекурсия */\nfn recur(n: i32) -> i32 {\n    // Условие завершения\n    if n == 1 {\n        return 1;\n    }\n    // Рекурсия: рекурсивный вызов\n    let res = recur(n - 1);\n    // Возврат: вернуть результат\n    n + res\n}\n
        recursion.c
        /* Рекурсия */\nint recur(int n) {\n    // Условие завершения\n    if (n == 1)\n        return 1;\n    // Рекурсия: рекурсивный вызов\n    int res = recur(n - 1);\n    // Возврат: вернуть результат\n    return n + res;\n}\n
        recursion.kt
        /* Рекурсия */\nfun recur(n: Int): Int {\n    // Условие завершения\n    if (n == 1)\n        return 1\n    // Рекурсивный шаг: рекурсивный вызов\n    val res = recur(n - 1)\n    // Возврат: вернуть результат\n    return n + res\n}\n
        recursion.rb
        ### Рекурсия ###\ndef recur(n)\n  # Условие завершения\n  return 1 if n == 1\n  # Рекурсия: рекурсивный вызов\n  res = recur(n - 1)\n  # Возврат: вернуть результат\n  n + res\nend\n
        Визуализация кода

        Во весь экран >

        На рисунке 2-3 представлен рекурсивный процесс этой функции.

        Рисунок 2-3   Рекурсивный процесс функции суммирования

        Хотя с точки зрения вычислений итерация и рекурсия могут давать одинаковый результат, они представляют собой совершенно разные парадигмы мышления и решения задач.

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

        Рассмотрим в качестве примера вышеупомянутую функцию суммирования, где решается задача \\(f(n) = 1 + 2 + \\dots + n\\) .

        • Итерация: моделирование процесса суммирования в цикле проходит от \\(1\\) до \\(n\\) , выполняя операцию суммирования на каждом шаге, чтобы получить итоговое значение \\(f(n)\\) .
        • Рекурсия: последовательное разбиение задачи на подзадачи вида \\(f(n) = n + f(n - 1)\\) до достижения базового случая \\(f(1) = 1\\) .
        ","path":["Глава 2. Анализ сложности","2.2   Итерация и рекурсия"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#1","level":3,"title":"1.   Стек вызовов","text":"

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

        • Контекстные данные функции хранятся в области памяти, называемой пространством стекового кадра, и освобождаются только после возврата функции. Поэтому рекурсия обычно требует больше памяти, чем итерация.
        • Рекурсивный вызов функции создает дополнительные накладные расходы. Поэтому рекурсия обычно менее эффективна по времени, чем цикл.

        Как показано на рисунке 2-4, до срабатывания условия завершения одновременно существует \\(n\\) невозвращенных рекурсивных функций, а глубина рекурсии равна \\(n\\) .

        Рисунок 2-4   Глубина рекурсивного вызова

        На практике глубина рекурсии, разрешенная языком программирования, обычно ограничена, и слишком глубокая рекурсия может привести к ошибке переполнения стека.

        ","path":["Глава 2. Анализ сложности","2.2   Итерация и рекурсия"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#2","level":3,"title":"2.   Хвостовая рекурсия","text":"

        Интересно, что если рекурсивный вызов происходит на последнем шаге перед возвратом функции , то компилятор или интерпретатор может оптимизировать этот вызов, сделав его по эффективности использования памяти сопоставимым с итерацией. Это называется хвостовой рекурсией (tail recursion).

        • Обычная рекурсия: когда функция возвращается на предыдущий уровень, необходимо продолжить выполнение кода, поэтому системе нужно сохранить контекст предыдущего вызова.
        • Хвостовая рекурсия: рекурсивный вызов является последней операцией перед возвратом функции, что означает, что после возврата на предыдущий уровень не требуется выполнять другие операции, поэтому системе не нужно сохранять контекст предыдущей функции.

        В качестве примера вычисления суммы \\(1 + 2 + \\dots + n\\) можно установить переменную результата res в качестве параметра функции, чтобы реализовать хвостовую рекурсию:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py
        def tail_recur(n, res):\n    \"\"\"Хвостовая рекурсия\"\"\"\n    # Условие завершения\n    if n == 0:\n        return res\n    # Хвостовой рекурсивный вызов\n    return tail_recur(n - 1, res + n)\n
        recursion.cpp
        /* Хвостовая рекурсия */\nint tailRecur(int n, int res) {\n    // Условие завершения\n    if (n == 0)\n        return res;\n    // Хвостовой рекурсивный вызов\n    return tailRecur(n - 1, res + n);\n}\n
        recursion.java
        /* Хвостовая рекурсия */\nint tailRecur(int n, int res) {\n    // Условие завершения\n    if (n == 0)\n        return res;\n    // Хвостовой рекурсивный вызов\n    return tailRecur(n - 1, res + n);\n}\n
        recursion.cs
        /* Хвостовая рекурсия */\nint TailRecur(int n, int res) {\n    // Условие завершения\n    if (n == 0)\n        return res;\n    // Хвостовой рекурсивный вызов\n    return TailRecur(n - 1, res + n);\n}\n
        recursion.go
        /* Хвостовая рекурсия */\nfunc tailRecur(n int, res int) int {\n    // Условие завершения\n    if n == 0 {\n        return res\n    }\n    // Хвостовой рекурсивный вызов\n    return tailRecur(n-1, res+n)\n}\n
        recursion.swift
        /* Хвостовая рекурсия */\nfunc tailRecur(n: Int, res: Int) -> Int {\n    // Условие завершения\n    if n == 0 {\n        return res\n    }\n    // Хвостовой рекурсивный вызов\n    return tailRecur(n: n - 1, res: res + n)\n}\n
        recursion.js
        /* Хвостовая рекурсия */\nfunction tailRecur(n, res) {\n    // Условие завершения\n    if (n === 0) return res;\n    // Хвостовой рекурсивный вызов\n    return tailRecur(n - 1, res + n);\n}\n
        recursion.ts
        /* Хвостовая рекурсия */\nfunction tailRecur(n: number, res: number): number {\n    // Условие завершения\n    if (n === 0) return res;\n    // Хвостовой рекурсивный вызов\n    return tailRecur(n - 1, res + n);\n}\n
        recursion.dart
        /* Хвостовая рекурсия */\nint tailRecur(int n, int res) {\n  // Условие завершения\n  if (n == 0) return res;\n  // Хвостовой рекурсивный вызов\n  return tailRecur(n - 1, res + n);\n}\n
        recursion.rs
        /* Хвостовая рекурсия */\nfn tail_recur(n: i32, res: i32) -> i32 {\n    // Условие завершения\n    if n == 0 {\n        return res;\n    }\n    // Хвостовой рекурсивный вызов\n    tail_recur(n - 1, res + n)\n}\n
        recursion.c
        /* Хвостовая рекурсия */\nint tailRecur(int n, int res) {\n    // Условие завершения\n    if (n == 0)\n        return res;\n    // Хвостовой рекурсивный вызов\n    return tailRecur(n - 1, res + n);\n}\n
        recursion.kt
        /* Хвостовая рекурсия */\ntailrec fun tailRecur(n: Int, res: Int): Int {\n    // Добавить ключевое слово tailrec, чтобы включить оптимизацию хвостовой рекурсии\n    // Условие завершения\n    if (n == 0)\n        return res\n    // Хвостовой рекурсивный вызов\n    return tailRecur(n - 1, res + n)\n}\n
        recursion.rb
        ### Хвостовая рекурсия ###\ndef tail_recur(n, res)\n  # Условие завершения\n  return res if n == 0\n  # Хвостовой рекурсивный вызов\n  tail_recur(n - 1, res + n)\nend\n
        Визуализация кода

        Во весь экран >

        Процесс выполнения хвостовой рекурсии показан на рисунке 2-5. Сравнивая обычную и хвостовую рекурсии, можно заметить, что точка выполнения операции суммирования у них различается.

        • Обычная рекурсия: операция суммирования выполняется в процессе возврата, после каждого возврата необходимо снова выполнить операцию суммирования.
        • Хвостовая рекурсия: операция суммирования выполняется в процессе вызова, а процесс возврата требует только последовательного возврата.

        Рисунок 2-5   Процесс хвостовой рекурсии

        Tip

        Обратите внимание: многие компиляторы и интерпретаторы не поддерживают оптимизацию хвостовой рекурсии. Например, Python по умолчанию такую оптимизацию не выполняет, поэтому даже функция в хвостово-рекурсивной форме все равно может привести к переполнению стека.

        ","path":["Глава 2. Анализ сложности","2.2   Итерация и рекурсия"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#3_1","level":3,"title":"3.   Дерево рекурсии","text":"

        При решении задач, связанных с алгоритмами типа «разделяй и властвуй», рекурсия часто оказывается более интуитивной и читабельной, чем итерация. Рассмотрим в качестве примера последовательность Фибоначчи.

        Question

        Дана последовательность Фибоначчи \\(0, 1, 1, 2, 3, 5, 8, 13, \\dots\\). Найди \\(n\\)-й элемент этой последовательности.

        Обозначив \\(n\\)-й член последовательности Фибоначчи как \\(f(n)\\) , можно сформулировать два утверждения.

        • Первые два числа последовательности: \\(f(1) = 0\\) и \\(f(2) = 1\\) .
        • Каждое число последовательности является суммой двух предыдущих чисел, то есть \\(f(n) = f(n - 1) + f(n - 2)\\) .

        Используя рекурсивные вызовы в соответствии с рекуррентным соотношением и принимая первые два числа за условия остановки, можно написать рекурсивный код. Вызов fib(n) позволит получить \\(n\\)-й член последовательности Фибоначчи:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py
        def fib(n: int) -> int:\n    \"\"\"Последовательность Фибоначчи: рекурсия\"\"\"\n    # Условие завершения: f(1) = 0, f(2) = 1\n    if n == 1 or n == 2:\n        return n - 1\n    # Рекурсивный вызов f(n) = f(n-1) + f(n-2)\n    res = fib(n - 1) + fib(n - 2)\n    # Вернуть результат f(n)\n    return res\n
        recursion.cpp
        /* Последовательность Фибоначчи: рекурсия */\nint fib(int n) {\n    // Условие завершения: f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1;\n    // Рекурсивный вызов f(n) = f(n-1) + f(n-2)\n    int res = fib(n - 1) + fib(n - 2);\n    // Вернуть результат f(n)\n    return res;\n}\n
        recursion.java
        /* Последовательность Фибоначчи: рекурсия */\nint fib(int n) {\n    // Условие завершения: f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1;\n    // Рекурсивный вызов f(n) = f(n-1) + f(n-2)\n    int res = fib(n - 1) + fib(n - 2);\n    // Вернуть результат f(n)\n    return res;\n}\n
        recursion.cs
        /* Последовательность Фибоначчи: рекурсия */\nint Fib(int n) {\n    // Условие завершения: f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1;\n    // Рекурсивный вызов f(n) = f(n-1) + f(n-2)\n    int res = Fib(n - 1) + Fib(n - 2);\n    // Вернуть результат f(n)\n    return res;\n}\n
        recursion.go
        /* Последовательность Фибоначчи: рекурсия */\nfunc fib(n int) int {\n    // Условие завершения: f(1) = 0, f(2) = 1\n    if n == 1 || n == 2 {\n        return n - 1\n    }\n    // Рекурсивный вызов f(n) = f(n-1) + f(n-2)\n    res := fib(n-1) + fib(n-2)\n    // Вернуть результат f(n)\n    return res\n}\n
        recursion.swift
        /* Последовательность Фибоначчи: рекурсия */\nfunc fib(n: Int) -> Int {\n    // Условие завершения: f(1) = 0, f(2) = 1\n    if n == 1 || n == 2 {\n        return n - 1\n    }\n    // Рекурсивный вызов f(n) = f(n-1) + f(n-2)\n    let res = fib(n: n - 1) + fib(n: n - 2)\n    // Вернуть результат f(n)\n    return res\n}\n
        recursion.js
        /* Последовательность Фибоначчи: рекурсия */\nfunction fib(n) {\n    // Условие завершения: f(1) = 0, f(2) = 1\n    if (n === 1 || n === 2) return n - 1;\n    // Рекурсивный вызов f(n) = f(n-1) + f(n-2)\n    const res = fib(n - 1) + fib(n - 2);\n    // Вернуть результат f(n)\n    return res;\n}\n
        recursion.ts
        /* Последовательность Фибоначчи: рекурсия */\nfunction fib(n: number): number {\n    // Условие завершения: f(1) = 0, f(2) = 1\n    if (n === 1 || n === 2) return n - 1;\n    // Рекурсивный вызов f(n) = f(n-1) + f(n-2)\n    const res = fib(n - 1) + fib(n - 2);\n    // Вернуть результат f(n)\n    return res;\n}\n
        recursion.dart
        /* Последовательность Фибоначчи: рекурсия */\nint fib(int n) {\n  // Условие завершения: f(1) = 0, f(2) = 1\n  if (n == 1 || n == 2) return n - 1;\n  // Рекурсивный вызов f(n) = f(n-1) + f(n-2)\n  int res = fib(n - 1) + fib(n - 2);\n  // Вернуть результат f(n)\n  return res;\n}\n
        recursion.rs
        /* Последовательность Фибоначчи: рекурсия */\nfn fib(n: i32) -> i32 {\n    // Условие завершения: f(1) = 0, f(2) = 1\n    if n == 1 || n == 2 {\n        return n - 1;\n    }\n    // Рекурсивный вызов f(n) = f(n-1) + f(n-2)\n    let res = fib(n - 1) + fib(n - 2);\n    // Вернуть результат\n    res\n}\n
        recursion.c
        /* Последовательность Фибоначчи: рекурсия */\nint fib(int n) {\n    // Условие завершения: f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1;\n    // Рекурсивный вызов f(n) = f(n-1) + f(n-2)\n    int res = fib(n - 1) + fib(n - 2);\n    // Вернуть результат f(n)\n    return res;\n}\n
        recursion.kt
        /* Последовательность Фибоначчи: рекурсия */\nfun fib(n: Int): Int {\n    // Условие завершения: f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1\n    // Рекурсивный вызов f(n) = f(n-1) + f(n-2)\n    val res = fib(n - 1) + fib(n - 2)\n    // Вернуть результат f(n)\n    return res\n}\n
        recursion.rb
        ### Последовательность Фибоначчи: рекурсия ###\ndef fib(n)\n  # Условие завершения: f(1) = 0, f(2) = 1\n  return n - 1 if n == 1 || n == 2\n  # Рекурсивный вызов f(n) = f(n-1) + f(n-2)\n  res = fib(n - 1) + fib(n - 2)\n  # Вернуть результат f(n)\n  res\nend\n
        Визуализация кода

        Во весь экран >

        Проанализировав приведенный код, можно заметить, что внутри функции осуществляется рекурсивный вызов двух функций, то есть из одного вызова образуются два ветвления. Как показано на рисунке 2-6, при последующем выполнении рекурсивных вызовов в итоге образуется дерево рекурсии (recursion tree) глубиной \\(n\\) .

        Рисунок 2-6   Дерево рекурсии последовательности Фибоначчи

        По своей сути рекурсия отражает парадигму мышления «разбиение задачи на более мелкие подзадачи», что делает стратегию «разделяй и властвуй» крайне важной.

        • С точки зрения алгоритмов многие важные алгоритмические стратегии, такие как поиск, сортировка, возврат, «разделяй и властвуй» и динамическое программирование, прямо или косвенно используют этот подход.
        • С точки зрения структур данных рекурсия естественно подходит для решения задач, связанных со списками, деревьями и графами, поскольку они очень хорошо поддаются анализу с использованием идеи «разделяй и властвуй».
        ","path":["Глава 2. Анализ сложности","2.2   Итерация и рекурсия"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#223","level":2,"title":"2.2.3   Сравнение","text":"

        Подводя итог, можно сказать, что итерация и рекурсия различаются по реализации, производительности и применимости, как показано в таблице 2-1.

        Таблица 2-1   Сравнение итерации и рекурсии

        Итерация Рекурсия Способ реализации Циклическая структура Функция вызывает саму себя Временная эффективность Обычно высокая эффективность, нет затрат на вызов функции Каждый вызов функции создает затраты Использование памяти Обычно используется фиксированный объем памяти Накопление вызовов функции может использовать значительное количество пространства стека Сфера использования Подходит для простых циклических задач, код интуитивно понятен и хорошо читаем Подходит для разбиения на подзадачи, для структур деревья и графы, алгоритмов «разделяй и властвуй», возврата и т. д.. Структура кода проста и ясна

        Tip

        Если дальнейшее содержание кажется сложным, можно вернуться к нему после чтения главы о «стеке».

        Какова же внутренняя связь между итерацией и рекурсией? В рассмотренном примере рекурсивной функции операция сложения выполняется на этапе возврата рекурсии. Это означает, что функция, вызванная первой, фактически завершает операцию сложения последней, что соответствует принципу стека «первым пришел - последним вышел».

        На самом деле такие термины рекурсии, как «стек вызовов» и «пространство стекового кадра», уже намекают на тесную связь между рекурсией и стеком.

        1. Вызов: когда вызывается функция, система выделяет для нее новый стековый кадр в «стеке вызовов» для хранения локальных переменных функции, параметров, адреса возврата и других данных.
        2. Возврат: когда функция завершает выполнение и возвращает результат, соответствующий стековый кадр удаляется из «стека вызовов», восстанавливая среду выполнения предыдущей функции.

        Таким образом, можно использовать явный стек для моделирования поведения стека вызовов, чтобы преобразовать рекурсию в итеративную форму:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py
        def for_loop_recur(n: int) -> int:\n    \"\"\"Имитация рекурсии итерацией\"\"\"\n    # Использовать явный стек для имитации системного стека вызовов\n    stack = []\n    res = 0\n    # Рекурсия: рекурсивный вызов\n    for i in range(n, 0, -1):\n        # Имитировать «рекурсию» с помощью операции помещения в стек\n        stack.append(i)\n    # Возврат: вернуть результат\n    while stack:\n        # Имитировать «возврат» с помощью операции извлечения из стека\n        res += stack.pop()\n    # res = 1+2+3+...+n\n    return res\n
        recursion.cpp
        /* Имитация рекурсии итерацией */\nint forLoopRecur(int n) {\n    // Использовать явный стек для имитации системного стека вызовов\n    stack<int> stack;\n    int res = 0;\n    // Рекурсия: рекурсивный вызов\n    for (int i = n; i > 0; i--) {\n        // Имитировать «рекурсию» с помощью операции помещения в стек\n        stack.push(i);\n    }\n    // Возврат: вернуть результат\n    while (!stack.empty()) {\n        // Имитировать «возврат» с помощью операции извлечения из стека\n        res += stack.top();\n        stack.pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
        recursion.java
        /* Имитация рекурсии итерацией */\nint forLoopRecur(int n) {\n    // Использовать явный стек для имитации системного стека вызовов\n    Stack<Integer> stack = new Stack<>();\n    int res = 0;\n    // Рекурсия: рекурсивный вызов\n    for (int i = n; i > 0; i--) {\n        // Имитировать «рекурсию» с помощью операции помещения в стек\n        stack.push(i);\n    }\n    // Возврат: вернуть результат\n    while (!stack.isEmpty()) {\n        // Имитировать «возврат» с помощью операции извлечения из стека\n        res += stack.pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
        recursion.cs
        /* Имитация рекурсии итерацией */\nint ForLoopRecur(int n) {\n    // Использовать явный стек для имитации системного стека вызовов\n    Stack<int> stack = new();\n    int res = 0;\n    // Рекурсия: рекурсивный вызов\n    for (int i = n; i > 0; i--) {\n        // Имитировать «рекурсию» с помощью операции помещения в стек\n        stack.Push(i);\n    }\n    // Возврат: вернуть результат\n    while (stack.Count > 0) {\n        // Имитировать «возврат» с помощью операции извлечения из стека\n        res += stack.Pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
        recursion.go
        /* Имитация рекурсии итерацией */\nfunc forLoopRecur(n int) int {\n    // Использовать явный стек для имитации системного стека вызовов\n    stack := list.New()\n    res := 0\n    // Рекурсия: рекурсивный вызов\n    for i := n; i > 0; i-- {\n        // Имитировать «рекурсию» с помощью операции помещения в стек\n        stack.PushBack(i)\n    }\n    // Возврат: вернуть результат\n    for stack.Len() != 0 {\n        // Имитировать «возврат» с помощью операции извлечения из стека\n        res += stack.Back().Value.(int)\n        stack.Remove(stack.Back())\n    }\n    // res = 1+2+3+...+n\n    return res\n}\n
        recursion.swift
        /* Имитация рекурсии итерацией */\nfunc forLoopRecur(n: Int) -> Int {\n    // Использовать явный стек для имитации системного стека вызовов\n    var stack: [Int] = []\n    var res = 0\n    // Рекурсия: рекурсивный вызов\n    for i in (1 ... n).reversed() {\n        // Имитировать «рекурсию» с помощью операции помещения в стек\n        stack.append(i)\n    }\n    // Возврат: вернуть результат\n    while !stack.isEmpty {\n        // Имитировать «возврат» с помощью операции извлечения из стека\n        res += stack.removeLast()\n    }\n    // res = 1+2+3+...+n\n    return res\n}\n
        recursion.js
        /* Имитация рекурсии итерацией */\nfunction forLoopRecur(n) {\n    // Использовать явный стек для имитации системного стека вызовов\n    const stack = [];\n    let res = 0;\n    // Рекурсия: рекурсивный вызов\n    for (let i = n; i > 0; i--) {\n        // Имитировать «рекурсию» с помощью операции помещения в стек\n        stack.push(i);\n    }\n    // Возврат: вернуть результат\n    while (stack.length) {\n        // Имитировать «возврат» с помощью операции извлечения из стека\n        res += stack.pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
        recursion.ts
        /* Имитация рекурсии итерацией */\nfunction forLoopRecur(n: number): number {\n    // Использовать явный стек для имитации системного стека вызовов\n    const stack: number[] = [];\n    let res: number = 0;\n    // Рекурсия: рекурсивный вызов\n    for (let i = n; i > 0; i--) {\n        // Имитировать «рекурсию» с помощью операции помещения в стек\n        stack.push(i);\n    }\n    // Возврат: вернуть результат\n    while (stack.length) {\n        // Имитировать «возврат» с помощью операции извлечения из стека\n        res += stack.pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
        recursion.dart
        /* Имитация рекурсии итерацией */\nint forLoopRecur(int n) {\n  // Использовать явный стек для имитации системного стека вызовов\n  List<int> stack = [];\n  int res = 0;\n  // Рекурсия: рекурсивный вызов\n  for (int i = n; i > 0; i--) {\n    // Имитировать «рекурсию» с помощью операции помещения в стек\n    stack.add(i);\n  }\n  // Возврат: вернуть результат\n  while (!stack.isEmpty) {\n    // Имитировать «возврат» с помощью операции извлечения из стека\n    res += stack.removeLast();\n  }\n  // res = 1+2+3+...+n\n  return res;\n}\n
        recursion.rs
        /* Имитация рекурсии итерацией */\nfn for_loop_recur(n: i32) -> i32 {\n    // Использовать явный стек для имитации системного стека вызовов\n    let mut stack = Vec::new();\n    let mut res = 0;\n    // Рекурсия: рекурсивный вызов\n    for i in (1..=n).rev() {\n        // Имитировать «рекурсию» с помощью операции помещения в стек\n        stack.push(i);\n    }\n    // Возврат: вернуть результат\n    while !stack.is_empty() {\n        // Имитировать «возврат» с помощью операции извлечения из стека\n        res += stack.pop().unwrap();\n    }\n    // res = 1+2+3+...+n\n    res\n}\n
        recursion.c
        /* Имитация рекурсии итерацией */\nint forLoopRecur(int n) {\n    int stack[1000]; // Использовать большой массив для имитации стека\n    int top = -1;    // Индекс вершины стека\n    int res = 0;\n    // Рекурсия: рекурсивный вызов\n    for (int i = n; i > 0; i--) {\n        // Имитировать «рекурсию» с помощью операции помещения в стек\n        stack[1 + top++] = i;\n    }\n    // Возврат: вернуть результат\n    while (top >= 0) {\n        // Имитировать «возврат» с помощью операции извлечения из стека\n        res += stack[top--];\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
        recursion.kt
        /* Имитация рекурсии итерацией */\nfun forLoopRecur(n: Int): Int {\n    // Использовать явный стек для имитации системного стека вызовов\n    val stack = Stack<Int>()\n    var res = 0\n    // Рекурсивный шаг: рекурсивный вызов\n    for (i in n downTo 0) {\n        // Имитировать «рекурсию» с помощью операции помещения в стек\n        stack.push(i)\n    }\n    // Возврат: вернуть результат\n    while (stack.isNotEmpty()) {\n        // Имитировать «возврат» с помощью операции извлечения из стека\n        res += stack.pop()\n    }\n    // res = 1+2+3+...+n\n    return res\n}\n
        recursion.rb
        ### Имитация рекурсии итерацией ###\ndef for_loop_recur(n)\n  # Использовать явный стек для имитации системного стека вызовов\n  stack = []\n  res = 0\n\n  # Рекурсия: рекурсивный вызов\n  for i in n.downto(0)\n    # Имитировать «рекурсию» с помощью операции помещения в стек\n    stack << i\n  end\n  # Возврат: вернуть результат\n  while !stack.empty?\n    res += stack.pop\n  end\n\n  # res = 1+2+3+...+n\n  res\nend\n
        Визуализация кода

        Во весь экран >

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

        • Преобразованный код может стать труднее для понимания и менее читаемым.
        • Для некоторых сложных задач моделирование поведения системного стека вызовов может оказаться очень трудным.

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

        ","path":["Глава 2. Анализ сложности","2.2   Итерация и рекурсия"],"tags":[]},{"location":"chapter_computational_complexity/performance_evaluation/","level":1,"title":"2.1   Оценка эффективности алгоритмов","text":"

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

        1. Найти решение задачи: алгоритм должен надежно находить правильное решение задачи в заданных пределах входных данных.
        2. Найти оптимальное решение: для одной и той же задачи может существовать несколько решений, и мы стремимся найти максимально эффективный алгоритм.

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

        • Временная эффективность: продолжительность выполнения алгоритма.
        • Пространственная эффективность: объем памяти, занимаемой алгоритмом.

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

        Методы оценки эффективности делятся на два типа: практическое тестирование и теоретическую оценку.

        ","path":["Глава 2. Анализ сложности","2.1   Оценка эффективности алгоритмов"],"tags":[]},{"location":"chapter_computational_complexity/performance_evaluation/#211","level":2,"title":"2.1.1   Практическое тестирование","text":"

        Предположим, у нас есть алгоритмы A и B, которые решают одну и ту же задачу, и необходимо сравнить их эффективность. Самый прямой метод - это запустить оба алгоритма на компьютере и зафиксировать время их выполнения и объем используемой памяти. Этот метод отражает реальную ситуацию, но имеет значительные ограничения.

        С одной стороны, сложно исключить влияние факторов тестовой среды. Аппаратная конфигурация влияет на производительность алгоритма. Например, если алгоритм обладает высокой степенью параллелизма, он будет лучше работать на многоядерных CPU. Если алгоритм интенсивно использует память, его производительность будет выше на высокопроизводительной памяти. Это означает, что результаты тестирования на разных машинах могут значительно отличаться, а для получения средней эффективности пришлось бы тестировать на различных платформах, что крайне затруднительно.

        С другой стороны, проведение полного тестирования требует значительных ресурсов. С изменением объема входных данных алгоритмы демонстрируют разную эффективность. Например, при небольшом объеме данных алгоритм A может работать быстрее, чем алгоритм B, но при большом объеме данных результат может быть противоположным. Следовательно, для получения убедительных выводов необходимо тестировать различные масштабы входных данных, что требует значительных вычислительных ресурсов.

        ","path":["Глава 2. Анализ сложности","2.1   Оценка эффективности алгоритмов"],"tags":[]},{"location":"chapter_computational_complexity/performance_evaluation/#212","level":2,"title":"2.1.2   Теоретическая оценка","text":"

        Из-за значительных ограничений практического тестирования можно рассмотреть возможность оценки эффективности алгоритмов только с помощью вычислений. Такой метод называется анализом асимптотической сложности (asymptotic complexity analysis), или сокращенно анализом сложности.

        Анализ сложности позволяет отразить зависимость между ресурсами времени и пространства, необходимыми для выполнения алгоритма, и размером входных данных. Он описывает тенденцию роста времени и пространства, необходимых для выполнения алгоритма, по мере увеличения размера входных данных. Это определение может показаться сложным, но его можно разбить на три ключевых момента.

        • «Ресурсы времени и пространства» соответствуют временной сложности (time complexity) и пространственной сложности (space complexity).
        • «По мере увеличения размера входных данных» означает, что сложность отражает зависимость эффективности алгоритма от объема входных данных.
        • «Тенденция роста времени и пространства» указывает, что анализ сложности фокусируется не на конкретных значениях времени выполнения или объема занимаемой памяти, а на скорости их роста.

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

        • Он не требует фактического выполнения кода, что делает его более экологичным и энергосберегающим.
        • Он независим от тестовой среды, а результаты анализа применимы ко всем платформам выполнения.
        • Он может продемонстрировать эффективность алгоритма при различных объемах данных, особенно при больших объемах.

        Tip

        Если понятие сложности пока все еще кажется вам запутанным, не переживайте: мы подробно разберем его в следующих разделах.

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

        Сложность - это математическое понятие, которое новичкам может показаться абстрактным и сложным для изучения. С этой точки зрения анализ сложности не то, с чего стоит начинать изучение алгоритмов. Однако, обсуждая особенности той или иной структуры данных или алгоритма, невозможно избежать анализа их скорости выполнения и использования памяти.

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

        ","path":["Глава 2. Анализ сложности","2.1   Оценка эффективности алгоритмов"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/","level":1,"title":"2.4   Пространственная сложность","text":"

        Пространственная сложность (space complexity) служит для оценки того, как меняется объем памяти, требуемой алгоритму, по мере роста объема данных. Это понятие очень похоже на временную сложность, только вместо времени выполнения рассматривается объем используемой памяти.

        ","path":["Глава 2. Анализ сложности","2.4   Пространственная сложность"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#241","level":2,"title":"2.4.1   Пространство, связанное с алгоритмом","text":"

        Память, которую использует алгоритм во время работы, в основном делится на следующие части.

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

        Как правило, при анализе пространственной сложности в расчет включают временное пространство и выходное пространство.

        Временное пространство можно дополнительно разделить на три части.

        • Временные данные: используются для хранения различных констант, переменных, объектов и т.д., возникающих во время выполнения алгоритма.
        • Пространство кадров стека: используется для хранения контекстных данных вызываемых функций. При каждом вызове функции система создает на вершине стека новый кадр. После возврата функции пространство этого кадра освобождается.
        • Пространство инструкций: используется для хранения скомпилированных инструкций программы и в реальном подсчете обычно не учитывается.

        При анализе пространственной сложности программы обычно учитываются временные данные, пространство стека и выходные данные, как показано на рисунке 2-15.

        Рисунок 2-15   Пространство, используемое алгоритмом

        Ниже приведен соответствующий код:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
        class Node:\n    \"\"\"Класс\"\"\"\n    def __init__(self, x: int):\n        self.val: int = x              # Значение узла\n        self.next: Node | None = None  # Ссылка на следующий узел\n\ndef function() -> int:\n    \"\"\"Функция\"\"\"\n    # Выполнить некоторые операции...\n    return 0\n\ndef algorithm(n) -> int:  # Входные данные\n    A = 0                 # Временные данные (константа, обычно обозначается заглавной буквой)\n    b = 0                 # Временные данные (переменная)\n    node = Node(0)        # Временные данные (объект)\n    c = function()        # Пространство кадра стека (вызов функции)\n    return A + b + c      # Выходные данные\n
        /* Структура */\nstruct Node {\n    int val;\n    Node *next;\n    Node(int x) : val(x), next(nullptr) {}\n};\n\n/* Функция */\nint func() {\n    // Выполнить некоторые операции...\n    return 0;\n}\n\nint algorithm(int n) {        // Входные данные\n    const int a = 0;          // Временные данные (константа)\n    int b = 0;                // Временные данные (переменная)\n    Node* node = new Node(0); // Временные данные (объект)\n    int c = func();           // Пространство кадра стека (вызов функции)\n    return a + b + c;         // Выходные данные\n}\n
        /* Класс */\nclass Node {\n    int val;\n    Node next;\n    Node(int x) { val = x; }\n}\n\n/* Функция */\nint function() {\n    // Выполнить некоторые операции...\n    return 0;\n}\n\nint algorithm(int n) {        // Входные данные\n    final int a = 0;          // Временные данные (константа)\n    int b = 0;                // Временные данные (переменная)\n    Node node = new Node(0);  // Временные данные (объект)\n    int c = function();       // Пространство кадра стека (вызов функции)\n    return a + b + c;         // Выходные данные\n}\n
        /* Класс */\nclass Node(int x) {\n    int val = x;\n    Node next;\n}\n\n/* Функция */\nint Function() {\n    // Выполнить некоторые операции...\n    return 0;\n}\n\nint Algorithm(int n) {        // Входные данные\n    const int a = 0;          // Временные данные (константа)\n    int b = 0;                // Временные данные (переменная)\n    Node node = new(0);       // Временные данные (объект)\n    int c = Function();       // Пространство кадра стека (вызов функции)\n    return a + b + c;         // Выходные данные\n}\n
        /* Структура */\ntype node struct {\n    val  int\n    next *node\n}\n\n/* Создать структуру node */\nfunc newNode(val int) *node {\n    return &node{val: val}\n}\n\n/* Функция */\nfunc function() int {\n    // Выполнить некоторые операции...\n    return 0\n}\n\nfunc algorithm(n int) int { // Входные данные\n    const a = 0             // Временные данные (константа)\n    b := 0                  // Временные данные (переменная)\n    newNode(0)              // Временные данные (объект)\n    c := function()         // Пространство кадра стека (вызов функции)\n    return a + b + c        // Выходные данные\n}\n
        /* Класс */\nclass Node {\n    var val: Int\n    var next: Node?\n\n    init(x: Int) {\n        val = x\n    }\n}\n\n/* Функция */\nfunc function() -> Int {\n    // Выполнить некоторые операции...\n    return 0\n}\n\nfunc algorithm(n: Int) -> Int { // Входные данные\n    let a = 0             // Временные данные (константа)\n    var b = 0             // Временные данные (переменная)\n    let node = Node(x: 0) // Временные данные (объект)\n    let c = function()    // Пространство кадра стека (вызов функции)\n    return a + b + c      // Выходные данные\n}\n
        /* Класс */\nclass Node {\n    val;\n    next;\n    constructor(val) {\n        this.val = val === undefined ? 0 : val; // Значение узла\n        this.next = null;                       // Ссылка на следующий узел\n    }\n}\n\n/* Функция */\nfunction constFunc() {\n    // Выполнить некоторые операции\n    return 0;\n}\n\nfunction algorithm(n) {       // Входные данные\n    const a = 0;              // Временные данные (константа)\n    let b = 0;                // Временные данные (переменная)\n    const node = new Node(0); // Временные данные (объект)\n    const c = constFunc();    // Пространство кадра стека (вызов функции)\n    return a + b + c;         // Выходные данные\n}\n
        /* Класс */\nclass Node {\n    val: number;\n    next: Node | null;\n    constructor(val?: number) {\n        this.val = val === undefined ? 0 : val; // Значение узла\n        this.next = null;                       // Ссылка на следующий узел\n    }\n}\n\n/* Функция */\nfunction constFunc(): number {\n    // Выполнить некоторые операции\n    return 0;\n}\n\nfunction algorithm(n: number): number { // Входные данные\n    const a = 0;                        // Временные данные (константа)\n    let b = 0;                          // Временные данные (переменная)\n    const node = new Node(0);           // Временные данные (объект)\n    const c = constFunc();              // Пространство кадра стека (вызов функции)\n    return a + b + c;                   // Выходные данные\n}\n
        /* Класс */\nclass Node {\n  int val;\n  Node next;\n  Node(this.val, [this.next]);\n}\n\n/* Функция */\nint function() {\n  // Выполнить некоторые операции...\n  return 0;\n}\n\nint algorithm(int n) {  // Входные данные\n  const int a = 0;      // Временные данные (константа)\n  int b = 0;            // Временные данные (переменная)\n  Node node = Node(0);  // Временные данные (объект)\n  int c = function();   // Пространство кадра стека (вызов функции)\n  return a + b + c;     // Выходные данные\n}\n
        use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* Структура */\nstruct Node {\n    val: i32,\n    next: Option<Rc<RefCell<Node>>>,\n}\n\n/* Создать структуру Node */\nimpl Node {\n    fn new(val: i32) -> Self {\n        Self { val: val, next: None }\n    }\n}\n\n/* Функция */\nfn function() -> i32 {      \n    // Выполнить некоторые операции...\n    return 0;\n}\n\nfn algorithm(n: i32) -> i32 {       // Входные данные\n    const a: i32 = 0;               // Временные данные (константа)\n    let mut b = 0;                  // Временные данные (переменная)\n    let node = Node::new(0);        // Временные данные (объект)\n    let c = function();             // Пространство кадра стека (вызов функции)\n    return a + b + c;               // Выходные данные\n}\n
        /* Функция */\nint func() {\n    // Выполнить некоторые операции...\n    return 0;\n}\n\nint algorithm(int n) { // Входные данные\n    const int a = 0;   // Временные данные (константа)\n    int b = 0;         // Временные данные (переменная)\n    int c = func();    // Пространство кадра стека (вызов функции)\n    return a + b + c;  // Выходные данные\n}\n
        /* Класс */\nclass Node(var _val: Int) {\n    var next: Node? = null\n}\n\n/* Функция */\nfun function(): Int {\n    // Выполнить некоторые операции...\n    return 0\n}\n\nfun algorithm(n: Int): Int { // Входные данные\n    val a = 0                // Временные данные (константа)\n    var b = 0                // Временные данные (переменная)\n    val node = Node(0)       // Временные данные (объект)\n    val c = function()       // Пространство кадра стека (вызов функции)\n    return a + b + c         // Выходные данные\n}\n
        ### Класс ###\nclass Node\n    attr_accessor :val      # Значение узла\n    attr_accessor :next     # Ссылка на следующий узел\n\n    def initialize(x)\n        @val = x\n    end\nend\n\n### Функция ###\ndef function\n    # Выполнить некоторые операции...\n    0\nend\n\n### Алгоритм ###\ndef algorithm(n)        # Входные данные\n    a = 0               # Временные данные (константа)\n    b = 0               # Временные данные (переменная)\n    node = Node.new(0)  # Временные данные (объект)\n    c = function        # Пространство кадра стека (вызов функции)\n    a + b + c           # Выходные данные\nend\n
        ","path":["Глава 2. Анализ сложности","2.4   Пространственная сложность"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#242","level":2,"title":"2.4.2   Метод вывода","text":"

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

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

        Рассмотрим следующий код. Понятие худшей пространственной сложности здесь имеет два значения.

        1. Ориентир на худшие входные данные: когда \\(n < 10\\) , пространственная сложность равна \\(O(1)\\) ; но когда \\(n > 10\\) , инициализированный массив nums занимает \\(O(n)\\) пространства, поэтому худшая пространственная сложность равна \\(O(n)\\) .
        2. Ориентир на пиковое использование памяти во время выполнения: например, до выполнения последней строки программа занимает \\(O(1)\\) пространства. При инициализации массива nums она занимает \\(O(n)\\) пространства, поэтому худшая пространственная сложность также равна \\(O(n)\\) .
        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
        def algorithm(n: int):\n    a = 0               # O(1)\n    b = [0] * 10000     # O(1)\n    if n > 10:\n        nums = [0] * n  # O(n)\n
        void algorithm(int n) {\n    int a = 0;               // O(1)\n    vector<int> b(10000);    // O(1)\n    if (n > 10)\n        vector<int> nums(n); // O(n)\n}\n
        void algorithm(int n) {\n    int a = 0;                   // O(1)\n    int[] b = new int[10000];    // O(1)\n    if (n > 10)\n        int[] nums = new int[n]; // O(n)\n}\n
        void Algorithm(int n) {\n    int a = 0;                   // O(1)\n    int[] b = new int[10000];    // O(1)\n    if (n > 10) {\n        int[] nums = new int[n]; // O(n)\n    }\n}\n
        func algorithm(n int) {\n    a := 0                      // O(1)\n    b := make([]int, 10000)     // O(1)\n    var nums []int\n    if n > 10 {\n        nums := make([]int, n)  // O(n)\n    }\n    fmt.Println(a, b, nums)\n}\n
        func algorithm(n: Int) {\n    let a = 0 // O(1)\n    let b = Array(repeating: 0, count: 10000) // O(1)\n    if n > 10 {\n        let nums = Array(repeating: 0, count: n) // O(n)\n    }\n}\n
        function algorithm(n) {\n    const a = 0;                   // O(1)\n    const b = new Array(10000);    // O(1)\n    if (n > 10) {\n        const nums = new Array(n); // O(n)\n    }\n}\n
        function algorithm(n: number): void {\n    const a = 0;                   // O(1)\n    const b = new Array(10000);    // O(1)\n    if (n > 10) {\n        const nums = new Array(n); // O(n)\n    }\n}\n
        void algorithm(int n) {\n  int a = 0;                            // O(1)\n  List<int> b = List.filled(10000, 0);  // O(1)\n  if (n > 10) {\n    List<int> nums = List.filled(n, 0); // O(n)\n  }\n}\n
        fn algorithm(n: i32) {\n    let a = 0;                              // O(1)\n    let b = [0; 10000];                     // O(1)\n    if n > 10 {\n        let nums = vec![0; n as usize];     // O(n)\n    }\n}\n
        void algorithm(int n) {\n    int a = 0;               // O(1)\n    int b[10000];            // O(1)\n    if (n > 10)\n        int nums[n] = {0};   // O(n)\n}\n
        fun algorithm(n: Int) {\n    val a = 0                    // O(1)\n    val b = IntArray(10000)      // O(1)\n    if (n > 10) {\n        val nums = IntArray(n)   // O(n)\n    }\n}\n
        def algorithm(n)\n    a = 0                           # O(1)\n    b = Array.new(10000)            # O(1)\n    nums = Array.new(n) if n > 10   # O(n)\nend\n

        В рекурсивных функциях необходимо учитывать пространство кадров стека. Рассмотрим следующий код:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
        def function() -> int:\n    # Выполнить некоторые операции\n    return 0\n\ndef loop(n: int):\n    \"\"\"Пространственная сложность цикла равна O(1)\"\"\"\n    for _ in range(n):\n        function()\n\ndef recur(n: int):\n    \"\"\"Пространственная сложность рекурсии равна O(n)\"\"\"\n    if n == 1:\n        return\n    return recur(n - 1)\n
        int func() {\n    // Выполнить некоторые операции\n    return 0;\n}\n/* Пространственная сложность цикла равна O(1) */\nvoid loop(int n) {\n    for (int i = 0; i < n; i++) {\n        func();\n    }\n}\n/* Пространственная сложность рекурсии равна O(n) */\nvoid recur(int n) {\n    if (n == 1) return;\n    recur(n - 1);\n}\n
        int function() {\n    // Выполнить некоторые операции\n    return 0;\n}\n/* Пространственная сложность цикла равна O(1) */\nvoid loop(int n) {\n    for (int i = 0; i < n; i++) {\n        function();\n    }\n}\n/* Пространственная сложность рекурсии равна O(n) */\nvoid recur(int n) {\n    if (n == 1) return;\n    recur(n - 1);\n}\n
        int Function() {\n    // Выполнить некоторые операции\n    return 0;\n}\n/* Пространственная сложность цикла равна O(1) */\nvoid Loop(int n) {\n    for (int i = 0; i < n; i++) {\n        Function();\n    }\n}\n/* Пространственная сложность рекурсии равна O(n) */\nint Recur(int n) {\n    if (n == 1) return 1;\n    return Recur(n - 1);\n}\n
        func function() int {\n    // Выполнить некоторые операции\n    return 0\n}\n\n/* Пространственная сложность цикла равна O(1) */\nfunc loop(n int) {\n    for i := 0; i < n; i++ {\n        function()\n    }\n}\n\n/* Пространственная сложность рекурсии равна O(n) */\nfunc recur(n int) {\n    if n == 1 {\n        return\n    }\n    recur(n - 1)\n}\n
        @discardableResult\nfunc function() -> Int {\n    // Выполнить некоторые операции\n    return 0\n}\n\n/* Пространственная сложность цикла равна O(1) */\nfunc loop(n: Int) {\n    for _ in 0 ..< n {\n        function()\n    }\n}\n\n/* Пространственная сложность рекурсии равна O(n) */\nfunc recur(n: Int) {\n    if n == 1 {\n        return\n    }\n    recur(n: n - 1)\n}\n
        function constFunc() {\n    // Выполнить некоторые операции\n    return 0;\n}\n/* Пространственная сложность цикла равна O(1) */\nfunction loop(n) {\n    for (let i = 0; i < n; i++) {\n        constFunc();\n    }\n}\n/* Пространственная сложность рекурсии равна O(n) */\nfunction recur(n) {\n    if (n === 1) return;\n    return recur(n - 1);\n}\n
        function constFunc(): number {\n    // Выполнить некоторые операции\n    return 0;\n}\n/* Пространственная сложность цикла равна O(1) */\nfunction loop(n: number): void {\n    for (let i = 0; i < n; i++) {\n        constFunc();\n    }\n}\n/* Пространственная сложность рекурсии равна O(n) */\nfunction recur(n: number): void {\n    if (n === 1) return;\n    return recur(n - 1);\n}\n
        int function() {\n  // Выполнить некоторые операции\n  return 0;\n}\n/* Пространственная сложность цикла равна O(1) */\nvoid loop(int n) {\n  for (int i = 0; i < n; i++) {\n    function();\n  }\n}\n/* Пространственная сложность рекурсии равна O(n) */\nvoid recur(int n) {\n  if (n == 1) return;\n  recur(n - 1);\n}\n
        fn function() -> i32 {\n    // Выполнить некоторые операции\n    return 0;\n}\n/* Пространственная сложность цикла равна O(1) */\nfn loop(n: i32) {\n    for i in 0..n {\n        function();\n    }\n}\n/* Пространственная сложность рекурсии равна O(n) */\nfn recur(n: i32) {\n    if n == 1 {\n        return;\n    }\n    recur(n - 1);\n}\n
        int func() {\n    // Выполнить некоторые операции\n    return 0;\n}\n/* Пространственная сложность цикла равна O(1) */\nvoid loop(int n) {\n    for (int i = 0; i < n; i++) {\n        func();\n    }\n}\n/* Пространственная сложность рекурсии равна O(n) */\nvoid recur(int n) {\n    if (n == 1) return;\n    recur(n - 1);\n}\n
        fun function(): Int {\n    // Выполнить некоторые операции\n    return 0\n}\n/* Пространственная сложность цикла равна O(1) */\nfun loop(n: Int) {\n    for (i in 0..<n) {\n        function()\n    }\n}\n/* Пространственная сложность рекурсии равна O(n) */\nfun recur(n: Int) {\n    if (n == 1) return\n    return recur(n - 1)\n}\n
        def function\n    # Выполнить некоторые операции\n    0\nend\n\n### Пространственная сложность цикла равна O(1) ###\ndef loop(n)\n    (0...n).each { function }\nend\n\n### Пространственная сложность рекурсии равна O(n) ###\ndef recur(n)\n    return if n == 1\n    recur(n - 1)\nend\n

        Функции loop() и recur() имеют временную сложность \\(O(n)\\) , но их пространственная сложность различается.

        • Функция loop() вызывает function() в цикле \\(n\\) раз. На каждой итерации function() возвращается и освобождает пространство своего кадра стека, поэтому пространственная сложность по-прежнему равна \\(O(1)\\) .
        • Рекурсивная функция recur() во время выполнения одновременно содержит \\(n\\) еще не завершившихся экземпляров recur() , поэтому занимает \\(O(n)\\) пространства кадров стека.
        ","path":["Глава 2. Анализ сложности","2.4   Пространственная сложность"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#243","level":2,"title":"2.4.3   Распространенные типы","text":"

        Пусть размер входных данных равен \\(n\\) . На рисунке 2-16 показаны распространенные типы пространственной сложности в порядке от меньшей к большей.

        \\[ \\begin{aligned} & O(1) < O(\\log n) < O(n) < O(n^2) < O(2^n) \\newline & \\text{Постоянная} < \\text{Логарифмическая} < \\text{Линейная} < \\text{Квадратичная} < \\text{Экспоненциальная} \\end{aligned} \\]

        Рисунок 2-16   Распространенные типы пространственной сложности

        ","path":["Глава 2. Анализ сложности","2.4   Пространственная сложность"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#1-o1","level":3,"title":"1.   Постоянная сложность \\(O(1)\\)","text":"

        Постоянная сложность обычно встречается у констант, переменных и объектов, количество которых не зависит от размера входных данных \\(n\\) .

        Следует заметить, что память, занятая инициализацией переменных или вызовом функций внутри цикла, освобождается при переходе к следующей итерации, поэтому она не накапливается, и пространственная сложность по-прежнему остается \\(O(1)\\) :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
        def function() -> int:\n    \"\"\"Функция\"\"\"\n    # Выполнить некоторые операции\n    return 0\n\ndef constant(n: int):\n    \"\"\"Постоянная сложность\"\"\"\n    # Константы, переменные и объекты занимают O(1) памяти\n    a = 0\n    nums = [0] * 10000\n    node = ListNode(0)\n    # Переменные в цикле занимают O(1) памяти\n    for _ in range(n):\n        c = 0\n    # Функции в цикле занимают O(1) памяти\n    for _ in range(n):\n        function()\n
        space_complexity.cpp
        /* Функция */\nint func() {\n    // Выполнить некоторые операции\n    return 0;\n}\n\n/* Постоянная сложность */\nvoid constant(int n) {\n    // Константы, переменные и объекты занимают O(1) памяти\n    const int a = 0;\n    int b = 0;\n    vector<int> nums(10000);\n    ListNode node(0);\n    // Переменные в цикле занимают O(1) памяти\n    for (int i = 0; i < n; i++) {\n        int c = 0;\n    }\n    // Функции в цикле занимают O(1) памяти\n    for (int i = 0; i < n; i++) {\n        func();\n    }\n}\n
        space_complexity.java
        /* Функция */\nint function() {\n    // Выполнить некоторые операции\n    return 0;\n}\n\n/* Постоянная сложность */\nvoid constant(int n) {\n    // Константы, переменные и объекты занимают O(1) памяти\n    final int a = 0;\n    int b = 0;\n    int[] nums = new int[10000];\n    ListNode node = new ListNode(0);\n    // Переменные в цикле занимают O(1) памяти\n    for (int i = 0; i < n; i++) {\n        int c = 0;\n    }\n    // Функции в цикле занимают O(1) памяти\n    for (int i = 0; i < n; i++) {\n        function();\n    }\n}\n
        space_complexity.cs
        /* Функция */\nint Function() {\n    // Выполнить некоторые операции\n    return 0;\n}\n\n/* Постоянная сложность */\nvoid Constant(int n) {\n    // Константы, переменные и объекты занимают O(1) памяти\n    int a = 0;\n    int b = 0;\n    int[] nums = new int[10000];\n    ListNode node = new(0);\n    // Переменные в цикле занимают O(1) памяти\n    for (int i = 0; i < n; i++) {\n        int c = 0;\n    }\n    // Функции в цикле занимают O(1) памяти\n    for (int i = 0; i < n; i++) {\n        Function();\n    }\n}\n
        space_complexity.go
        /* Функция */\nfunc function() int {\n    // Выполнить некоторые операции...\n    return 0\n}\n\n/* Постоянная сложность */\nfunc spaceConstant(n int) {\n    // Константы, переменные и объекты занимают O(1) памяти\n    const a = 0\n    b := 0\n    nums := make([]int, 10000)\n    node := newNode(0)\n    // Переменные в цикле занимают O(1) памяти\n    var c int\n    for i := 0; i < n; i++ {\n        c = 0\n    }\n    // Функции в цикле занимают O(1) памяти\n    for i := 0; i < n; i++ {\n        function()\n    }\n    b += 0\n    c += 0\n    nums[0] = 0\n    node.val = 0\n}\n
        space_complexity.swift
        /* Функция */\n@discardableResult\nfunc function() -> Int {\n    // Выполнить некоторые операции\n    return 0\n}\n\n/* Постоянная сложность */\nfunc constant(n: Int) {\n    // Константы, переменные и объекты занимают O(1) памяти\n    let a = 0\n    var b = 0\n    let nums = Array(repeating: 0, count: 10000)\n    let node = ListNode(x: 0)\n    // Переменные в цикле занимают O(1) памяти\n    for _ in 0 ..< n {\n        let c = 0\n    }\n    // Функции в цикле занимают O(1) памяти\n    for _ in 0 ..< n {\n        function()\n    }\n}\n
        space_complexity.js
        /* Функция */\nfunction constFunc() {\n    // Выполнить некоторые операции\n    return 0;\n}\n\n/* Постоянная сложность */\nfunction constant(n) {\n    // Константы, переменные и объекты занимают O(1) памяти\n    const a = 0;\n    const b = 0;\n    const nums = new Array(10000);\n    const node = new ListNode(0);\n    // Переменные в цикле занимают O(1) памяти\n    for (let i = 0; i < n; i++) {\n        const c = 0;\n    }\n    // Функции в цикле занимают O(1) памяти\n    for (let i = 0; i < n; i++) {\n        constFunc();\n    }\n}\n
        space_complexity.ts
        /* Функция */\nfunction constFunc(): number {\n    // Выполнить некоторые операции\n    return 0;\n}\n\n/* Постоянная сложность */\nfunction constant(n: number): void {\n    // Константы, переменные и объекты занимают O(1) памяти\n    const a = 0;\n    const b = 0;\n    const nums = new Array(10000);\n    const node = new ListNode(0);\n    // Переменные в цикле занимают O(1) памяти\n    for (let i = 0; i < n; i++) {\n        const c = 0;\n    }\n    // Функции в цикле занимают O(1) памяти\n    for (let i = 0; i < n; i++) {\n        constFunc();\n    }\n}\n
        space_complexity.dart
        /* Функция */\nint function() {\n  // Выполнить некоторые операции\n  return 0;\n}\n\n/* Постоянная сложность */\nvoid constant(int n) {\n  // Константы, переменные и объекты занимают O(1) памяти\n  final int a = 0;\n  int b = 0;\n  List<int> nums = List.filled(10000, 0);\n  ListNode node = ListNode(0);\n  // Переменные в цикле занимают O(1) памяти\n  for (var i = 0; i < n; i++) {\n    int c = 0;\n  }\n  // Функции в цикле занимают O(1) памяти\n  for (var i = 0; i < n; i++) {\n    function();\n  }\n}\n
        space_complexity.rs
        /* Функция */\nfn function() -> i32 {\n    // Выполнить некоторые операции\n    return 0;\n}\n\n/* Постоянная сложность */\n#[allow(unused)]\nfn constant(n: i32) {\n    // Константы, переменные и объекты занимают O(1) памяти\n    const A: i32 = 0;\n    let b = 0;\n    let nums = vec![0; 10000];\n    let node = ListNode::new(0);\n    // Переменные в цикле занимают O(1) памяти\n    for i in 0..n {\n        let c = 0;\n    }\n    // Функции в цикле занимают O(1) памяти\n    for i in 0..n {\n        function();\n    }\n}\n
        space_complexity.c
        /* Функция */\nint func() {\n    // Выполнить некоторые операции\n    return 0;\n}\n\n/* Постоянная сложность */\nvoid constant(int n) {\n    // Константы, переменные и объекты занимают O(1) памяти\n    const int a = 0;\n    int b = 0;\n    int nums[1000];\n    ListNode *node = newListNode(0);\n    free(node);\n    // Переменные в цикле занимают O(1) памяти\n    for (int i = 0; i < n; i++) {\n        int c = 0;\n    }\n    // Функции в цикле занимают O(1) памяти\n    for (int i = 0; i < n; i++) {\n        func();\n    }\n}\n
        space_complexity.kt
        /* Функция */\nfun function(): Int {\n    // Выполнить некоторые операции\n    return 0\n}\n\n/* Постоянная сложность */\nfun constant(n: Int) {\n    // Константы, переменные и объекты занимают O(1) памяти\n    val a = 0\n    var b = 0\n    val nums = Array(10000) { 0 }\n    val node = ListNode(0)\n    // Переменные в цикле занимают O(1) памяти\n    for (i in 0..<n) {\n        val c = 0\n    }\n    // Функции в цикле занимают O(1) памяти\n    for (i in 0..<n) {\n        function()\n    }\n}\n
        space_complexity.rb
        ### Функция ###\ndef function\n  # Выполнить некоторые операции\n  0\nend\n\n### Постоянная сложность ###\ndef constant(n)\n  # Константы, переменные и объекты занимают O(1) памяти\n  a = 0\n  nums = [0] * 10000\n  node = ListNode.new\n\n  # Переменные в цикле занимают O(1) памяти\n  (0...n).each { c = 0 }\n  # Функции в цикле занимают O(1) памяти\n  (0...n).each { function }\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 2. Анализ сложности","2.4   Пространственная сложность"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#2-on","level":3,"title":"2.   Линейная сложность \\(O(n)\\)","text":"

        Линейная сложность часто встречается у массивов, списков, стеков, очередей и других структур, число элементов в которых пропорционально \\(n\\) :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
        def linear(n: int):\n    \"\"\"Линейная сложность\"\"\"\n    # Список длины n занимает O(n) памяти\n    nums = [0] * n\n    # Хеш-таблица длины n занимает O(n) памяти\n    hmap = dict[int, str]()\n    for i in range(n):\n        hmap[i] = str(i)\n
        space_complexity.cpp
        /* Линейная сложность */\nvoid linear(int n) {\n    // Массив длины n занимает O(n) памяти\n    vector<int> nums(n);\n    // Список длины n занимает O(n) памяти\n    vector<ListNode> nodes;\n    for (int i = 0; i < n; i++) {\n        nodes.push_back(ListNode(i));\n    }\n    // Хеш-таблица длины n занимает O(n) памяти\n    unordered_map<int, string> map;\n    for (int i = 0; i < n; i++) {\n        map[i] = to_string(i);\n    }\n}\n
        space_complexity.java
        /* Линейная сложность */\nvoid linear(int n) {\n    // Массив длины n занимает O(n) памяти\n    int[] nums = new int[n];\n    // Список длины n занимает O(n) памяти\n    List<ListNode> nodes = new ArrayList<>();\n    for (int i = 0; i < n; i++) {\n        nodes.add(new ListNode(i));\n    }\n    // Хеш-таблица длины n занимает O(n) памяти\n    Map<Integer, String> map = new HashMap<>();\n    for (int i = 0; i < n; i++) {\n        map.put(i, String.valueOf(i));\n    }\n}\n
        space_complexity.cs
        /* Линейная сложность */\nvoid Linear(int n) {\n    // Массив длины n занимает O(n) памяти\n    int[] nums = new int[n];\n    // Список длины n занимает O(n) памяти\n    List<ListNode> nodes = [];\n    for (int i = 0; i < n; i++) {\n        nodes.Add(new ListNode(i));\n    }\n    // Хеш-таблица длины n занимает O(n) памяти\n    Dictionary<int, string> map = [];\n    for (int i = 0; i < n; i++) {\n        map.Add(i, i.ToString());\n    }\n}\n
        space_complexity.go
        /* Линейная сложность */\nfunc spaceLinear(n int) {\n    // Массив длины n занимает O(n) памяти\n    _ = make([]int, n)\n    // Список длины n занимает O(n) памяти\n    var nodes []*node\n    for i := 0; i < n; i++ {\n        nodes = append(nodes, newNode(i))\n    }\n    // Хеш-таблица длины n занимает O(n) памяти\n    m := make(map[int]string, n)\n    for i := 0; i < n; i++ {\n        m[i] = strconv.Itoa(i)\n    }\n}\n
        space_complexity.swift
        /* Линейная сложность */\nfunc linear(n: Int) {\n    // Массив длины n занимает O(n) памяти\n    let nums = Array(repeating: 0, count: n)\n    // Список длины n занимает O(n) памяти\n    let nodes = (0 ..< n).map { ListNode(x: $0) }\n    // Хеш-таблица длины n занимает O(n) памяти\n    let map = Dictionary(uniqueKeysWithValues: (0 ..< n).map { ($0, \"\\($0)\") })\n}\n
        space_complexity.js
        /* Линейная сложность */\nfunction linear(n) {\n    // Массив длины n занимает O(n) памяти\n    const nums = new Array(n);\n    // Список длины n занимает O(n) памяти\n    const nodes = [];\n    for (let i = 0; i < n; i++) {\n        nodes.push(new ListNode(i));\n    }\n    // Хеш-таблица длины n занимает O(n) памяти\n    const map = new Map();\n    for (let i = 0; i < n; i++) {\n        map.set(i, i.toString());\n    }\n}\n
        space_complexity.ts
        /* Линейная сложность */\nfunction linear(n: number): void {\n    // Массив длины n занимает O(n) памяти\n    const nums = new Array(n);\n    // Список длины n занимает O(n) памяти\n    const nodes: ListNode[] = [];\n    for (let i = 0; i < n; i++) {\n        nodes.push(new ListNode(i));\n    }\n    // Хеш-таблица длины n занимает O(n) памяти\n    const map = new Map();\n    for (let i = 0; i < n; i++) {\n        map.set(i, i.toString());\n    }\n}\n
        space_complexity.dart
        /* Линейная сложность */\nvoid linear(int n) {\n  // Массив длины n занимает O(n) памяти\n  List<int> nums = List.filled(n, 0);\n  // Список длины n занимает O(n) памяти\n  List<ListNode> nodes = [];\n  for (var i = 0; i < n; i++) {\n    nodes.add(ListNode(i));\n  }\n  // Хеш-таблица длины n занимает O(n) памяти\n  Map<int, String> map = HashMap();\n  for (var i = 0; i < n; i++) {\n    map.putIfAbsent(i, () => i.toString());\n  }\n}\n
        space_complexity.rs
        /* Линейная сложность */\n#[allow(unused)]\nfn linear(n: i32) {\n    // Массив длины n занимает O(n) памяти\n    let mut nums = vec![0; n as usize];\n    // Список длины n занимает O(n) памяти\n    let mut nodes = Vec::new();\n    for i in 0..n {\n        nodes.push(ListNode::new(i))\n    }\n    // Хеш-таблица длины n занимает O(n) памяти\n    let mut map = HashMap::new();\n    for i in 0..n {\n        map.insert(i, i.to_string());\n    }\n}\n
        space_complexity.c
        /* Хеш-таблица */\ntypedef struct {\n    int key;\n    int val;\n    UT_hash_handle hh; // Реализовано на основе uthash.h\n} HashTable;\n\n/* Линейная сложность */\nvoid linear(int n) {\n    // Массив длины n занимает O(n) памяти\n    int *nums = malloc(sizeof(int) * n);\n    free(nums);\n\n    // Список длины n занимает O(n) памяти\n    ListNode **nodes = malloc(sizeof(ListNode *) * n);\n    for (int i = 0; i < n; i++) {\n        nodes[i] = newListNode(i);\n    }\n    // Освобождение памяти\n    for (int i = 0; i < n; i++) {\n        free(nodes[i]);\n    }\n    free(nodes);\n\n    // Хеш-таблица длины n занимает O(n) памяти\n    HashTable *h = NULL;\n    for (int i = 0; i < n; i++) {\n        HashTable *tmp = malloc(sizeof(HashTable));\n        tmp->key = i;\n        tmp->val = i;\n        HASH_ADD_INT(h, key, tmp);\n    }\n\n    // Освобождение памяти\n    HashTable *curr, *tmp;\n    HASH_ITER(hh, h, curr, tmp) {\n        HASH_DEL(h, curr);\n        free(curr);\n    }\n}\n
        space_complexity.kt
        /* Линейная сложность */\nfun linear(n: Int) {\n    // Массив длины n занимает O(n) памяти\n    val nums = Array(n) { 0 }\n    // Список длины n занимает O(n) памяти\n    val nodes = mutableListOf<ListNode>()\n    for (i in 0..<n) {\n        nodes.add(ListNode(i))\n    }\n    // Хеш-таблица длины n занимает O(n) памяти\n    val map = mutableMapOf<Int, String>()\n    for (i in 0..<n) {\n        map[i] = i.toString()\n    }\n}\n
        space_complexity.rb
        ### Линейная сложность ###\ndef linear(n)\n  # Список длины n занимает O(n) памяти\n  nums = Array.new(n, 0)\n\n  # Хеш-таблица длины n занимает O(n) памяти\n  hmap = {}\n  for i in 0...n\n    hmap[i] = i.to_s\n  end\nend\n
        Визуализация кода

        Во весь экран >

        Как показано на рисунке 2-17, глубина рекурсии этой функции равна \\(n\\) , то есть одновременно существует \\(n\\) еще не завершившихся функций linear_recur() , которые используют \\(O(n)\\) пространства кадров стека:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
        def linear_recur(n: int):\n    \"\"\"Линейная сложность (рекурсивная реализация)\"\"\"\n    print(\"Рекурсия n =\", n)\n    if n == 1:\n        return\n    linear_recur(n - 1)\n
        space_complexity.cpp
        /* Линейная сложность (рекурсивная реализация) */\nvoid linearRecur(int n) {\n    cout << \"Рекурсия n = \" << n << endl;\n    if (n == 1)\n        return;\n    linearRecur(n - 1);\n}\n
        space_complexity.java
        /* Линейная сложность (рекурсивная реализация) */\nvoid linearRecur(int n) {\n    System.out.println(\"Рекурсия n = \" + n);\n    if (n == 1)\n        return;\n    linearRecur(n - 1);\n}\n
        space_complexity.cs
        /* Линейная сложность (рекурсивная реализация) */\nvoid LinearRecur(int n) {\n    Console.WriteLine(\"Рекурсия n = \" + n);\n    if (n == 1) return;\n    LinearRecur(n - 1);\n}\n
        space_complexity.go
        /* Линейная сложность (рекурсивная реализация) */\nfunc spaceLinearRecur(n int) {\n    fmt.Println(\"Рекурсия n =\", n)\n    if n == 1 {\n        return\n    }\n    spaceLinearRecur(n - 1)\n}\n
        space_complexity.swift
        /* Линейная сложность (рекурсивная реализация) */\nfunc linearRecur(n: Int) {\n    print(\"Рекурсия n = \\(n)\")\n    if n == 1 {\n        return\n    }\n    linearRecur(n: n - 1)\n}\n
        space_complexity.js
        /* Линейная сложность (рекурсивная реализация) */\nfunction linearRecur(n) {\n    console.log(`Рекурсия n = ${n}`);\n    if (n === 1) return;\n    linearRecur(n - 1);\n}\n
        space_complexity.ts
        /* Линейная сложность (рекурсивная реализация) */\nfunction linearRecur(n: number): void {\n    console.log(`Рекурсия n = ${n}`);\n    if (n === 1) return;\n    linearRecur(n - 1);\n}\n
        space_complexity.dart
        /* Линейная сложность (рекурсивная реализация) */\nvoid linearRecur(int n) {\n  print('Рекурсия n = $n');\n  if (n == 1) return;\n  linearRecur(n - 1);\n}\n
        space_complexity.rs
        /* Линейная сложность (рекурсивная реализация) */\nfn linear_recur(n: i32) {\n    println!(\"Рекурсия n = {}\", n);\n    if n == 1 {\n        return;\n    };\n    linear_recur(n - 1);\n}\n
        space_complexity.c
        /* Линейная сложность (рекурсивная реализация) */\nvoid linearRecur(int n) {\n    printf(\"Рекурсия n = %d\\r\\n\", n);\n    if (n == 1)\n        return;\n    linearRecur(n - 1);\n}\n
        space_complexity.kt
        /* Линейная сложность (рекурсивная реализация) */\nfun linearRecur(n: Int) {\n    println(\"Рекурсия n = $n\")\n    if (n == 1)\n        return\n    linearRecur(n - 1)\n}\n
        space_complexity.rb
        ### Линейная сложность ###\ndef linear(n)\n  # Список длины n занимает O(n) памяти\n  nums = Array.new(n, 0)\n\n  # Хеш-таблица длины n занимает O(n) памяти\n  hmap = {}\n  for i in 0...n\n    hmap[i] = i.to_s\n  end\nend\n\n# ## Линейная сложность (рекурсивная реализация) ###\ndef linear_recur(n)\n  puts \"Рекурсия n = #{n}\"\n  return if n == 1\n  linear_recur(n - 1)\nend\n
        Визуализация кода

        Во весь экран >

        Рисунок 2-17   Линейная пространственная сложность, порождаемая рекурсивной функцией

        ","path":["Глава 2. Анализ сложности","2.4   Пространственная сложность"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#3-on2","level":3,"title":"3.   Квадратичная сложность \\(O(n^2)\\)","text":"

        Квадратичная сложность часто встречается у матриц и графов, где число элементов связано с \\(n\\) квадратичной зависимостью:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
        def quadratic(n: int):\n    \"\"\"Квадратичная сложность\"\"\"\n    # Двумерный список занимает O(n^2) памяти\n    num_matrix = [[0] * n for _ in range(n)]\n
        space_complexity.cpp
        /* Квадратичная сложность */\nvoid quadratic(int n) {\n    // Двумерный список занимает O(n^2) памяти\n    vector<vector<int>> numMatrix;\n    for (int i = 0; i < n; i++) {\n        vector<int> tmp;\n        for (int j = 0; j < n; j++) {\n            tmp.push_back(0);\n        }\n        numMatrix.push_back(tmp);\n    }\n}\n
        space_complexity.java
        /* Квадратичная сложность */\nvoid quadratic(int n) {\n    // Матрица занимает O(n^2) памяти\n    int[][] numMatrix = new int[n][n];\n    // Двумерный список занимает O(n^2) памяти\n    List<List<Integer>> numList = new ArrayList<>();\n    for (int i = 0; i < n; i++) {\n        List<Integer> tmp = new ArrayList<>();\n        for (int j = 0; j < n; j++) {\n            tmp.add(0);\n        }\n        numList.add(tmp);\n    }\n}\n
        space_complexity.cs
        /* Квадратичная сложность */\nvoid Quadratic(int n) {\n    // Матрица занимает O(n^2) памяти\n    int[,] numMatrix = new int[n, n];\n    // Двумерный список занимает O(n^2) памяти\n    List<List<int>> numList = [];\n    for (int i = 0; i < n; i++) {\n        List<int> tmp = [];\n        for (int j = 0; j < n; j++) {\n            tmp.Add(0);\n        }\n        numList.Add(tmp);\n    }\n}\n
        space_complexity.go
        /* Квадратичная сложность */\nfunc spaceQuadratic(n int) {\n    // Матрица занимает O(n^2) памяти\n    numMatrix := make([][]int, n)\n    for i := 0; i < n; i++ {\n        numMatrix[i] = make([]int, n)\n    }\n}\n
        space_complexity.swift
        /* Квадратичная сложность */\nfunc quadratic(n: Int) {\n    // Двумерный список занимает O(n^2) памяти\n    let numList = Array(repeating: Array(repeating: 0, count: n), count: n)\n}\n
        space_complexity.js
        /* Квадратичная сложность */\nfunction quadratic(n) {\n    // Матрица занимает O(n^2) памяти\n    const numMatrix = Array(n)\n        .fill(null)\n        .map(() => Array(n).fill(null));\n    // Двумерный список занимает O(n^2) памяти\n    const numList = [];\n    for (let i = 0; i < n; i++) {\n        const tmp = [];\n        for (let j = 0; j < n; j++) {\n            tmp.push(0);\n        }\n        numList.push(tmp);\n    }\n}\n
        space_complexity.ts
        /* Квадратичная сложность */\nfunction quadratic(n: number): void {\n    // Матрица занимает O(n^2) памяти\n    const numMatrix = Array(n)\n        .fill(null)\n        .map(() => Array(n).fill(null));\n    // Двумерный список занимает O(n^2) памяти\n    const numList = [];\n    for (let i = 0; i < n; i++) {\n        const tmp = [];\n        for (let j = 0; j < n; j++) {\n            tmp.push(0);\n        }\n        numList.push(tmp);\n    }\n}\n
        space_complexity.dart
        /* Квадратичная сложность */\nvoid quadratic(int n) {\n  // Матрица занимает O(n^2) памяти\n  List<List<int>> numMatrix = List.generate(n, (_) => List.filled(n, 0));\n  // Двумерный список занимает O(n^2) памяти\n  List<List<int>> numList = [];\n  for (var i = 0; i < n; i++) {\n    List<int> tmp = [];\n    for (int j = 0; j < n; j++) {\n      tmp.add(0);\n    }\n    numList.add(tmp);\n  }\n}\n
        space_complexity.rs
        /* Квадратичная сложность */\n#[allow(unused)]\nfn quadratic(n: i32) {\n    // Матрица занимает O(n^2) памяти\n    let num_matrix = vec![vec![0; n as usize]; n as usize];\n    // Двумерный список занимает O(n^2) памяти\n    let mut num_list = Vec::new();\n    for i in 0..n {\n        let mut tmp = Vec::new();\n        for j in 0..n {\n            tmp.push(0);\n        }\n        num_list.push(tmp);\n    }\n}\n
        space_complexity.c
        /* Квадратичная сложность */\nvoid quadratic(int n) {\n    // Двумерный список занимает O(n^2) памяти\n    int **numMatrix = malloc(sizeof(int *) * n);\n    for (int i = 0; i < n; i++) {\n        int *tmp = malloc(sizeof(int) * n);\n        for (int j = 0; j < n; j++) {\n            tmp[j] = 0;\n        }\n        numMatrix[i] = tmp;\n    }\n\n    // Освобождение памяти\n    for (int i = 0; i < n; i++) {\n        free(numMatrix[i]);\n    }\n    free(numMatrix);\n}\n
        space_complexity.kt
        /* Квадратичная сложность */\nfun quadratic(n: Int) {\n    // Матрица занимает O(n^2) памяти\n    val numMatrix = arrayOfNulls<Array<Int>?>(n)\n    // Двумерный список занимает O(n^2) памяти\n    val numList = mutableListOf<MutableList<Int>>()\n    for (i in 0..<n) {\n        val tmp = mutableListOf<Int>()\n        for (j in 0..<n) {\n            tmp.add(0)\n        }\n        numList.add(tmp)\n    }\n}\n
        space_complexity.rb
        ### Квадратичная сложность ###\ndef quadratic(n)\n  # Двумерный список занимает O(n^2) памяти\n  Array.new(n) { Array.new(n, 0) }\nend\n
        Визуализация кода

        Во весь экран >

        Как показано на рисунке 2-18, глубина рекурсии этой функции равна \\(n\\) , и в каждой рекурсивной функции инициализируется массив длины \\(n\\) , \\(n-1\\) , \\(\\dots\\) , \\(2\\) , \\(1\\). Его средняя длина равна \\(n / 2\\) , поэтому в сумме используется \\(O(n^2)\\) пространства:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
        def quadratic_recur(n: int) -> int:\n    \"\"\"Квадратичная сложность (рекурсивная реализация)\"\"\"\n    if n <= 0:\n        return 0\n    # Длина массива nums равна n, n-1, ..., 2, 1\n    nums = [0] * n\n    return quadratic_recur(n - 1)\n
        space_complexity.cpp
        /* Квадратичная сложность (рекурсивная реализация) */\nint quadraticRecur(int n) {\n    if (n <= 0)\n        return 0;\n    vector<int> nums(n);\n    cout << \"Рекурсия n = \" << n << \" , длина nums = \" << nums.size() << endl;\n    return quadraticRecur(n - 1);\n}\n
        space_complexity.java
        /* Квадратичная сложность (рекурсивная реализация) */\nint quadraticRecur(int n) {\n    if (n <= 0)\n        return 0;\n    // Длина массива nums равна n, n-1, ..., 2, 1\n    int[] nums = new int[n];\n    System.out.println(\"В рекурсии n = \" + n + \", длина nums = \" + nums.length);\n    return quadraticRecur(n - 1);\n}\n
        space_complexity.cs
        /* Квадратичная сложность (рекурсивная реализация) */\nint QuadraticRecur(int n) {\n    if (n <= 0) return 0;\n    int[] nums = new int[n];\n    Console.WriteLine(\"В рекурсии n = \" + n + \", длина nums = \" + nums.Length);\n    return QuadraticRecur(n - 1);\n}\n
        space_complexity.go
        /* Квадратичная сложность (рекурсивная реализация) */\nfunc spaceQuadraticRecur(n int) int {\n    if n <= 0 {\n        return 0\n    }\n    nums := make([]int, n)\n    fmt.Printf(\"В рекурсии n = %d, длина nums = %d\\n\", n, len(nums))\n    return spaceQuadraticRecur(n - 1)\n}\n
        space_complexity.swift
        /* Квадратичная сложность (рекурсивная реализация) */\n@discardableResult\nfunc quadraticRecur(n: Int) -> Int {\n    if n <= 0 {\n        return 0\n    }\n    // Длина массива nums равна n, n-1, ..., 2, 1\n    let nums = Array(repeating: 0, count: n)\n    print(\"В рекурсии n = \\(n), длина nums = \\(nums.count)\")\n    return quadraticRecur(n: n - 1)\n}\n
        space_complexity.js
        /* Квадратичная сложность (рекурсивная реализация) */\nfunction quadraticRecur(n) {\n    if (n <= 0) return 0;\n    const nums = new Array(n);\n    console.log(`В рекурсии n = ${n} длина nums = ${nums.length}`);\n    return quadraticRecur(n - 1);\n}\n
        space_complexity.ts
        /* Квадратичная сложность (рекурсивная реализация) */\nfunction quadraticRecur(n: number): number {\n    if (n <= 0) return 0;\n    const nums = new Array(n);\n    console.log(`В рекурсии n = ${n} длина nums = ${nums.length}`);\n    return quadraticRecur(n - 1);\n}\n
        space_complexity.dart
        /* Квадратичная сложность (рекурсивная реализация) */\nint quadraticRecur(int n) {\n  if (n <= 0) return 0;\n  List<int> nums = List.filled(n, 0);\n  print('В рекурсии n = $n длина nums = ${nums.length}');\n  return quadraticRecur(n - 1);\n}\n
        space_complexity.rs
        /* Квадратичная сложность (рекурсивная реализация) */\nfn quadratic_recur(n: i32) -> i32 {\n    if n <= 0 {\n        return 0;\n    };\n    // Длина массива nums равна n, n-1, ..., 2, 1\n    let nums = vec![0; n as usize];\n    println!(\"В рекурсии n = {} , длина nums = {}\", n, nums.len());\n    return quadratic_recur(n - 1);\n}\n
        space_complexity.c
        /* Квадратичная сложность (рекурсивная реализация) */\nint quadraticRecur(int n) {\n    if (n <= 0)\n        return 0;\n    int *nums = malloc(sizeof(int) * n);\n    printf(\"Рекурсия n = %d, длина nums = %d\\r\\n\", n, n);\n    int res = quadraticRecur(n - 1);\n    free(nums);\n    return res;\n}\n
        space_complexity.kt
        /* Квадратичная сложность (рекурсивная реализация) */\ntailrec fun quadraticRecur(n: Int): Int {\n    if (n <= 0)\n        return 0\n    // Длина массива nums равна n, n-1, ..., 2, 1\n    val nums = Array(n) { 0 }\n    println(\"В рекурсии n = $n длина nums = ${nums.size}\")\n    return quadraticRecur(n - 1)\n}\n
        space_complexity.rb
        ### Квадратичная сложность ###\ndef quadratic(n)\n  # Двумерный список занимает O(n^2) памяти\n  Array.new(n) { Array.new(n, 0) }\nend\n\n# ## Квадратичная сложность (рекурсивная реализация) ###\ndef quadratic_recur(n)\n  return 0 unless n > 0\n\n  # Длина массива nums равна n, n-1, ..., 2, 1\n  nums = Array.new(n, 0)\n  quadratic_recur(n - 1)\nend\n
        Визуализация кода

        Во весь экран >

        Рисунок 2-18   Квадратичная пространственная сложность, порождаемая рекурсивной функцией

        ","path":["Глава 2. Анализ сложности","2.4   Пространственная сложность"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#4-o2n","level":3,"title":"4.   Экспоненциальная сложность \\(O(2^n)\\)","text":"

        Экспоненциальная сложность часто встречается у бинарных деревьев. Как видно на рисунке 2-19, полное бинарное дерево с \\(n\\) уровнями содержит \\(2^n - 1\\) узлов и занимает \\(O(2^n)\\) пространства:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
        def build_tree(n: int) -> TreeNode | None:\n    \"\"\"Экспоненциальная сложность (построение полного двоичного дерева)\"\"\"\n    if n == 0:\n        return None\n    root = TreeNode(0)\n    root.left = build_tree(n - 1)\n    root.right = build_tree(n - 1)\n    return root\n
        space_complexity.cpp
        /* Экспоненциальная сложность (построение полного двоичного дерева) */\nTreeNode *buildTree(int n) {\n    if (n == 0)\n        return nullptr;\n    TreeNode *root = new TreeNode(0);\n    root->left = buildTree(n - 1);\n    root->right = buildTree(n - 1);\n    return root;\n}\n
        space_complexity.java
        /* Экспоненциальная сложность (построение полного двоичного дерева) */\nTreeNode buildTree(int n) {\n    if (n == 0)\n        return null;\n    TreeNode root = new TreeNode(0);\n    root.left = buildTree(n - 1);\n    root.right = buildTree(n - 1);\n    return root;\n}\n
        space_complexity.cs
        /* Экспоненциальная сложность (построение полного двоичного дерева) */\nTreeNode? BuildTree(int n) {\n    if (n == 0) return null;\n    TreeNode root = new(0) {\n        left = BuildTree(n - 1),\n        right = BuildTree(n - 1)\n    };\n    return root;\n}\n
        space_complexity.go
        /* Экспоненциальная сложность (построение полного двоичного дерева) */\nfunc buildTree(n int) *TreeNode {\n    if n == 0 {\n        return nil\n    }\n    root := NewTreeNode(0)\n    root.Left = buildTree(n - 1)\n    root.Right = buildTree(n - 1)\n    return root\n}\n
        space_complexity.swift
        /* Экспоненциальная сложность (построение полного двоичного дерева) */\nfunc buildTree(n: Int) -> TreeNode? {\n    if n == 0 {\n        return nil\n    }\n    let root = TreeNode(x: 0)\n    root.left = buildTree(n: n - 1)\n    root.right = buildTree(n: n - 1)\n    return root\n}\n
        space_complexity.js
        /* Экспоненциальная сложность (построение полного двоичного дерева) */\nfunction buildTree(n) {\n    if (n === 0) return null;\n    const root = new TreeNode(0);\n    root.left = buildTree(n - 1);\n    root.right = buildTree(n - 1);\n    return root;\n}\n
        space_complexity.ts
        /* Экспоненциальная сложность (построение полного двоичного дерева) */\nfunction buildTree(n: number): TreeNode | null {\n    if (n === 0) return null;\n    const root = new TreeNode(0);\n    root.left = buildTree(n - 1);\n    root.right = buildTree(n - 1);\n    return root;\n}\n
        space_complexity.dart
        /* Экспоненциальная сложность (построение полного двоичного дерева) */\nTreeNode? buildTree(int n) {\n  if (n == 0) return null;\n  TreeNode root = TreeNode(0);\n  root.left = buildTree(n - 1);\n  root.right = buildTree(n - 1);\n  return root;\n}\n
        space_complexity.rs
        /* Экспоненциальная сложность (построение полного двоичного дерева) */\nfn build_tree(n: i32) -> Option<Rc<RefCell<TreeNode>>> {\n    if n == 0 {\n        return None;\n    };\n    let root = TreeNode::new(0);\n    root.borrow_mut().left = build_tree(n - 1);\n    root.borrow_mut().right = build_tree(n - 1);\n    return Some(root);\n}\n
        space_complexity.c
        /* Экспоненциальная сложность (построение полного двоичного дерева) */\nTreeNode *buildTree(int n) {\n    if (n == 0)\n        return NULL;\n    TreeNode *root = newTreeNode(0);\n    root->left = buildTree(n - 1);\n    root->right = buildTree(n - 1);\n    return root;\n}\n
        space_complexity.kt
        /* Экспоненциальная сложность (построение полного двоичного дерева) */\nfun buildTree(n: Int): TreeNode? {\n    if (n == 0)\n        return null\n    val root = TreeNode(0)\n    root.left = buildTree(n - 1)\n    root.right = buildTree(n - 1)\n    return root\n}\n
        space_complexity.rb
        ### Квадратичная сложность ###\ndef quadratic(n)\n  # Двумерный список занимает O(n^2) памяти\n  Array.new(n) { Array.new(n, 0) }\nend\n\n# ## Квадратичная сложность (рекурсивная реализация) ###\ndef quadratic_recur(n)\n  return 0 unless n > 0\n\n  # Длина массива nums равна n, n-1, ..., 2, 1\n  nums = Array.new(n, 0)\n  quadratic_recur(n - 1)\nend\n\n# ## Экспоненциальная сложность (построение полного двоичного дерева) ###\ndef build_tree(n)\n  return if n == 0\n\n  TreeNode.new.tap do |root|\n    root.left = build_tree(n - 1)\n    root.right = build_tree(n - 1)\n  end\nend\n
        Визуализация кода

        Во весь экран >

        Рисунок 2-19   Экспоненциальная пространственная сложность, порождаемая полным бинарным деревом

        ","path":["Глава 2. Анализ сложности","2.4   Пространственная сложность"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#5-olog-n","level":3,"title":"5.   Логарифмическая сложность \\(O(\\log n)\\)","text":"

        Логарифмическая сложность часто встречается в алгоритмах «разделяй и властвуй». Например, при сортировке слиянием входной массив длины \\(n\\) на каждом шаге рекурсии делится пополам, образуя рекурсивное дерево высоты \\(\\log n\\) и используя \\(O(\\log n)\\) пространства кадров стека.

        Еще один пример - преобразование числа в строку. Если задано положительное целое число \\(n\\) , то количество его цифр равно \\(\\lfloor \\log_{10} n \\rfloor + 1\\) , то есть длина соответствующей строки тоже равна \\(\\lfloor \\log_{10} n \\rfloor + 1\\) , следовательно, пространственная сложность составляет \\(O(\\log_{10} n + 1) = O(\\log n)\\) .

        ","path":["Глава 2. Анализ сложности","2.4   Пространственная сложность"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#244","level":2,"title":"2.4.4   Компромисс между временем и пространством","text":"

        В идеальных условиях хотелось бы, чтобы и временная, и пространственная сложность алгоритма были оптимальными. Однако на практике одновременно оптимизировать и время, и память обычно очень трудно.

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

        Выбор между этими двумя идеями зависит от того, что важнее в конкретной задаче. В большинстве случаев время ценнее памяти, поэтому стратегия обмена пространства на время используется чаще. Но при очень больших объемах данных контроль пространственной сложности тоже становится крайне важным.

        ","path":["Глава 2. Анализ сложности","2.4   Пространственная сложность"],"tags":[]},{"location":"chapter_computational_complexity/summary/","level":1,"title":"2.5   Резюме","text":"","path":["Глава 2. Анализ сложности","2.5   Резюме"],"tags":[]},{"location":"chapter_computational_complexity/summary/#1","level":3,"title":"1.   Ключевые выводы","text":"

        Оценка эффективности алгоритмов

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

        Временная сложность

        • Временная сложность используется для оценки тенденции изменения времени выполнения алгоритма с увеличением объема данных, что позволяет оценивать его эффективность. Однако в некоторых случаях она может работать не так хорошо, например когда объем входных данных мал или временная сложность одинакова, что не позволяет точно сравнить эффективность алгоритмов.
        • Худшая временная сложность обозначается символом Big \\(O\\) и соответствует асимптотической верхней границе, отражая уровень роста количества операций \\(T(n)\\) при стремлении \\(n\\) к бесконечности.
        • Определение временной сложности включает два этапа: сначала подсчитывается количество операций, затем определяется асимптотическая верхняя граница.
        • Наиболее распространенные временные сложности в порядке возрастания: \\(O(1)\\), \\(O(\\log n)\\), \\(O(n)\\), \\(O(n \\log n)\\), \\(O(n^2)\\), \\(O(2^n)\\) и \\(O(n!)\\).
        • Временная сложность некоторых алгоритмов не является фиксированной и зависит от распределения входных данных. Временная сложность делится на худшую, лучшую и среднюю. Лучшая временная сложность почти не используется, так как для достижения лучшего случая входные данные должны соответствовать строгим критериям.
        • Средняя временная сложность отражает эффективность алгоритма при случайных входных данных и наиболее близка к реальной производительности алгоритма. Для расчета средней временной сложности необходимо учитывать распределение входных данных и математическое ожидание.

        Пространственная сложность

        • Пространственная сложность аналогична временной сложности и используется для оценки тенденции изменения объема памяти, занимаемой алгоритмом, с увеличением объема данных.
        • Память, используемую в процессе выполнения алгоритма, можно разделить на входное пространство, временное пространство и выходное пространство. Обычно при расчете пространственной сложности входное пространство не учитывается. Временное пространство делится на временные данные, пространство стека и пространство инструкций, причем пространство стека обычно влияет на сложность только в рекурсивных функциях.
        • Обычно рассматривается только худшая пространственная сложность, то есть пространственная сложность алгоритма при худших входных данных и в худший момент выполнения.
        • Наиболее распространенные пространственные сложности в порядке возрастания: \\(O(1)\\), \\(O(\\log n)\\), \\(O(n)\\), \\(O(n^2)\\) и \\(O(2^n)\\).
        ","path":["Глава 2. Анализ сложности","2.5   Резюме"],"tags":[]},{"location":"chapter_computational_complexity/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

        Q: Является ли пространственная сложность хвостовой рекурсии равной \\(O(1)\\)?

        Теоретически пространственную сложность хвостово-рекурсивных функций можно оптимизировать до \\(O(1)\\) . Однако большинство языков программирования (например Java, Python, C++, Go, C# и другие) не поддерживают автоматическую оптимизацию хвостовой рекурсии, поэтому на практике пространственная сложность обычно считается равной \\(O(n)\\) .

        Q: В чем разница между терминами function и method?

        Функция (function) может выполняться независимо, и все ее параметры передаются явно. Метод (method) связан с объектом, неявно получает объект, который его вызывает, и может работать с данными, содержащимися в экземпляре класса.

        Ниже это проиллюстрировано на примере нескольких распространенных языков программирования.

        • C - процедурный язык программирования без объектно-ориентированной модели, поэтому в нем есть только функции. Однако мы можем имитировать объектно-ориентированное программирование через структуры (struct), и функции, связанные со структурами, эквивалентны методам в других языках.
        • Java и C# - объектно-ориентированные языки программирования, в которых блоки кода (методы) обычно являются частью класса. Статические методы по поведению похожи на функции, потому что они привязаны к классу и не могут обращаться к конкретным переменным экземпляра.
        • C++ и Python поддерживают как процедурное программирование (функции), так и объектно-ориентированное программирование (методы).

        Q: Отражает ли диаграмма «распространенных типов пространственной сложности» абсолютный размер занятой памяти?

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

        Если взять \\(n = 8\\) , можно заметить, что значения на кривых не совпадают напрямую с соответствующими функциями. Это связано с тем, что каждая кривая содержит константный член, который сжимает диапазон значений до визуально удобного масштаба.

        На практике, поскольку мы обычно не знаем, какова «константная» сложность каждого метода, только по сложности мы, как правило, не можем выбрать оптимальное решение для случая \\(n = 8\\) . Но для \\(n = 8^5\\) выбор уже очевиден: в этой области доминирует именно тенденция роста.

        Q: Бывают ли случаи, когда в реальных сценариях алгоритм специально проектируют так, чтобы жертвовать временем ради пространства или пространством ради времени?

        На практике в большинстве случаев выбирают обмен пространства на время. Например, для индексов в базах данных обычно строят B+ деревья или хеш-индексы, расходуя значительный объем памяти ради эффективных запросов уровня \\(O(\\log n)\\) или даже \\(O(1)\\).

        В сценариях, где память особенно дорога, наоборот, могут жертвовать временем ради пространства. Например, в embedded-разработке память устройства очень ограничена, поэтому инженеры могут отказаться от хеш-таблиц и выбрать последовательный поиск по массиву, экономя память ценой более медленного поиска.

        ","path":["Глава 2. Анализ сложности","2.5   Резюме"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/","level":1,"title":"2.3   Временная сложность","text":"

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

        1. Определить платформу выполнения, включая конфигурацию оборудования, язык программирования, системную среду и т.д., поскольку все эти факторы влияют на эффективность выполнения кода.
        2. Оценить время выполнения различных вычислительных операций, например операция сложения + требует 1 нс , операция умножения * требует 10 нс , операция вывода print() требует 5 нс и т.д.
        3. Подсчитать все вычислительные операции в коде и суммировать время выполнения всех операций, чтобы получить общее время работы.

        Например, в следующем коде размер входных данных равен \\(n\\) :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
        # На некоторой платформе выполнения\ndef algorithm(n: int):\n    a = 2      # 1 нс\n    a = a + 1  # 1 нс\n    a = a * 2  # 10 нс\n    # Цикл выполняется n раз\n    for _ in range(n):  # 1 нс\n        print(0)        # 5 нс\n
        // На некоторой платформе выполнения\nvoid algorithm(int n) {\n    int a = 2;  // 1 нс\n    a = a + 1;  // 1 нс\n    a = a * 2;  // 10 нс\n    // Цикл выполняется n раз\n    for (int i = 0; i < n; i++) {  // 1 нс\n        cout << 0 << endl;         // 5 нс\n    }\n}\n
        // На некоторой платформе выполнения\nvoid algorithm(int n) {\n    int a = 2;  // 1 нс\n    a = a + 1;  // 1 нс\n    a = a * 2;  // 10 нс\n    // Цикл выполняется n раз\n    for (int i = 0; i < n; i++) {  // 1 нс\n        System.out.println(0);     // 5 нс\n    }\n}\n
        // На некоторой платформе выполнения\nvoid Algorithm(int n) {\n    int a = 2;  // 1 нс\n    a = a + 1;  // 1 нс\n    a = a * 2;  // 10 нс\n    // Цикл выполняется n раз\n    for (int i = 0; i < n; i++) {  // 1 нс\n        Console.WriteLine(0);      // 5 нс\n    }\n}\n
        // На некоторой платформе выполнения\nfunc algorithm(n int) {\n    a := 2     // 1 нс\n    a = a + 1  // 1 нс\n    a = a * 2  // 10 нс\n    // Цикл выполняется n раз\n    for i := 0; i < n; i++ {  // 1 нс\n        fmt.Println(a)        // 5 нс\n    }\n}\n
        // На некоторой платформе выполнения\nfunc algorithm(n: Int) {\n    var a = 2 // 1 нс\n    a = a + 1 // 1 нс\n    a = a * 2 // 10 нс\n    // Цикл выполняется n раз\n    for _ in 0 ..< n { // 1 нс\n        print(0) // 5 нс\n    }\n}\n
        // На некоторой платформе выполнения\nfunction algorithm(n) {\n    var a = 2; // 1 нс\n    a = a + 1; // 1 нс\n    a = a * 2; // 10 нс\n    // Цикл выполняется n раз\n    for(let i = 0; i < n; i++) { // 1 нс\n        console.log(0); // 5 нс\n    }\n}\n
        // На некоторой платформе выполнения\nfunction algorithm(n: number): void {\n    var a: number = 2; // 1 нс\n    a = a + 1; // 1 нс\n    a = a * 2; // 10 нс\n    // Цикл выполняется n раз\n    for(let i = 0; i < n; i++) { // 1 нс\n        console.log(0); // 5 нс\n    }\n}\n
        // На некоторой платформе выполнения\nvoid algorithm(int n) {\n  int a = 2; // 1 нс\n  a = a + 1; // 1 нс\n  a = a * 2; // 10 нс\n  // Цикл выполняется n раз\n  for (int i = 0; i < n; i++) { // 1 нс\n    print(0); // 5 нс\n  }\n}\n
        // На некоторой платформе выполнения\nfn algorithm(n: i32) {\n    let mut a = 2;      // 1 нс\n    a = a + 1;          // 1 нс\n    a = a * 2;          // 10 нс\n    // Цикл выполняется n раз\n    for _ in 0..n {     // 1 нс\n        println!(\"{}\", 0);  // 5 нс\n    }\n}\n
        // На некоторой платформе выполнения\nvoid algorithm(int n) {\n    int a = 2;  // 1 нс\n    a = a + 1;  // 1 нс\n    a = a * 2;  // 10 нс\n    // Цикл выполняется n раз\n    for (int i = 0; i < n; i++) {   // 1 нс\n        printf(\"%d\", 0);            // 5 нс\n    }\n}\n
        // На некоторой платформе выполнения\nfun algorithm(n: Int) {\n    var a = 2 // 1 нс\n    a = a + 1 // 1 нс\n    a = a * 2 // 10 нс\n    // Цикл выполняется n раз\n    for (i in 0..<n) {  // 1 нс\n        println(0)      // 5 нс\n    }\n}\n
        # На некоторой платформе выполнения\ndef algorithm(n)\n    a = 2       # 1 нс\n    a = a + 1   # 1 нс\n    a = a * 2   # 10 нс\n    # Цикл выполняется n раз\n    (0...n).each do # 1 нс\n        puts 0      # 5 нс\n    end\nend\n

        Согласно приведенному выше методу, время работы алгоритма равно \\((6n + 12)\\) нс :

        \\[ 1 + 1 + 10 + (1 + 5) \\times n = 6n + 12 \\]

        Но на практике подсчитывать реальное время выполнения алгоритма и неразумно, и нереалистично. Во-первых, мы не хотим привязывать оценку времени к конкретной платформе, потому что алгоритм должен запускаться на самых разных платформах. Во-вторых, нам трудно определить время выполнения каждого типа операций, а это делает точную оценку крайне затруднительной.

        ","path":["Глава 2. Анализ сложности","2.3   Временная сложность"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#231","level":2,"title":"2.3.1   Подсчет тенденции роста времени","text":"

        Анализ временной сложности оценивает не само время выполнения алгоритма, а тенденцию роста этого времени по мере увеличения объема данных.

        Понятие «тенденции роста времени» выглядит довольно абстрактным, поэтому разберем его на примере. Предположим, размер входных данных равен \\(n\\) , и даны три алгоритма A , B и C :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
        # Временная сложность алгоритма A: постоянная\ndef algorithm_A(n: int):\n    print(0)\n# Временная сложность алгоритма B: линейная\ndef algorithm_B(n: int):\n    for _ in range(n):\n        print(0)\n# Временная сложность алгоритма C: постоянная\ndef algorithm_C(n: int):\n    for _ in range(1000000):\n        print(0)\n
        // Временная сложность алгоритма A: постоянная\nvoid algorithm_A(int n) {\n    cout << 0 << endl;\n}\n// Временная сложность алгоритма B: линейная\nvoid algorithm_B(int n) {\n    for (int i = 0; i < n; i++) {\n        cout << 0 << endl;\n    }\n}\n// Временная сложность алгоритма C: постоянная\nvoid algorithm_C(int n) {\n    for (int i = 0; i < 1000000; i++) {\n        cout << 0 << endl;\n    }\n}\n
        // Временная сложность алгоритма A: постоянная\nvoid algorithm_A(int n) {\n    System.out.println(0);\n}\n// Временная сложность алгоритма B: линейная\nvoid algorithm_B(int n) {\n    for (int i = 0; i < n; i++) {\n        System.out.println(0);\n    }\n}\n// Временная сложность алгоритма C: постоянная\nvoid algorithm_C(int n) {\n    for (int i = 0; i < 1000000; i++) {\n        System.out.println(0);\n    }\n}\n
        // Временная сложность алгоритма A: постоянная\nvoid AlgorithmA(int n) {\n    Console.WriteLine(0);\n}\n// Временная сложность алгоритма B: линейная\nvoid AlgorithmB(int n) {\n    for (int i = 0; i < n; i++) {\n        Console.WriteLine(0);\n    }\n}\n// Временная сложность алгоритма C: постоянная\nvoid AlgorithmC(int n) {\n    for (int i = 0; i < 1000000; i++) {\n        Console.WriteLine(0);\n    }\n}\n
        // Временная сложность алгоритма A: постоянная\nfunc algorithm_A(n int) {\n    fmt.Println(0)\n}\n// Временная сложность алгоритма B: линейная\nfunc algorithm_B(n int) {\n    for i := 0; i < n; i++ {\n        fmt.Println(0)\n    }\n}\n// Временная сложность алгоритма C: постоянная\nfunc algorithm_C(n int) {\n    for i := 0; i < 1000000; i++ {\n        fmt.Println(0)\n    }\n}\n
        // Временная сложность алгоритма A: постоянная\nfunc algorithmA(n: Int) {\n    print(0)\n}\n\n// Временная сложность алгоритма B: линейная\nfunc algorithmB(n: Int) {\n    for _ in 0 ..< n {\n        print(0)\n    }\n}\n\n// Временная сложность алгоритма C: постоянная\nfunc algorithmC(n: Int) {\n    for _ in 0 ..< 1_000_000 {\n        print(0)\n    }\n}\n
        // Временная сложность алгоритма A: постоянная\nfunction algorithm_A(n) {\n    console.log(0);\n}\n// Временная сложность алгоритма B: линейная\nfunction algorithm_B(n) {\n    for (let i = 0; i < n; i++) {\n        console.log(0);\n    }\n}\n// Временная сложность алгоритма C: постоянная\nfunction algorithm_C(n) {\n    for (let i = 0; i < 1000000; i++) {\n        console.log(0);\n    }\n}\n
        // Временная сложность алгоритма A: постоянная\nfunction algorithm_A(n: number): void {\n    console.log(0);\n}\n// Временная сложность алгоритма B: линейная\nfunction algorithm_B(n: number): void {\n    for (let i = 0; i < n; i++) {\n        console.log(0);\n    }\n}\n// Временная сложность алгоритма C: постоянная\nfunction algorithm_C(n: number): void {\n    for (let i = 0; i < 1000000; i++) {\n        console.log(0);\n    }\n}\n
        // Временная сложность алгоритма A: постоянная\nvoid algorithmA(int n) {\n  print(0);\n}\n// Временная сложность алгоритма B: линейная\nvoid algorithmB(int n) {\n  for (int i = 0; i < n; i++) {\n    print(0);\n  }\n}\n// Временная сложность алгоритма C: постоянная\nvoid algorithmC(int n) {\n  for (int i = 0; i < 1000000; i++) {\n    print(0);\n  }\n}\n
        // Временная сложность алгоритма A: постоянная\nfn algorithm_A(n: i32) {\n    println!(\"{}\", 0);\n}\n// Временная сложность алгоритма B: линейная\nfn algorithm_B(n: i32) {\n    for _ in 0..n {\n        println!(\"{}\", 0);\n    }\n}\n// Временная сложность алгоритма C: постоянная\nfn algorithm_C(n: i32) {\n    for _ in 0..1000000 {\n        println!(\"{}\", 0);\n    }\n}\n
        // Временная сложность алгоритма A: постоянная\nvoid algorithm_A(int n) {\n    printf(\"%d\", 0);\n}\n// Временная сложность алгоритма B: линейная\nvoid algorithm_B(int n) {\n    for (int i = 0; i < n; i++) {\n        printf(\"%d\", 0);\n    }\n}\n// Временная сложность алгоритма C: постоянная\nvoid algorithm_C(int n) {\n    for (int i = 0; i < 1000000; i++) {\n        printf(\"%d\", 0);\n    }\n}\n
        // Временная сложность алгоритма A: постоянная\nfun algoritm_A(n: Int) {\n    println(0)\n}\n// Временная сложность алгоритма B: линейная\nfun algorithm_B(n: Int) {\n    for (i in 0..<n){\n        println(0)\n    }\n}\n// Временная сложность алгоритма C: постоянная\nfun algorithm_C(n: Int) {\n    for (i in 0..<1000000) {\n        println(0)\n    }\n}\n
        # Временная сложность алгоритма A: постоянная\ndef algorithm_A(n)\n    puts 0\nend\n\n# Временная сложность алгоритма B: линейная\ndef algorithm_B(n)\n    (0...n).each { puts 0 }\nend\n\n# Временная сложность алгоритма C: постоянная\ndef algorithm_C(n)\n    (0...1_000_000).each { puts 0 }\nend\n

        На рисунке 2-7 показаны временные сложности трех приведенных выше функций.

        • У алгоритма A есть только одна операция вывода, и время его работы не растет с увеличением \\(n\\) . Такую временную сложность называют постоянной.
        • В алгоритме B операция вывода выполняется в цикле \\(n\\) раз, поэтому время работы растет линейно по мере увеличения \\(n\\) . Такая временная сложность называется линейной.
        • В алгоритме C операция вывода выполняется \\(1000000\\) раз. Хотя время работы велико, оно не зависит от размера входных данных \\(n\\) . Поэтому временная сложность C такая же, как у A , и тоже является постоянной.

        Рисунок 2-7   Тенденции роста времени для алгоритмов A, B и C

        Какие особенности имеет анализ временной сложности по сравнению с непосредственным измерением времени работы алгоритма?

        • Временная сложность позволяет эффективно оценивать эффективность алгоритма. Например, время работы алгоритма B растет линейно: при \\(n > 1\\) он медленнее алгоритма A , а при \\(n > 1000000\\) медленнее алгоритма C . Если размер входных данных достаточно велик, алгоритм с постоянной сложностью обязательно лучше алгоритма с линейной сложностью. В этом и состоит смысл тенденции роста времени.
        • Метод вывода временной сложности проще. Платформа выполнения и тип вычислительных операций не влияют на тенденцию роста времени работы алгоритма. Поэтому в анализе временной сложности можно считать время выполнения всех вычислительных операций одинаковым единичным временем и тем самым упростить подсчет времени выполнения до подсчета количества операций.
        • У временной сложности есть и определенные ограничения. Например, хотя временная сложность алгоритмов A и C одинакова, их реальное время выполнения сильно различается. Точно так же, хотя временная сложность B выше, чем у C , при малых \\(n\\) алгоритм B очевидно лучше C . Несмотря на эти ограничения, анализ сложности все равно остается самым эффективным и самым распространенным способом оценки алгоритмов.
        ","path":["Глава 2. Анализ сложности","2.3   Временная сложность"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#232","level":2,"title":"2.3.2   Асимптотическая верхняя граница функции","text":"

        Для функции с входным размером \\(n\\) :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
        def algorithm(n: int):\n    a = 1      # +1\n    a = a + 1  # +1\n    a = a * 2  # +1\n    # Цикл выполняется n раз\n    for i in range(n):  # +1\n        print(0)        # +1\n
        void algorithm(int n) {\n    int a = 1;  // +1\n    a = a + 1;  // +1\n    a = a * 2;  // +1\n    // Цикл выполняется n раз\n    for (int i = 0; i < n; i++) { // +1 (каждый раз выполняется i ++)\n        cout << 0 << endl;    // +1\n    }\n}\n
        void algorithm(int n) {\n    int a = 1;  // +1\n    a = a + 1;  // +1\n    a = a * 2;  // +1\n    // Цикл выполняется n раз\n    for (int i = 0; i < n; i++) { // +1 (каждый раз выполняется i ++)\n        System.out.println(0);    // +1\n    }\n}\n
        void Algorithm(int n) {\n    int a = 1;  // +1\n    a = a + 1;  // +1\n    a = a * 2;  // +1\n    // Цикл выполняется n раз\n    for (int i = 0; i < n; i++) {   // +1 (каждый раз выполняется i ++)\n        Console.WriteLine(0);   // +1\n    }\n}\n
        func algorithm(n int) {\n    a := 1      // +1\n    a = a + 1   // +1\n    a = a * 2   // +1\n    // Цикл выполняется n раз\n    for i := 0; i < n; i++ {   // +1\n        fmt.Println(a)         // +1\n    }\n}\n
        func algorithm(n: Int) {\n    var a = 1 // +1\n    a = a + 1 // +1\n    a = a * 2 // +1\n    // Цикл выполняется n раз\n    for _ in 0 ..< n { // +1\n        print(0) // +1\n    }\n}\n
        function algorithm(n) {\n    var a = 1; // +1\n    a += 1; // +1\n    a *= 2; // +1\n    // Цикл выполняется n раз\n    for(let i = 0; i < n; i++){ // +1 (каждый раз выполняется i ++)\n        console.log(0); // +1\n    }\n}\n
        function algorithm(n: number): void{\n    var a: number = 1; // +1\n    a += 1; // +1\n    a *= 2; // +1\n    // Цикл выполняется n раз\n    for(let i = 0; i < n; i++){ // +1 (каждый раз выполняется i ++)\n        console.log(0); // +1\n    }\n}\n
        void algorithm(int n) {\n  int a = 1; // +1\n  a = a + 1; // +1\n  a = a * 2; // +1\n  // Цикл выполняется n раз\n  for (int i = 0; i < n; i++) { // +1 (каждый раз выполняется i ++)\n    print(0); // +1\n  }\n}\n
        fn algorithm(n: i32) {\n    let mut a = 1;   // +1\n    a = a + 1;      // +1\n    a = a * 2;      // +1\n\n    // Цикл выполняется n раз\n    for _ in 0..n { // +1 (каждый раз выполняется i ++)\n        println!(\"{}\", 0); // +1\n    }\n}\n
        void algorithm(int n) {\n    int a = 1;  // +1\n    a = a + 1;  // +1\n    a = a * 2;  // +1\n    // Цикл выполняется n раз\n    for (int i = 0; i < n; i++) {   // +1 (каждый раз выполняется i ++)\n        printf(\"%d\", 0);            // +1\n    }\n}\n
        fun algorithm(n: Int) {\n    var a = 1 // +1\n    a = a + 1 // +1\n    a = a * 2 // +1\n    // Цикл выполняется n раз\n    for (i in 0..<n) { // +1 (каждый раз выполняется i ++)\n        println(0) // +1\n    }\n}\n
        def algorithm(n)\n    a = 1       # +1\n    a = a + 1   # +1\n    a = a * 2   # +1\n    # Цикл выполняется n раз\n    (0...n).each do # +1\n        puts 0      # +1\n    end\nend\n

        Пусть количество операций алгоритма является функцией от размера входных данных \\(n\\) и обозначается как \\(T(n)\\). Тогда для приведенной выше функции число операций равно:

        \\[ T(n) = 3 + 2n \\]

        \\(T(n)\\) - линейная функция, а это означает, что тенденция роста времени работы линейна, следовательно, временная сложность здесь тоже линейна.

        Линейную временную сложность записывают как \\(O(n)\\). Этот математический символ называется нотацией Big \\(O\\) (big-\\(O\\) notation) и обозначает асимптотическую верхнюю границу (asymptotic upper bound) функции \\(T(n)\\) .

        Иными словами, анализ временной сложности сводится к определению асимптотической верхней границы числа операций \\(T(n)\\), и у этого понятия есть строгое математическое определение.

        Асимптотическая верхняя граница функции

        Если существуют положительное действительное число \\(c\\) и действительное число \\(n_0\\) , такие что для всех \\(n > n_0\\) выполняется \\(T(n) \\leq c \\cdot f(n)\\) , то можно считать, что \\(f(n)\\) задает асимптотическую верхнюю границу для \\(T(n)\\). Это записывается как \\(T(n) = O(f(n))\\) .

        Как показано на рисунке 2-8, вычислить асимптотическую верхнюю границу - значит найти такую функцию \\(f(n)\\) , что при стремлении \\(n\\) к бесконечности функции \\(T(n)\\) и \\(f(n)\\) имеют один и тот же порядок роста и отличаются только постоянным коэффициентом \\(c\\).

        Рисунок 2-8   Асимптотическая верхняя граница функции

        ","path":["Глава 2. Анализ сложности","2.3   Временная сложность"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#233","level":2,"title":"2.3.3   Метод вывода","text":"

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

        Согласно определению, после того как мы определили \\(f(n)\\) , можно получить временную сложность \\(O(f(n))\\) . Но как определить саму асимптотическую верхнюю границу \\(f(n)\\) ? В целом процесс состоит из двух шагов: сначала подсчитать количество операций, затем определить асимптотическую верхнюю границу.

        ","path":["Глава 2. Анализ сложности","2.3   Временная сложность"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#1-1","level":3,"title":"1.   Шаг 1: подсчет количества операций","text":"

        Для кода это можно делать построчно сверху вниз. Однако, поскольку в выражении \\(c \\cdot f(n)\\) постоянный коэффициент \\(c\\) может быть сколь угодно большим, различные коэффициенты и постоянные члены в числе операций \\(T(n)\\) можно игнорировать. Исходя из этого принципа, можно сформулировать следующие упрощающие приемы подсчета.

        1. Игнорировать константы в \\(T(n)\\). Они не зависят от \\(n\\) , а значит не влияют на временную сложность.
        2. Опускать все коэффициенты. Например, циклы на \\(2n\\) раз или \\(5n + 1\\) раз можно упростить до \\(n\\) раз, потому что коэффициент перед \\(n\\) не влияет на временную сложность.
        3. При вложенных циклах использовать умножение. Общее число операций равно произведению числа операций внешнего и внутреннего циклов. При этом для каждого уровня цикла по-прежнему можно применять приемы из пунктов 1. и 2. .

        Для заданной функции мы можем использовать перечисленные выше приемы и подсчитать число операций:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
        def algorithm(n: int):\n    a = 1      # +0 (прием 1)\n    a = a + n  # +0 (прием 1)\n    # +n (прием 2)\n    for i in range(5 * n + 1):\n        print(0)\n    # +n*n (прием 3)\n    for i in range(2 * n):\n        for j in range(n + 1):\n            print(0)\n
        void algorithm(int n) {\n    int a = 1;  // +0 (прием 1)\n    a = a + n;  // +0 (прием 1)\n    // +n (прием 2)\n    for (int i = 0; i < 5 * n + 1; i++) {\n        cout << 0 << endl;\n    }\n    // +n*n (прием 3)\n    for (int i = 0; i < 2 * n; i++) {\n        for (int j = 0; j < n + 1; j++) {\n            cout << 0 << endl;\n        }\n    }\n}\n
        void algorithm(int n) {\n    int a = 1;  // +0 (прием 1)\n    a = a + n;  // +0 (прием 1)\n    // +n (прием 2)\n    for (int i = 0; i < 5 * n + 1; i++) {\n        System.out.println(0);\n    }\n    // +n*n (прием 3)\n    for (int i = 0; i < 2 * n; i++) {\n        for (int j = 0; j < n + 1; j++) {\n            System.out.println(0);\n        }\n    }\n}\n
        void Algorithm(int n) {\n    int a = 1;  // +0 (прием 1)\n    a = a + n;  // +0 (прием 1)\n    // +n (прием 2)\n    for (int i = 0; i < 5 * n + 1; i++) {\n        Console.WriteLine(0);\n    }\n    // +n*n (прием 3)\n    for (int i = 0; i < 2 * n; i++) {\n        for (int j = 0; j < n + 1; j++) {\n            Console.WriteLine(0);\n        }\n    }\n}\n
        func algorithm(n int) {\n    a := 1     // +0 (прием 1)\n    a = a + n  // +0 (прием 1)\n    // +n (прием 2)\n    for i := 0; i < 5 * n + 1; i++ {\n        fmt.Println(0)\n    }\n    // +n*n (прием 3)\n    for i := 0; i < 2 * n; i++ {\n        for j := 0; j < n + 1; j++ {\n            fmt.Println(0)\n        }\n    }\n}\n
        func algorithm(n: Int) {\n    var a = 1 // +0 (прием 1)\n    a = a + n // +0 (прием 1)\n    // +n (прием 2)\n    for _ in 0 ..< (5 * n + 1) {\n        print(0)\n    }\n    // +n*n (прием 3)\n    for _ in 0 ..< (2 * n) {\n        for _ in 0 ..< (n + 1) {\n            print(0)\n        }\n    }\n}\n
        function algorithm(n) {\n    let a = 1;  // +0 (прием 1)\n    a = a + n;  // +0 (прием 1)\n    // +n (прием 2)\n    for (let i = 0; i < 5 * n + 1; i++) {\n        console.log(0);\n    }\n    // +n*n (прием 3)\n    for (let i = 0; i < 2 * n; i++) {\n        for (let j = 0; j < n + 1; j++) {\n            console.log(0);\n        }\n    }\n}\n
        function algorithm(n: number): void {\n    let a = 1;  // +0 (прием 1)\n    a = a + n;  // +0 (прием 1)\n    // +n (прием 2)\n    for (let i = 0; i < 5 * n + 1; i++) {\n        console.log(0);\n    }\n    // +n*n (прием 3)\n    for (let i = 0; i < 2 * n; i++) {\n        for (let j = 0; j < n + 1; j++) {\n            console.log(0);\n        }\n    }\n}\n
        void algorithm(int n) {\n  int a = 1; // +0 (прием 1)\n  a = a + n; // +0 (прием 1)\n  // +n (прием 2)\n  for (int i = 0; i < 5 * n + 1; i++) {\n    print(0);\n  }\n  // +n*n (прием 3)\n  for (int i = 0; i < 2 * n; i++) {\n    for (int j = 0; j < n + 1; j++) {\n      print(0);\n    }\n  }\n}\n
        fn algorithm(n: i32) {\n    let mut a = 1;     // +0 (прием 1)\n    a = a + n;        // +0 (прием 1)\n\n    // +n (прием 2)\n    for i in 0..(5 * n + 1) {\n        println!(\"{}\", 0);\n    }\n\n    // +n*n (прием 3)\n    for i in 0..(2 * n) {\n        for j in 0..(n + 1) {\n            println!(\"{}\", 0);\n        }\n    }\n}\n
        void algorithm(int n) {\n    int a = 1;  // +0 (прием 1)\n    a = a + n;  // +0 (прием 1)\n    // +n (прием 2)\n    for (int i = 0; i < 5 * n + 1; i++) {\n        printf(\"%d\", 0);\n    }\n    // +n*n (прием 3)\n    for (int i = 0; i < 2 * n; i++) {\n        for (int j = 0; j < n + 1; j++) {\n            printf(\"%d\", 0);\n        }\n    }\n}\n
        fun algorithm(n: Int) {\n    var a = 1   // +0 (прием 1)\n    a = a + n   // +0 (прием 1)\n    // +n (прием 2)\n    for (i in 0..<5 * n + 1) {\n        println(0)\n    }\n    // +n*n (прием 3)\n    for (i in 0..<2 * n) {\n        for (j in 0..<n + 1) {\n            println(0)\n        }\n    }\n}\n
        def algorithm(n)\n    a = 1       # +0 (прием 1)\n    a = a + n   # +0 (прием 1)\n    # +n (прием 2)\n    (0...(5 * n + 1)).each do { puts 0 }\n    # +n*n (прием 3)\n    (0...(2 * n)).each do\n        (0...(n + 1)).each do { puts 0 }\n    end\nend\n

        Следующая формула показывает результаты подсчета до и после использования перечисленных выше приемов. В обоих случаях выводимая временная сложность равна \\(O(n^2)\\) .

        \\[ \\begin{aligned} T(n) & = 2n(n + 1) + (5n + 1) + 2 & \\text{полный подсчет (-.-|||)} \\newline & = 2n^2 + 7n + 3 \\newline T(n) & = n^2 + n & \\text{ленивый подсчет (o.O)} \\end{aligned} \\]","path":["Глава 2. Анализ сложности","2.3   Временная сложность"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#2-2","level":3,"title":"2.   Шаг 2: определение асимптотической верхней границы","text":"

        **Временная сложность определяется старшим по степени членом в \\(T(n)\\) **. Это связано с тем, что при стремлении \\(n\\) к бесконечности именно старший член начинает доминировать, а влиянием остальных членов можно пренебречь.

        В таблице 2-2 приведены несколько примеров. Некоторые значения специально сделаны преувеличенными, чтобы подчеркнуть вывод: коэффициент не способен изменить порядок. Когда \\(n\\) стремится к бесконечности, эти константы становятся несущественными.

        Таблица 2-2   Временная сложность, соответствующая разному количеству операций

        Число операций \\(T(n)\\) Временная сложность \\(O(f(n))\\) \\(100000\\) \\(O(1)\\) \\(3n + 2\\) \\(O(n)\\) \\(2n^2 + 3n + 2\\) \\(O(n^2)\\) \\(n^3 + 10000n^2\\) \\(O(n^3)\\) \\(2^n + 10000n^{10000}\\) \\(O(2^n)\\)","path":["Глава 2. Анализ сложности","2.3   Временная сложность"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#234","level":2,"title":"2.3.4   Распространенные типы","text":"

        Пусть размер входных данных равен \\(n\\). Распространенные типы временной сложности показаны на рисунке 2-9 в порядке от меньшей к большей.

        \\[ \\begin{aligned} & O(1) < O(\\log n) < O(n) < O(n \\log n) < O(n^2) < O(2^n) < O(n!) \\newline & \\text{Постоянная} < \\text{Логарифмическая} < \\text{Линейная} < \\text{Линейно-логарифмическая} < \\text{Квадратичная} < \\text{Экспоненциальная} < \\text{Факториальная} \\end{aligned} \\]

        Рисунок 2-9   Распространенные типы временной сложности

        ","path":["Глава 2. Анализ сложности","2.3   Временная сложность"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#1-o1","level":3,"title":"1.   Постоянная сложность \\(O(1)\\)","text":"

        Число операций при постоянной сложности не зависит от размера входных данных \\(n\\) , то есть не изменяется вместе с изменением \\(n\\) .

        В следующей функции, хотя число операций size может быть большим, оно не зависит от размера входных данных \\(n\\) , поэтому временная сложность по-прежнему равна \\(O(1)\\) :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
        def constant(n: int) -> int:\n    \"\"\"Постоянная сложность\"\"\"\n    count = 0\n    size = 100000\n    for _ in range(size):\n        count += 1\n    return count\n
        time_complexity.cpp
        /* Постоянная сложность */\nint constant(int n) {\n    int count = 0;\n    int size = 100000;\n    for (int i = 0; i < size; i++)\n        count++;\n    return count;\n}\n
        time_complexity.java
        /* Постоянная сложность */\nint constant(int n) {\n    int count = 0;\n    int size = 100000;\n    for (int i = 0; i < size; i++)\n        count++;\n    return count;\n}\n
        time_complexity.cs
        /* Постоянная сложность */\nint Constant(int n) {\n    int count = 0;\n    int size = 100000;\n    for (int i = 0; i < size; i++)\n        count++;\n    return count;\n}\n
        time_complexity.go
        /* Постоянная сложность */\nfunc constant(n int) int {\n    count := 0\n    size := 100000\n    for i := 0; i < size; i++ {\n        count++\n    }\n    return count\n}\n
        time_complexity.swift
        /* Постоянная сложность */\nfunc constant(n: Int) -> Int {\n    var count = 0\n    let size = 100_000\n    for _ in 0 ..< size {\n        count += 1\n    }\n    return count\n}\n
        time_complexity.js
        /* Постоянная сложность */\nfunction constant(n) {\n    let count = 0;\n    const size = 100000;\n    for (let i = 0; i < size; i++) count++;\n    return count;\n}\n
        time_complexity.ts
        /* Постоянная сложность */\nfunction constant(n: number): number {\n    let count = 0;\n    const size = 100000;\n    for (let i = 0; i < size; i++) count++;\n    return count;\n}\n
        time_complexity.dart
        /* Постоянная сложность */\nint constant(int n) {\n  int count = 0;\n  int size = 100000;\n  for (var i = 0; i < size; i++) {\n    count++;\n  }\n  return count;\n}\n
        time_complexity.rs
        /* Постоянная сложность */\nfn constant(n: i32) -> i32 {\n    _ = n;\n    let mut count = 0;\n    let size = 100_000;\n    for _ in 0..size {\n        count += 1;\n    }\n    count\n}\n
        time_complexity.c
        /* Постоянная сложность */\nint constant(int n) {\n    int count = 0;\n    int size = 100000;\n    int i = 0;\n    for (int i = 0; i < size; i++) {\n        count++;\n    }\n    return count;\n}\n
        time_complexity.kt
        /* Постоянная сложность */\nfun constant(n: Int): Int {\n    var count = 0\n    val size = 100000\n    for (i in 0..<size)\n        count++\n    return count\n}\n
        time_complexity.rb
        ### Постоянная сложность ###\ndef constant(n)\n  count = 0\n  size = 100000\n\n  (0...size).each { count += 1 }\n\n  count\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 2. Анализ сложности","2.3   Временная сложность"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#2-on","level":3,"title":"2.   Линейная сложность \\(O(n)\\)","text":"

        Линейная сложность характеризуется тем, что число операций растет линейно относительно размера входных данных \\(n\\) . Линейная сложность обычно встречается в одноуровневых циклах:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
        def linear(n: int) -> int:\n    \"\"\"Линейная сложность\"\"\"\n    count = 0\n    for _ in range(n):\n        count += 1\n    return count\n
        time_complexity.cpp
        /* Линейная сложность */\nint linear(int n) {\n    int count = 0;\n    for (int i = 0; i < n; i++)\n        count++;\n    return count;\n}\n
        time_complexity.java
        /* Линейная сложность */\nint linear(int n) {\n    int count = 0;\n    for (int i = 0; i < n; i++)\n        count++;\n    return count;\n}\n
        time_complexity.cs
        /* Линейная сложность */\nint Linear(int n) {\n    int count = 0;\n    for (int i = 0; i < n; i++)\n        count++;\n    return count;\n}\n
        time_complexity.go
        /* Линейная сложность */\nfunc linear(n int) int {\n    count := 0\n    for i := 0; i < n; i++ {\n        count++\n    }\n    return count\n}\n
        time_complexity.swift
        /* Линейная сложность */\nfunc linear(n: Int) -> Int {\n    var count = 0\n    for _ in 0 ..< n {\n        count += 1\n    }\n    return count\n}\n
        time_complexity.js
        /* Линейная сложность */\nfunction linear(n) {\n    let count = 0;\n    for (let i = 0; i < n; i++) count++;\n    return count;\n}\n
        time_complexity.ts
        /* Линейная сложность */\nfunction linear(n: number): number {\n    let count = 0;\n    for (let i = 0; i < n; i++) count++;\n    return count;\n}\n
        time_complexity.dart
        /* Линейная сложность */\nint linear(int n) {\n  int count = 0;\n  for (var i = 0; i < n; i++) {\n    count++;\n  }\n  return count;\n}\n
        time_complexity.rs
        /* Линейная сложность */\nfn linear(n: i32) -> i32 {\n    let mut count = 0;\n    for _ in 0..n {\n        count += 1;\n    }\n    count\n}\n
        time_complexity.c
        /* Линейная сложность */\nint linear(int n) {\n    int count = 0;\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
        time_complexity.kt
        /* Линейная сложность */\nfun linear(n: Int): Int {\n    var count = 0\n    for (i in 0..<n)\n        count++\n    return count\n}\n
        time_complexity.rb
        ### Линейная сложность ###\ndef linear(n)\n  count = 0\n  (0...n).each { count += 1 }\n  count\nend\n
        Визуализация кода

        Во весь экран >

        Операции обхода массива и обхода связного списка имеют временную сложность \\(O(n)\\) , где \\(n\\) - длина массива или списка:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
        def array_traversal(nums: list[int]) -> int:\n    \"\"\"Линейная сложность (обход массива)\"\"\"\n    count = 0\n    # Число итераций пропорционально длине массива\n    for num in nums:\n        count += 1\n    return count\n
        time_complexity.cpp
        /* Линейная сложность (обход массива) */\nint arrayTraversal(vector<int> &nums) {\n    int count = 0;\n    // Число итераций пропорционально длине массива\n    for (int num : nums) {\n        count++;\n    }\n    return count;\n}\n
        time_complexity.java
        /* Линейная сложность (обход массива) */\nint arrayTraversal(int[] nums) {\n    int count = 0;\n    // Число итераций пропорционально длине массива\n    for (int num : nums) {\n        count++;\n    }\n    return count;\n}\n
        time_complexity.cs
        /* Линейная сложность (обход массива) */\nint ArrayTraversal(int[] nums) {\n    int count = 0;\n    // Число итераций пропорционально длине массива\n    foreach (int num in nums) {\n        count++;\n    }\n    return count;\n}\n
        time_complexity.go
        /* Линейная сложность (обход массива) */\nfunc arrayTraversal(nums []int) int {\n    count := 0\n    // Число итераций пропорционально длине массива\n    for range nums {\n        count++\n    }\n    return count\n}\n
        time_complexity.swift
        /* Линейная сложность (обход массива) */\nfunc arrayTraversal(nums: [Int]) -> Int {\n    var count = 0\n    // Число итераций пропорционально длине массива\n    for _ in nums {\n        count += 1\n    }\n    return count\n}\n
        time_complexity.js
        /* Линейная сложность (обход массива) */\nfunction arrayTraversal(nums) {\n    let count = 0;\n    // Число итераций пропорционально длине массива\n    for (let i = 0; i < nums.length; i++) {\n        count++;\n    }\n    return count;\n}\n
        time_complexity.ts
        /* Линейная сложность (обход массива) */\nfunction arrayTraversal(nums: number[]): number {\n    let count = 0;\n    // Число итераций пропорционально длине массива\n    for (let i = 0; i < nums.length; i++) {\n        count++;\n    }\n    return count;\n}\n
        time_complexity.dart
        /* Линейная сложность (обход массива) */\nint arrayTraversal(List<int> nums) {\n  int count = 0;\n  // Число итераций пропорционально длине массива\n  for (var _num in nums) {\n    count++;\n  }\n  return count;\n}\n
        time_complexity.rs
        /* Линейная сложность (обход массива) */\nfn array_traversal(nums: &[i32]) -> i32 {\n    let mut count = 0;\n    // Число итераций пропорционально длине массива\n    for _ in nums {\n        count += 1;\n    }\n    count\n}\n
        time_complexity.c
        /* Линейная сложность (обход массива) */\nint arrayTraversal(int *nums, int n) {\n    int count = 0;\n    // Число итераций пропорционально длине массива\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
        time_complexity.kt
        /* Линейная сложность (обход массива) */\nfun arrayTraversal(nums: IntArray): Int {\n    var count = 0\n    // Число итераций пропорционально длине массива\n    for (num in nums) {\n        count++\n    }\n    return count\n}\n
        time_complexity.rb
        ### Линейная сложность ###\ndef linear(n)\n  count = 0\n  (0...n).each { count += 1 }\n  count\nend\n\n# ## Линейная сложность (обход массива) ###\ndef array_traversal(nums)\n  count = 0\n\n  # Число итераций пропорционально длине массива\n  for num in nums\n    count += 1\n  end\n\n  count\nend\n
        Визуализация кода

        Во весь экран >

        Стоит отметить, что размер входных данных \\(n\\) нужно определять конкретно в зависимости от типа входа. Например, в первом примере переменная \\(n\\) сама является размером входных данных. Во втором примере размером данных служит длина массива.

        ","path":["Глава 2. Анализ сложности","2.3   Временная сложность"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#3-on2","level":3,"title":"3.   Квадратичная сложность \\(O(n^2)\\)","text":"

        Квадратичная сложность характеризуется тем, что число операций растет квадратично относительно размера входных данных \\(n\\) . Квадратичная сложность обычно встречается во вложенных циклах: временная сложность внешнего и внутреннего циклов равна \\(O(n)\\) , поэтому общая временная сложность составляет \\(O(n^2)\\) :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
        def quadratic(n: int) -> int:\n    \"\"\"Квадратичная сложность\"\"\"\n    count = 0\n    # Число итераций квадратично зависит от размера данных n\n    for i in range(n):\n        for j in range(n):\n            count += 1\n    return count\n
        time_complexity.cpp
        /* Квадратичная сложность */\nint quadratic(int n) {\n    int count = 0;\n    // Число итераций квадратично зависит от размера данных n\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
        time_complexity.java
        /* Квадратичная сложность */\nint quadratic(int n) {\n    int count = 0;\n    // Число итераций квадратично зависит от размера данных n\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
        time_complexity.cs
        /* Квадратичная сложность */\nint Quadratic(int n) {\n    int count = 0;\n    // Число итераций квадратично зависит от размера данных n\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
        time_complexity.go
        /* Квадратичная сложность */\nfunc quadratic(n int) int {\n    count := 0\n    // Число итераций квадратично зависит от размера данных n\n    for i := 0; i < n; i++ {\n        for j := 0; j < n; j++ {\n            count++\n        }\n    }\n    return count\n}\n
        time_complexity.swift
        /* Квадратичная сложность */\nfunc quadratic(n: Int) -> Int {\n    var count = 0\n    // Число итераций квадратично зависит от размера данных n\n    for _ in 0 ..< n {\n        for _ in 0 ..< n {\n            count += 1\n        }\n    }\n    return count\n}\n
        time_complexity.js
        /* Квадратичная сложность */\nfunction quadratic(n) {\n    let count = 0;\n    // Число итераций квадратично зависит от размера данных n\n    for (let i = 0; i < n; i++) {\n        for (let j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
        time_complexity.ts
        /* Квадратичная сложность */\nfunction quadratic(n: number): number {\n    let count = 0;\n    // Число итераций квадратично зависит от размера данных n\n    for (let i = 0; i < n; i++) {\n        for (let j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
        time_complexity.dart
        /* Квадратичная сложность */\nint quadratic(int n) {\n  int count = 0;\n  // Число итераций квадратично зависит от размера данных n\n  for (int i = 0; i < n; i++) {\n    for (int j = 0; j < n; j++) {\n      count++;\n    }\n  }\n  return count;\n}\n
        time_complexity.rs
        /* Квадратичная сложность */\nfn quadratic(n: i32) -> i32 {\n    let mut count = 0;\n    // Число итераций квадратично зависит от размера данных n\n    for _ in 0..n {\n        for _ in 0..n {\n            count += 1;\n        }\n    }\n    count\n}\n
        time_complexity.c
        /* Квадратичная сложность */\nint quadratic(int n) {\n    int count = 0;\n    // Число итераций квадратично зависит от размера данных n\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
        time_complexity.kt
        /* Квадратичная сложность */\nfun quadratic(n: Int): Int {\n    var count = 0\n    // Число итераций квадратично зависит от размера данных n\n    for (i in 0..<n) {\n        for (j in 0..<n) {\n            count++\n        }\n    }\n    return count\n}\n
        time_complexity.rb
        ### Квадратичная сложность ###\ndef quadratic(n)\n  count = 0\n\n  # Число итераций квадратично зависит от размера данных n\n  for i in 0...n\n    for j in 0...n\n      count += 1\n    end\n  end\n\n  count\nend\n
        Визуализация кода

        Во весь экран >

        На рисунке 2-10 сравниваются три временные сложности: постоянная, линейная и квадратичная.

        Рисунок 2-10   Постоянная, линейная и квадратичная временная сложность

        Возьмем в качестве примера пузырьковую сортировку: внешний цикл выполняется \\(n - 1\\) раз, внутренний цикл выполняется \\(n-1\\) , \\(n-2\\) , \\(\\dots\\) , \\(2\\) , \\(1\\) раз, в среднем это \\(n / 2\\) раз, поэтому временная сложность равна \\(O((n - 1)n / 2) = O(n^2)\\) :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
        def bubble_sort(nums: list[int]) -> int:\n    \"\"\"Квадратичная сложность (пузырьковая сортировка)\"\"\"\n    count = 0  # Счетчик\n    # Внешний цикл: неотсортированный диапазон [0, i]\n    for i in range(len(nums) - 1, 0, -1):\n        # Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for j in range(i):\n            if nums[j] > nums[j + 1]:\n                # Поменять местами nums[j] и nums[j + 1]\n                tmp: int = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = tmp\n                count += 3  # Обмен элементов включает 3 элементарные операции\n    return count\n
        time_complexity.cpp
        /* Квадратичная сложность (пузырьковая сортировка) */\nint bubbleSort(vector<int> &nums) {\n    int count = 0; // Счетчик\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (int i = nums.size() - 1; i > 0; i--) {\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Поменять местами nums[j] и nums[j + 1]\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // Обмен элементов включает 3 элементарные операции\n            }\n        }\n    }\n    return count;\n}\n
        time_complexity.java
        /* Квадратичная сложность (пузырьковая сортировка) */\nint bubbleSort(int[] nums) {\n    int count = 0; // Счетчик\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (int i = nums.length - 1; i > 0; i--) {\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Поменять местами nums[j] и nums[j + 1]\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // Обмен элементов включает 3 элементарные операции\n            }\n        }\n    }\n    return count;\n}\n
        time_complexity.cs
        /* Квадратичная сложность (пузырьковая сортировка) */\nint BubbleSort(int[] nums) {\n    int count = 0;  // Счетчик\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (int i = nums.Length - 1; i > 0; i--) {\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Поменять местами nums[j] и nums[j + 1]\n                (nums[j + 1], nums[j]) = (nums[j], nums[j + 1]);\n                count += 3;  // Обмен элементов включает 3 элементарные операции\n            }\n        }\n    }\n    return count;\n}\n
        time_complexity.go
        /* Квадратичная сложность (пузырьковая сортировка) */\nfunc bubbleSort(nums []int) int {\n    count := 0 // Счетчик\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for i := len(nums) - 1; i > 0; i-- {\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for j := 0; j < i; j++ {\n            if nums[j] > nums[j+1] {\n                // Поменять местами nums[j] и nums[j + 1]\n                tmp := nums[j]\n                nums[j] = nums[j+1]\n                nums[j+1] = tmp\n                count += 3 // Обмен элементов включает 3 элементарные операции\n            }\n        }\n    }\n    return count\n}\n
        time_complexity.swift
        /* Квадратичная сложность (пузырьковая сортировка) */\nfunc bubbleSort(nums: inout [Int]) -> Int {\n    var count = 0 // Счетчик\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for i in nums.indices.dropFirst().reversed() {\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for j in 0 ..< i {\n            if nums[j] > nums[j + 1] {\n                // Поменять местами nums[j] и nums[j + 1]\n                let tmp = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = tmp\n                count += 3 // Обмен элементов включает 3 элементарные операции\n            }\n        }\n    }\n    return count\n}\n
        time_complexity.js
        /* Квадратичная сложность (пузырьковая сортировка) */\nfunction bubbleSort(nums) {\n    let count = 0; // Счетчик\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Поменять местами nums[j] и nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // Обмен элементов включает 3 элементарные операции\n            }\n        }\n    }\n    return count;\n}\n
        time_complexity.ts
        /* Квадратичная сложность (пузырьковая сортировка) */\nfunction bubbleSort(nums: number[]): number {\n    let count = 0; // Счетчик\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Поменять местами nums[j] и nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // Обмен элементов включает 3 элементарные операции\n            }\n        }\n    }\n    return count;\n}\n
        time_complexity.dart
        /* Квадратичная сложность (пузырьковая сортировка) */\nint bubbleSort(List<int> nums) {\n  int count = 0; // Счетчик\n  // Внешний цикл: неотсортированный диапазон [0, i]\n  for (var i = nums.length - 1; i > 0; i--) {\n    // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n    for (var j = 0; j < i; j++) {\n      if (nums[j] > nums[j + 1]) {\n        // Поменять местами nums[j] и nums[j + 1]\n        int tmp = nums[j];\n        nums[j] = nums[j + 1];\n        nums[j + 1] = tmp;\n        count += 3; // Обмен элементов включает 3 элементарные операции\n      }\n    }\n  }\n  return count;\n}\n
        time_complexity.rs
        /* Квадратичная сложность (пузырьковая сортировка) */\nfn bubble_sort(nums: &mut [i32]) -> i32 {\n    let mut count = 0; // Счетчик\n\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for i in (1..nums.len()).rev() {\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for j in 0..i {\n            if nums[j] > nums[j + 1] {\n                // Поменять местами nums[j] и nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // Обмен элементов включает 3 элементарные операции\n            }\n        }\n    }\n    count\n}\n
        time_complexity.c
        /* Квадратичная сложность (пузырьковая сортировка) */\nint bubbleSort(int *nums, int n) {\n    int count = 0; // Счетчик\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (int i = n - 1; i > 0; i--) {\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Поменять местами nums[j] и nums[j + 1]\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // Обмен элементов включает 3 элементарные операции\n            }\n        }\n    }\n    return count;\n}\n
        time_complexity.kt
        /* Квадратичная сложность (пузырьковая сортировка) */\nfun bubbleSort(nums: IntArray): Int {\n    var count = 0 // Счетчик\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (i in nums.size - 1 downTo 1) {\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (j in 0..<i) {\n            if (nums[j] > nums[j + 1]) {\n                // Поменять местами nums[j] и nums[j + 1]\n                val temp = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = temp\n                count += 3 // Обмен элементов включает 3 элементарные операции\n            }\n        }\n    }\n    return count\n}\n
        time_complexity.rb
        ### Квадратичная сложность ###\ndef quadratic(n)\n  count = 0\n\n  # Число итераций квадратично зависит от размера данных n\n  for i in 0...n\n    for j in 0...n\n      count += 1\n    end\n  end\n\n  count\nend\n\n# ## Квадратичная сложность (пузырьковая сортировка) ###\ndef bubble_sort(nums)\n  count = 0  # Счетчик\n\n  # Внешний цикл: неотсортированный диапазон [0, i]\n  for i in (nums.length - 1).downto(0)\n    # Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # Поменять местами nums[j] и nums[j + 1]\n        tmp = nums[j]\n        nums[j] = nums[j + 1]\n        nums[j + 1] = tmp\n        count += 3 # Обмен элементов включает 3 элементарные операции\n      end\n    end\n  end\n\n  count\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 2. Анализ сложности","2.3   Временная сложность"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#4-o2n","level":3,"title":"4.   Экспоненциальная сложность \\(O(2^n)\\)","text":"

        Типичный пример экспоненциального роста в биологии - деление клеток: в начальном состоянии есть одна клетка, после одного деления их становится 2, после двух делений - 4 и так далее. После \\(n\\) раундов деления клеток становится \\(2^n\\) .

        На рисунке 2-11 и в следующем коде моделируется процесс деления клеток. Временная сложность равна \\(O(2^n)\\) . Здесь входное значение \\(n\\) обозначает число раундов деления, а возвращаемое значение count обозначает общее число делений.

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
        def exponential(n: int) -> int:\n    \"\"\"Экспоненциальная сложность (итеративная реализация)\"\"\"\n    count = 0\n    base = 1\n    # На каждом шаге клетка делится надвое, образуя последовательность 1, 2, 4, 8, ..., 2^(n-1)\n    for _ in range(n):\n        for _ in range(base):\n            count += 1\n        base *= 2\n    # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count\n
        time_complexity.cpp
        /* Экспоненциальная сложность (итеративная реализация) */\nint exponential(int n) {\n    int count = 0, base = 1;\n    // На каждом шаге клетка делится надвое, образуя последовательность 1, 2, 4, 8, ..., 2^(n-1)\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < base; j++) {\n            count++;\n        }\n        base *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
        time_complexity.java
        /* Экспоненциальная сложность (итеративная реализация) */\nint exponential(int n) {\n    int count = 0, base = 1;\n    // На каждом шаге клетка делится надвое, образуя последовательность 1, 2, 4, 8, ..., 2^(n-1)\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < base; j++) {\n            count++;\n        }\n        base *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
        time_complexity.cs
        /* Экспоненциальная сложность (итеративная реализация) */\nint Exponential(int n) {\n    int count = 0, bas = 1;\n    // На каждом шаге клетка делится надвое, образуя последовательность 1, 2, 4, 8, ..., 2^(n-1)\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < bas; j++) {\n            count++;\n        }\n        bas *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
        time_complexity.go
        /* Экспоненциальная сложность (итеративная реализация) */\nfunc exponential(n int) int {\n    count, base := 0, 1\n    // На каждом шаге клетка делится надвое, образуя последовательность 1, 2, 4, 8, ..., 2^(n-1)\n    for i := 0; i < n; i++ {\n        for j := 0; j < base; j++ {\n            count++\n        }\n        base *= 2\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count\n}\n
        time_complexity.swift
        /* Экспоненциальная сложность (итеративная реализация) */\nfunc exponential(n: Int) -> Int {\n    var count = 0\n    var base = 1\n    // На каждом шаге клетка делится надвое, образуя последовательность 1, 2, 4, 8, ..., 2^(n-1)\n    for _ in 0 ..< n {\n        for _ in 0 ..< base {\n            count += 1\n        }\n        base *= 2\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count\n}\n
        time_complexity.js
        /* Экспоненциальная сложность (итеративная реализация) */\nfunction exponential(n) {\n    let count = 0,\n        base = 1;\n    // На каждом шаге клетка делится надвое, образуя последовательность 1, 2, 4, 8, ..., 2^(n-1)\n    for (let i = 0; i < n; i++) {\n        for (let j = 0; j < base; j++) {\n            count++;\n        }\n        base *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
        time_complexity.ts
        /* Экспоненциальная сложность (итеративная реализация) */\nfunction exponential(n: number): number {\n    let count = 0,\n        base = 1;\n    // На каждом шаге клетка делится надвое, образуя последовательность 1, 2, 4, 8, ..., 2^(n-1)\n    for (let i = 0; i < n; i++) {\n        for (let j = 0; j < base; j++) {\n            count++;\n        }\n        base *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
        time_complexity.dart
        /* Экспоненциальная сложность (итеративная реализация) */\nint exponential(int n) {\n  int count = 0, base = 1;\n  // На каждом шаге клетка делится надвое, образуя последовательность 1, 2, 4, 8, ..., 2^(n-1)\n  for (var i = 0; i < n; i++) {\n    for (var j = 0; j < base; j++) {\n      count++;\n    }\n    base *= 2;\n  }\n  // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n  return count;\n}\n
        time_complexity.rs
        /* Экспоненциальная сложность (итеративная реализация) */\nfn exponential(n: i32) -> i32 {\n    let mut count = 0;\n    let mut base = 1;\n    // На каждом шаге клетка делится надвое, образуя последовательность 1, 2, 4, 8, ..., 2^(n-1)\n    for _ in 0..n {\n        for _ in 0..base {\n            count += 1\n        }\n        base *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    count\n}\n
        time_complexity.c
        /* Экспоненциальная сложность (итеративная реализация) */\nint exponential(int n) {\n    int count = 0;\n    int bas = 1;\n    // На каждом шаге клетка делится надвое, образуя последовательность 1, 2, 4, 8, ..., 2^(n-1)\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < bas; j++) {\n            count++;\n        }\n        bas *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
        time_complexity.kt
        /* Экспоненциальная сложность (итеративная реализация) */\nfun exponential(n: Int): Int {\n    var count = 0\n    var base = 1\n    // На каждом шаге клетка делится надвое, образуя последовательность 1, 2, 4, 8, ..., 2^(n-1)\n    for (i in 0..<n) {\n        for (j in 0..<base) {\n            count++\n        }\n        base *= 2\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count\n}\n
        time_complexity.rb
        ### Квадратичная сложность ###\ndef quadratic(n)\n  count = 0\n\n  # Число итераций квадратично зависит от размера данных n\n  for i in 0...n\n    for j in 0...n\n      count += 1\n    end\n  end\n\n  count\nend\n\n# ## Квадратичная сложность (пузырьковая сортировка) ###\ndef bubble_sort(nums)\n  count = 0  # Счетчик\n\n  # Внешний цикл: неотсортированный диапазон [0, i]\n  for i in (nums.length - 1).downto(0)\n    # Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # Поменять местами nums[j] и nums[j + 1]\n        tmp = nums[j]\n        nums[j] = nums[j + 1]\n        nums[j + 1] = tmp\n        count += 3 # Обмен элементов включает 3 элементарные операции\n      end\n    end\n  end\n\n  count\nend\n\n# ## Экспоненциальная сложность (итеративная реализация) ###\ndef exponential(n)\n  count, base = 0, 1\n\n  # На каждом шаге клетка делится надвое, образуя последовательность 1, 2, 4, 8, ..., 2^(n-1)\n  (0...n).each do\n    (0...base).each { count += 1 }\n    base *= 2\n  end\n\n  # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n  count\nend\n
        Визуализация кода

        Во весь экран >

        Рисунок 2-11   Экспоненциальная временная сложность

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

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
        def exp_recur(n: int) -> int:\n    \"\"\"Экспоненциальная сложность (рекурсивная реализация)\"\"\"\n    if n == 1:\n        return 1\n    return exp_recur(n - 1) + exp_recur(n - 1) + 1\n
        time_complexity.cpp
        /* Экспоненциальная сложность (рекурсивная реализация) */\nint expRecur(int n) {\n    if (n == 1)\n        return 1;\n    return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
        time_complexity.java
        /* Экспоненциальная сложность (рекурсивная реализация) */\nint expRecur(int n) {\n    if (n == 1)\n        return 1;\n    return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
        time_complexity.cs
        /* Экспоненциальная сложность (рекурсивная реализация) */\nint ExpRecur(int n) {\n    if (n == 1) return 1;\n    return ExpRecur(n - 1) + ExpRecur(n - 1) + 1;\n}\n
        time_complexity.go
        /* Экспоненциальная сложность (рекурсивная реализация) */\nfunc expRecur(n int) int {\n    if n == 1 {\n        return 1\n    }\n    return expRecur(n-1) + expRecur(n-1) + 1\n}\n
        time_complexity.swift
        /* Экспоненциальная сложность (рекурсивная реализация) */\nfunc expRecur(n: Int) -> Int {\n    if n == 1 {\n        return 1\n    }\n    return expRecur(n: n - 1) + expRecur(n: n - 1) + 1\n}\n
        time_complexity.js
        /* Экспоненциальная сложность (рекурсивная реализация) */\nfunction expRecur(n) {\n    if (n === 1) return 1;\n    return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
        time_complexity.ts
        /* Экспоненциальная сложность (рекурсивная реализация) */\nfunction expRecur(n: number): number {\n    if (n === 1) return 1;\n    return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
        time_complexity.dart
        /* Экспоненциальная сложность (рекурсивная реализация) */\nint expRecur(int n) {\n  if (n == 1) return 1;\n  return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
        time_complexity.rs
        /* Экспоненциальная сложность (рекурсивная реализация) */\nfn exp_recur(n: i32) -> i32 {\n    if n == 1 {\n        return 1;\n    }\n    exp_recur(n - 1) + exp_recur(n - 1) + 1\n}\n
        time_complexity.c
        /* Экспоненциальная сложность (рекурсивная реализация) */\nint expRecur(int n) {\n    if (n == 1)\n        return 1;\n    return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
        time_complexity.kt
        /* Экспоненциальная сложность (рекурсивная реализация) */\nfun expRecur(n: Int): Int {\n    if (n == 1) {\n        return 1\n    }\n    return expRecur(n - 1) + expRecur(n - 1) + 1\n}\n
        time_complexity.rb
        ### Квадратичная сложность ###\ndef quadratic(n)\n  count = 0\n\n  # Число итераций квадратично зависит от размера данных n\n  for i in 0...n\n    for j in 0...n\n      count += 1\n    end\n  end\n\n  count\nend\n\n# ## Квадратичная сложность (пузырьковая сортировка) ###\ndef bubble_sort(nums)\n  count = 0  # Счетчик\n\n  # Внешний цикл: неотсортированный диапазон [0, i]\n  for i in (nums.length - 1).downto(0)\n    # Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # Поменять местами nums[j] и nums[j + 1]\n        tmp = nums[j]\n        nums[j] = nums[j + 1]\n        nums[j + 1] = tmp\n        count += 3 # Обмен элементов включает 3 элементарные операции\n      end\n    end\n  end\n\n  count\nend\n\n# ## Экспоненциальная сложность (итеративная реализация) ###\ndef exponential(n)\n  count, base = 0, 1\n\n  # На каждом шаге клетка делится надвое, образуя последовательность 1, 2, 4, 8, ..., 2^(n-1)\n  (0...n).each do\n    (0...base).each { count += 1 }\n    base *= 2\n  end\n\n  # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n  count\nend\n\n# ## Экспоненциальная сложность (рекурсивная реализация) ###\ndef exp_recur(n)\n  return 1 if n == 1\n  exp_recur(n - 1) + exp_recur(n - 1) + 1\nend\n
        Визуализация кода

        Во весь экран >

        Экспоненциальный рост происходит очень быстро и часто встречается в переборных методах, грубой силе, поиске с возвратом и тому подобных подходах. Для задач большого масштаба экспоненциальная сложность неприемлема, и обычно приходится применять динамическое программирование, жадные алгоритмы и другие стратегии.

        ","path":["Глава 2. Анализ сложности","2.3   Временная сложность"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#5-olog-n","level":3,"title":"5.   Логарифмическая сложность \\(O(\\log n)\\)","text":"

        В противоположность экспоненциальной, логарифмическая сложность описывает ситуацию, когда в каждом раунде размер задачи уменьшается вдвое. Пусть размер входных данных равен \\(n\\). Так как на каждом шаге размер уменьшается вдвое, число итераций равно \\(\\log_2 n\\) , то есть является обратной функцией к \\(2^n\\) .

        На рисунке 2-12 и в следующем коде моделируется процесс, в котором в каждом раунде размер задачи уменьшается вдвое. Временная сложность равна \\(O(\\log_2 n)\\) и кратко записывается как \\(O(\\log n)\\) :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
        def logarithmic(n: int) -> int:\n    \"\"\"Логарифмическая сложность (итеративная реализация)\"\"\"\n    count = 0\n    while n > 1:\n        n = n / 2\n        count += 1\n    return count\n
        time_complexity.cpp
        /* Логарифмическая сложность (итеративная реализация) */\nint logarithmic(int n) {\n    int count = 0;\n    while (n > 1) {\n        n = n / 2;\n        count++;\n    }\n    return count;\n}\n
        time_complexity.java
        /* Логарифмическая сложность (итеративная реализация) */\nint logarithmic(int n) {\n    int count = 0;\n    while (n > 1) {\n        n = n / 2;\n        count++;\n    }\n    return count;\n}\n
        time_complexity.cs
        /* Логарифмическая сложность (итеративная реализация) */\nint Logarithmic(int n) {\n    int count = 0;\n    while (n > 1) {\n        n /= 2;\n        count++;\n    }\n    return count;\n}\n
        time_complexity.go
        /* Логарифмическая сложность (итеративная реализация) */\nfunc logarithmic(n int) int {\n    count := 0\n    for n > 1 {\n        n = n / 2\n        count++\n    }\n    return count\n}\n
        time_complexity.swift
        /* Логарифмическая сложность (итеративная реализация) */\nfunc logarithmic(n: Int) -> Int {\n    var count = 0\n    var n = n\n    while n > 1 {\n        n = n / 2\n        count += 1\n    }\n    return count\n}\n
        time_complexity.js
        /* Логарифмическая сложность (итеративная реализация) */\nfunction logarithmic(n) {\n    let count = 0;\n    while (n > 1) {\n        n = n / 2;\n        count++;\n    }\n    return count;\n}\n
        time_complexity.ts
        /* Логарифмическая сложность (итеративная реализация) */\nfunction logarithmic(n: number): number {\n    let count = 0;\n    while (n > 1) {\n        n = n / 2;\n        count++;\n    }\n    return count;\n}\n
        time_complexity.dart
        /* Логарифмическая сложность (итеративная реализация) */\nint logarithmic(int n) {\n  int count = 0;\n  while (n > 1) {\n    n = n ~/ 2;\n    count++;\n  }\n  return count;\n}\n
        time_complexity.rs
        /* Логарифмическая сложность (итеративная реализация) */\nfn logarithmic(mut n: i32) -> i32 {\n    let mut count = 0;\n    while n > 1 {\n        n = n / 2;\n        count += 1;\n    }\n    count\n}\n
        time_complexity.c
        /* Логарифмическая сложность (итеративная реализация) */\nint logarithmic(int n) {\n    int count = 0;\n    while (n > 1) {\n        n = n / 2;\n        count++;\n    }\n    return count;\n}\n
        time_complexity.kt
        /* Логарифмическая сложность (итеративная реализация) */\nfun logarithmic(n: Int): Int {\n    var n1 = n\n    var count = 0\n    while (n1 > 1) {\n        n1 /= 2\n        count++\n    }\n    return count\n}\n
        time_complexity.rb
        ### Квадратичная сложность ###\ndef quadratic(n)\n  count = 0\n\n  # Число итераций квадратично зависит от размера данных n\n  for i in 0...n\n    for j in 0...n\n      count += 1\n    end\n  end\n\n  count\nend\n\n# ## Квадратичная сложность (пузырьковая сортировка) ###\ndef bubble_sort(nums)\n  count = 0  # Счетчик\n\n  # Внешний цикл: неотсортированный диапазон [0, i]\n  for i in (nums.length - 1).downto(0)\n    # Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # Поменять местами nums[j] и nums[j + 1]\n        tmp = nums[j]\n        nums[j] = nums[j + 1]\n        nums[j + 1] = tmp\n        count += 3 # Обмен элементов включает 3 элементарные операции\n      end\n    end\n  end\n\n  count\nend\n\n# ## Экспоненциальная сложность (итеративная реализация) ###\ndef exponential(n)\n  count, base = 0, 1\n\n  # На каждом шаге клетка делится надвое, образуя последовательность 1, 2, 4, 8, ..., 2^(n-1)\n  (0...n).each do\n    (0...base).each { count += 1 }\n    base *= 2\n  end\n\n  # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n  count\nend\n\n# ## Экспоненциальная сложность (рекурсивная реализация) ###\ndef exp_recur(n)\n  return 1 if n == 1\n  exp_recur(n - 1) + exp_recur(n - 1) + 1\nend\n\n# ## Логарифмическая сложность (итеративная реализация) ###\ndef logarithmic(n)\n  count = 0\n\n  while n > 1\n    n /= 2\n    count += 1\n  end\n\n  count\nend\n
        Визуализация кода

        Во весь экран >

        Рисунок 2-12   Логарифмическая временная сложность

        Подобно экспоненциальной сложности, логарифмическая также часто встречается в рекурсивных функциях. Следующий код формирует рекурсивное дерево высотой \\(\\log_2 n\\) :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
        def log_recur(n: int) -> int:\n    \"\"\"Логарифмическая сложность (рекурсивная реализация)\"\"\"\n    if n <= 1:\n        return 0\n    return log_recur(n / 2) + 1\n
        time_complexity.cpp
        /* Логарифмическая сложность (рекурсивная реализация) */\nint logRecur(int n) {\n    if (n <= 1)\n        return 0;\n    return logRecur(n / 2) + 1;\n}\n
        time_complexity.java
        /* Логарифмическая сложность (рекурсивная реализация) */\nint logRecur(int n) {\n    if (n <= 1)\n        return 0;\n    return logRecur(n / 2) + 1;\n}\n
        time_complexity.cs
        /* Логарифмическая сложность (рекурсивная реализация) */\nint LogRecur(int n) {\n    if (n <= 1) return 0;\n    return LogRecur(n / 2) + 1;\n}\n
        time_complexity.go
        /* Логарифмическая сложность (рекурсивная реализация) */\nfunc logRecur(n int) int {\n    if n <= 1 {\n        return 0\n    }\n    return logRecur(n/2) + 1\n}\n
        time_complexity.swift
        /* Логарифмическая сложность (рекурсивная реализация) */\nfunc logRecur(n: Int) -> Int {\n    if n <= 1 {\n        return 0\n    }\n    return logRecur(n: n / 2) + 1\n}\n
        time_complexity.js
        /* Логарифмическая сложность (рекурсивная реализация) */\nfunction logRecur(n) {\n    if (n <= 1) return 0;\n    return logRecur(n / 2) + 1;\n}\n
        time_complexity.ts
        /* Логарифмическая сложность (рекурсивная реализация) */\nfunction logRecur(n: number): number {\n    if (n <= 1) return 0;\n    return logRecur(n / 2) + 1;\n}\n
        time_complexity.dart
        /* Логарифмическая сложность (рекурсивная реализация) */\nint logRecur(int n) {\n  if (n <= 1) return 0;\n  return logRecur(n ~/ 2) + 1;\n}\n
        time_complexity.rs
        /* Логарифмическая сложность (рекурсивная реализация) */\nfn log_recur(n: i32) -> i32 {\n    if n <= 1 {\n        return 0;\n    }\n    log_recur(n / 2) + 1\n}\n
        time_complexity.c
        /* Логарифмическая сложность (рекурсивная реализация) */\nint logRecur(int n) {\n    if (n <= 1)\n        return 0;\n    return logRecur(n / 2) + 1;\n}\n
        time_complexity.kt
        /* Логарифмическая сложность (рекурсивная реализация) */\nfun logRecur(n: Int): Int {\n    if (n <= 1)\n        return 0\n    return logRecur(n / 2) + 1\n}\n
        time_complexity.rb
        ### Квадратичная сложность ###\ndef quadratic(n)\n  count = 0\n\n  # Число итераций квадратично зависит от размера данных n\n  for i in 0...n\n    for j in 0...n\n      count += 1\n    end\n  end\n\n  count\nend\n\n# ## Квадратичная сложность (пузырьковая сортировка) ###\ndef bubble_sort(nums)\n  count = 0  # Счетчик\n\n  # Внешний цикл: неотсортированный диапазон [0, i]\n  for i in (nums.length - 1).downto(0)\n    # Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # Поменять местами nums[j] и nums[j + 1]\n        tmp = nums[j]\n        nums[j] = nums[j + 1]\n        nums[j + 1] = tmp\n        count += 3 # Обмен элементов включает 3 элементарные операции\n      end\n    end\n  end\n\n  count\nend\n\n# ## Экспоненциальная сложность (итеративная реализация) ###\ndef exponential(n)\n  count, base = 0, 1\n\n  # На каждом шаге клетка делится надвое, образуя последовательность 1, 2, 4, 8, ..., 2^(n-1)\n  (0...n).each do\n    (0...base).each { count += 1 }\n    base *= 2\n  end\n\n  # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n  count\nend\n\n# ## Экспоненциальная сложность (рекурсивная реализация) ###\ndef exp_recur(n)\n  return 1 if n == 1\n  exp_recur(n - 1) + exp_recur(n - 1) + 1\nend\n\n# ## Логарифмическая сложность (итеративная реализация) ###\ndef logarithmic(n)\n  count = 0\n\n  while n > 1\n    n /= 2\n    count += 1\n  end\n\n  count\nend\n\n# ## Логарифмическая сложность (рекурсивная реализация) ###\ndef log_recur(n)\n  return 0 unless n > 1\n  log_recur(n / 2) + 1\nend\n
        Визуализация кода

        Во весь экран >

        Логарифмическая сложность часто встречается в алгоритмах, основанных на стратегии «разделяй и властвуй», и отражает идеи разбиения на части и упрощения сложной задачи. Она растет медленно и считается одной из самых желательных временных сложностей после константной.

        Каково основание у \\(O(\\log n)\\) ?

        Точнее говоря, «разделение на \\(m\\) частей» соответствует временной сложности \\(O(\\log_m n)\\) . А по формуле перехода к другому основанию логарифма мы получаем равные по сложности выражения с разными основаниями:

        \\[ O(\\log_m n) = O(\\log_k n / \\log_k m) = O(\\log_k n) \\]

        Иными словами, основание \\(m\\) можно менять без влияния на сложность. Поэтому мы обычно опускаем основание \\(m\\) и напрямую записываем логарифмическую сложность как \\(O(\\log n)\\) .

        ","path":["Глава 2. Анализ сложности","2.3   Временная сложность"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#6-on-log-n","level":3,"title":"6.   Линейно-логарифмическая сложность \\(O(n \\log n)\\)","text":"

        Линейно-логарифмическая сложность часто встречается в рекурсивных разбиениях, где временная сложность одного измерения равна \\(O(\\log n)\\) , а другого - \\(O(n)\\) . Соответствующий код выглядит следующим образом:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
        def linear_log_recur(n: int) -> int:\n    \"\"\"Линейно-логарифмическая сложность\"\"\"\n    if n <= 1:\n        return 1\n    # Разделение надвое: размер подзадачи уменьшается вдвое\n    count = linear_log_recur(n // 2) + linear_log_recur(n // 2)\n    # Текущая подзадача содержит n операций\n    for _ in range(n):\n        count += 1\n    return count\n
        time_complexity.cpp
        /* Линейно-логарифмическая сложность */\nint linearLogRecur(int n) {\n    if (n <= 1)\n        return 1;\n    int count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
        time_complexity.java
        /* Линейно-логарифмическая сложность */\nint linearLogRecur(int n) {\n    if (n <= 1)\n        return 1;\n    int count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
        time_complexity.cs
        /* Линейно-логарифмическая сложность */\nint LinearLogRecur(int n) {\n    if (n <= 1) return 1;\n    int count = LinearLogRecur(n / 2) + LinearLogRecur(n / 2);\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
        time_complexity.go
        /* Линейно-логарифмическая сложность */\nfunc linearLogRecur(n int) int {\n    if n <= 1 {\n        return 1\n    }\n    count := linearLogRecur(n/2) + linearLogRecur(n/2)\n    for i := 0; i < n; i++ {\n        count++\n    }\n    return count\n}\n
        time_complexity.swift
        /* Линейно-логарифмическая сложность */\nfunc linearLogRecur(n: Int) -> Int {\n    if n <= 1 {\n        return 1\n    }\n    var count = linearLogRecur(n: n / 2) + linearLogRecur(n: n / 2)\n    for _ in stride(from: 0, to: n, by: 1) {\n        count += 1\n    }\n    return count\n}\n
        time_complexity.js
        /* Линейно-логарифмическая сложность */\nfunction linearLogRecur(n) {\n    if (n <= 1) return 1;\n    let count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n    for (let i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
        time_complexity.ts
        /* Линейно-логарифмическая сложность */\nfunction linearLogRecur(n: number): number {\n    if (n <= 1) return 1;\n    let count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n    for (let i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
        time_complexity.dart
        /* Линейно-логарифмическая сложность */\nint linearLogRecur(int n) {\n  if (n <= 1) return 1;\n  int count = linearLogRecur(n ~/ 2) + linearLogRecur(n ~/ 2);\n  for (var i = 0; i < n; i++) {\n    count++;\n  }\n  return count;\n}\n
        time_complexity.rs
        /* Линейно-логарифмическая сложность */\nfn linear_log_recur(n: i32) -> i32 {\n    if n <= 1 {\n        return 1;\n    }\n    let mut count = linear_log_recur(n / 2) + linear_log_recur(n / 2);\n    for _ in 0..n {\n        count += 1;\n    }\n    return count;\n}\n
        time_complexity.c
        /* Линейно-логарифмическая сложность */\nint linearLogRecur(int n) {\n    if (n <= 1)\n        return 1;\n    int count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
        time_complexity.kt
        /* Линейно-логарифмическая сложность */\nfun linearLogRecur(n: Int): Int {\n    if (n <= 1)\n        return 1\n    var count = linearLogRecur(n / 2) + linearLogRecur(n / 2)\n    for (i in 0..<n) {\n        count++\n    }\n    return count\n}\n
        time_complexity.rb
        ### Линейно-логарифмическая сложность ###\ndef linear_log_recur(n)\n  return 1 unless n > 1\n\n  count = linear_log_recur(n / 2) + linear_log_recur(n / 2)\n  (0...n).each { count += 1 }\n\n  count\nend\n
        Визуализация кода

        Во весь экран >

        На рисунке 2-13 показано, как возникает линейно-логарифмическая сложность. Общее число операций на каждом уровне бинарного дерева равно \\(n\\) , а дерево имеет \\(\\log_2 n + 1\\) уровней, поэтому временная сложность равна \\(O(n \\log n)\\) .

        Рисунок 2-13   Линейно-логарифмическая временная сложность

        Временная сложность основных алгоритмов сортировки обычно равна \\(O(n \\log n)\\) , например у быстрой сортировки, сортировки слиянием, пирамидальной сортировки и т.д.

        ","path":["Глава 2. Анализ сложности","2.3   Временная сложность"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#7-on","level":3,"title":"7.   Факториальная сложность \\(O(n!)\\)","text":"

        Факториальная сложность соответствует математической задаче полной перестановки. Если даны \\(n\\) попарно различных элементов, то число всех возможных перестановок равно:

        \\[ n! = n \\times (n - 1) \\times (n - 2) \\times \\dots \\times 2 \\times 1 \\]

        Факториал обычно реализуют через рекурсию. Как показано на рисунке 2-14 и в следующем коде, на первом уровне происходит ветвление на \\(n\\) подзадач, на втором - на \\(n - 1\\) и так далее, пока на \\(n\\)-м уровне ветвление не прекращается:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
        def factorial_recur(n: int) -> int:\n    \"\"\"Факториальная сложность (рекурсивная реализация)\"\"\"\n    if n == 0:\n        return 1\n    count = 0\n    # Из одного получается n\n    for _ in range(n):\n        count += factorial_recur(n - 1)\n    return count\n
        time_complexity.cpp
        /* Факториальная сложность (рекурсивная реализация) */\nint factorialRecur(int n) {\n    if (n == 0)\n        return 1;\n    int count = 0;\n    // Из одного получается n\n    for (int i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
        time_complexity.java
        /* Факториальная сложность (рекурсивная реализация) */\nint factorialRecur(int n) {\n    if (n == 0)\n        return 1;\n    int count = 0;\n    // Из одного получается n\n    for (int i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
        time_complexity.cs
        /* Факториальная сложность (рекурсивная реализация) */\nint FactorialRecur(int n) {\n    if (n == 0) return 1;\n    int count = 0;\n    // Из одного получается n\n    for (int i = 0; i < n; i++) {\n        count += FactorialRecur(n - 1);\n    }\n    return count;\n}\n
        time_complexity.go
        /* Факториальная сложность (рекурсивная реализация) */\nfunc factorialRecur(n int) int {\n    if n == 0 {\n        return 1\n    }\n    count := 0\n    // Из одного получается n\n    for i := 0; i < n; i++ {\n        count += factorialRecur(n - 1)\n    }\n    return count\n}\n
        time_complexity.swift
        /* Факториальная сложность (рекурсивная реализация) */\nfunc factorialRecur(n: Int) -> Int {\n    if n == 0 {\n        return 1\n    }\n    var count = 0\n    // Из одного получается n\n    for _ in 0 ..< n {\n        count += factorialRecur(n: n - 1)\n    }\n    return count\n}\n
        time_complexity.js
        /* Факториальная сложность (рекурсивная реализация) */\nfunction factorialRecur(n) {\n    if (n === 0) return 1;\n    let count = 0;\n    // Из одного получается n\n    for (let i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
        time_complexity.ts
        /* Факториальная сложность (рекурсивная реализация) */\nfunction factorialRecur(n: number): number {\n    if (n === 0) return 1;\n    let count = 0;\n    // Из одного получается n\n    for (let i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
        time_complexity.dart
        /* Факториальная сложность (рекурсивная реализация) */\nint factorialRecur(int n) {\n  if (n == 0) return 1;\n  int count = 0;\n  // Из одного получается n\n  for (var i = 0; i < n; i++) {\n    count += factorialRecur(n - 1);\n  }\n  return count;\n}\n
        time_complexity.rs
        /* Факториальная сложность (рекурсивная реализация) */\nfn factorial_recur(n: i32) -> i32 {\n    if n == 0 {\n        return 1;\n    }\n    let mut count = 0;\n    // Из одного получается n\n    for _ in 0..n {\n        count += factorial_recur(n - 1);\n    }\n    count\n}\n
        time_complexity.c
        /* Факториальная сложность (рекурсивная реализация) */\nint factorialRecur(int n) {\n    if (n == 0)\n        return 1;\n    int count = 0;\n    for (int i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
        time_complexity.kt
        /* Факториальная сложность (рекурсивная реализация) */\nfun factorialRecur(n: Int): Int {\n    if (n == 0)\n        return 1\n    var count = 0\n    // Из одного получается n\n    for (i in 0..<n) {\n        count += factorialRecur(n - 1)\n    }\n    return count\n}\n
        time_complexity.rb
        ### Линейно-логарифмическая сложность ###\ndef linear_log_recur(n)\n  return 1 unless n > 1\n\n  count = linear_log_recur(n / 2) + linear_log_recur(n / 2)\n  (0...n).each { count += 1 }\n\n  count\nend\n\n# ## Факториальная сложность (рекурсивная реализация) ###\ndef factorial_recur(n)\n  return 1 if n == 0\n\n  count = 0\n  # Из одного получается n\n  (0...n).each { count += factorial_recur(n - 1) }\n\n  count\nend\n
        Визуализация кода

        Во весь экран >

        Рисунок 2-14   Факториальная временная сложность

        Следует отметить, что поскольку при \\(n \\geq 4\\) всегда выполняется \\(n! > 2^n\\) , факториальная сложность растет еще быстрее, чем экспоненциальная, и при больших \\(n\\) становится неприемлемой.

        ","path":["Глава 2. Анализ сложности","2.3   Временная сложность"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#235","level":2,"title":"2.3.5   Худшая, лучшая и средняя временная сложность","text":"

        Временная эффективность алгоритма часто не фиксирована, а зависит от распределения входных данных. Предположим, на вход подается массив nums длины \\(n\\) , состоящий из чисел от \\(1\\) до \\(n\\) , каждое из которых встречается ровно один раз. При этом порядок элементов случайно перемешан. Задача состоит в том, чтобы вернуть индекс элемента \\(1\\) . Тогда можно сделать следующие выводы.

        • Когда nums = [?, ?, ..., 1] , то есть когда последний элемент равен \\(1\\) , нужно полностью пройти по массиву, что дает худшую временную сложность \\(O(n)\\) .
        • Когда nums = [1, ?, ?, ...] , то есть когда первый элемент равен \\(1\\) , независимо от длины массива продолжать обход не нужно, что дает лучшую временную сложность \\(\\Omega(1)\\) .

        Худшая временная сложность соответствует асимптотической верхней границе функции и обозначается нотацией Big \\(O\\) . Соответственно, лучшая временная сложность соответствует асимптотической нижней границе функции и обозначается символом \\(\\Omega\\) :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby worst_best_time_complexity.py
        def random_numbers(n: int) -> list[int]:\n    \"\"\"Сгенерировать массив с элементами 1, 2, ..., n в случайном порядке\"\"\"\n    # Создать массив nums =: 1, 2, 3, ..., n\n    nums = [i for i in range(1, n + 1)]\n    # Случайно перемешать элементы массива\n    random.shuffle(nums)\n    return nums\n\ndef find_one(nums: list[int]) -> int:\n    \"\"\"Найти индекс числа 1 в массиве nums\"\"\"\n    for i in range(len(nums)):\n        # Когда элемент 1 находится в начале массива, достигается лучшая временная сложность O(1)\n        # Когда элемент 1 находится в конце массива, достигается худшая временная сложность O(n)\n        if nums[i] == 1:\n            return i\n    return -1\n
        worst_best_time_complexity.cpp
        /* Создать массив с элементами { 1, 2, ..., n } в случайном порядке */\nvector<int> randomNumbers(int n) {\n    vector<int> nums(n);\n    // Создать массив nums = { 1, 2, 3, ..., n }\n    for (int i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // Использовать системное время для генерации случайного seed\n    unsigned seed = chrono::system_clock::now().time_since_epoch().count();\n    // Случайно перемешать элементы массива\n    shuffle(nums.begin(), nums.end(), default_random_engine(seed));\n    return nums;\n}\n\n/* Найти индекс числа 1 в массиве nums */\nint findOne(vector<int> &nums) {\n    for (int i = 0; i < nums.size(); i++) {\n        // Когда элемент 1 находится в начале массива, достигается лучшая временная сложность O(1)\n        // Когда элемент 1 находится в конце массива, достигается худшая временная сложность O(n)\n        if (nums[i] == 1)\n            return i;\n    }\n    return -1;\n}\n
        worst_best_time_complexity.java
        /* Создать массив с элементами { 1, 2, ..., n } в случайном порядке */\nint[] randomNumbers(int n) {\n    Integer[] nums = new Integer[n];\n    // Создать массив nums = { 1, 2, 3, ..., n }\n    for (int i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // Случайно перемешать элементы массива\n    Collections.shuffle(Arrays.asList(nums));\n    // Integer[] -> int[]\n    int[] res = new int[n];\n    for (int i = 0; i < n; i++) {\n        res[i] = nums[i];\n    }\n    return res;\n}\n\n/* Найти индекс числа 1 в массиве nums */\nint findOne(int[] nums) {\n    for (int i = 0; i < nums.length; i++) {\n        // Когда элемент 1 находится в начале массива, достигается лучшая временная сложность O(1)\n        // Когда элемент 1 находится в конце массива, достигается худшая временная сложность O(n)\n        if (nums[i] == 1)\n            return i;\n    }\n    return -1;\n}\n
        worst_best_time_complexity.cs
        /* Создать массив с элементами { 1, 2, ..., n } в случайном порядке */\nint[] RandomNumbers(int n) {\n    int[] nums = new int[n];\n    // Создать массив nums = { 1, 2, 3, ..., n }\n    for (int i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n\n    // Случайно перемешать элементы массива\n    for (int i = 0; i < nums.Length; i++) {\n        int index = new Random().Next(i, nums.Length);\n        (nums[i], nums[index]) = (nums[index], nums[i]);\n    }\n    return nums;\n}\n\n/* Найти индекс числа 1 в массиве nums */\nint FindOne(int[] nums) {\n    for (int i = 0; i < nums.Length; i++) {\n        // Когда элемент 1 находится в начале массива, достигается лучшая временная сложность O(1)\n        // Когда элемент 1 находится в конце массива, достигается худшая временная сложность O(n)\n        if (nums[i] == 1)\n            return i;\n    }\n    return -1;\n}\n
        worst_best_time_complexity.go
        /* Создать массив с элементами { 1, 2, ..., n } в случайном порядке */\nfunc randomNumbers(n int) []int {\n    nums := make([]int, n)\n    // Создать массив nums = { 1, 2, 3, ..., n }\n    for i := 0; i < n; i++ {\n        nums[i] = i + 1\n    }\n    // Случайно перемешать элементы массива\n    rand.Shuffle(len(nums), func(i, j int) {\n        nums[i], nums[j] = nums[j], nums[i]\n    })\n    return nums\n}\n\n/* Найти индекс числа 1 в массиве nums */\nfunc findOne(nums []int) int {\n    for i := 0; i < len(nums); i++ {\n        // Когда элемент 1 находится в начале массива, достигается лучшая временная сложность O(1)\n        // Когда элемент 1 находится в конце массива, достигается худшая временная сложность O(n)\n        if nums[i] == 1 {\n            return i\n        }\n    }\n    return -1\n}\n
        worst_best_time_complexity.swift
        /* Создать массив с элементами { 1, 2, ..., n } в случайном порядке */\nfunc randomNumbers(n: Int) -> [Int] {\n    // Создать массив nums = { 1, 2, 3, ..., n }\n    var nums = Array(1 ... n)\n    // Случайно перемешать элементы массива\n    nums.shuffle()\n    return nums\n}\n\n/* Найти индекс числа 1 в массиве nums */\nfunc findOne(nums: [Int]) -> Int {\n    for i in nums.indices {\n        // Когда элемент 1 находится в начале массива, достигается лучшая временная сложность O(1)\n        // Когда элемент 1 находится в конце массива, достигается худшая временная сложность O(n)\n        if nums[i] == 1 {\n            return i\n        }\n    }\n    return -1\n}\n
        worst_best_time_complexity.js
        /* Создать массив с элементами { 1, 2, ..., n } в случайном порядке */\nfunction randomNumbers(n) {\n    const nums = Array(n);\n    // Создать массив nums = { 1, 2, 3, ..., n }\n    for (let i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // Случайно перемешать элементы массива\n    for (let i = 0; i < n; i++) {\n        const r = Math.floor(Math.random() * (i + 1));\n        const temp = nums[i];\n        nums[i] = nums[r];\n        nums[r] = temp;\n    }\n    return nums;\n}\n\n/* Найти индекс числа 1 в массиве nums */\nfunction findOne(nums) {\n    for (let i = 0; i < nums.length; i++) {\n        // Когда элемент 1 находится в начале массива, достигается лучшая временная сложность O(1)\n        // Когда элемент 1 находится в конце массива, достигается худшая временная сложность O(n)\n        if (nums[i] === 1) {\n            return i;\n        }\n    }\n    return -1;\n}\n
        worst_best_time_complexity.ts
        /* Создать массив с элементами { 1, 2, ..., n } в случайном порядке */\nfunction randomNumbers(n: number): number[] {\n    const nums = Array(n);\n    // Создать массив nums = { 1, 2, 3, ..., n }\n    for (let i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // Случайно перемешать элементы массива\n    for (let i = 0; i < n; i++) {\n        const r = Math.floor(Math.random() * (i + 1));\n        const temp = nums[i];\n        nums[i] = nums[r];\n        nums[r] = temp;\n    }\n    return nums;\n}\n\n/* Найти индекс числа 1 в массиве nums */\nfunction findOne(nums: number[]): number {\n    for (let i = 0; i < nums.length; i++) {\n        // Когда элемент 1 находится в начале массива, достигается лучшая временная сложность O(1)\n        // Когда элемент 1 находится в конце массива, достигается худшая временная сложность O(n)\n        if (nums[i] === 1) {\n            return i;\n        }\n    }\n    return -1;\n}\n
        worst_best_time_complexity.dart
        /* Создать массив с элементами { 1, 2, ..., n } в случайном порядке */\nList<int> randomNumbers(int n) {\n  final nums = List.filled(n, 0);\n  // Создать массив nums = { 1, 2, 3, ..., n }\n  for (var i = 0; i < n; i++) {\n    nums[i] = i + 1;\n  }\n  // Случайно перемешать элементы массива\n  nums.shuffle();\n\n  return nums;\n}\n\n/* Найти индекс числа 1 в массиве nums */\nint findOne(List<int> nums) {\n  for (var i = 0; i < nums.length; i++) {\n    // Когда элемент 1 находится в начале массива, достигается лучшая временная сложность O(1)\n    // Когда элемент 1 находится в конце массива, достигается худшая временная сложность O(n)\n    if (nums[i] == 1) return i;\n  }\n\n  return -1;\n}\n
        worst_best_time_complexity.rs
        /* Создать массив с элементами { 1, 2, ..., n } в случайном порядке */\nfn random_numbers(n: i32) -> Vec<i32> {\n    // Создать массив nums = { 1, 2, 3, ..., n }\n    let mut nums = (1..=n).collect::<Vec<i32>>();\n    // Случайно перемешать элементы массива\n    nums.shuffle(&mut thread_rng());\n    nums\n}\n\n/* Найти индекс числа 1 в массиве nums */\nfn find_one(nums: &[i32]) -> Option<usize> {\n    for i in 0..nums.len() {\n        // Когда элемент 1 находится в начале массива, достигается лучшая временная сложность O(1)\n        // Когда элемент 1 находится в конце массива, достигается худшая временная сложность O(n)\n        if nums[i] == 1 {\n            return Some(i);\n        }\n    }\n    None\n}\n
        worst_best_time_complexity.c
        /* Создать массив с элементами { 1, 2, ..., n } в случайном порядке */\nint *randomNumbers(int n) {\n    // Выделить память в куче (создать одномерный массив переменной длины: число элементов равно n, тип элементов — int)\n    int *nums = (int *)malloc(n * sizeof(int));\n    // Создать массив nums = { 1, 2, 3, ..., n }\n    for (int i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // Случайно перемешать элементы массива\n    for (int i = n - 1; i > 0; i--) {\n        int j = rand() % (i + 1);\n        int temp = nums[i];\n        nums[i] = nums[j];\n        nums[j] = temp;\n    }\n    return nums;\n}\n\n/* Найти индекс числа 1 в массиве nums */\nint findOne(int *nums, int n) {\n    for (int i = 0; i < n; i++) {\n        // Когда элемент 1 находится в начале массива, достигается лучшая временная сложность O(1)\n        // Когда элемент 1 находится в конце массива, достигается худшая временная сложность O(n)\n        if (nums[i] == 1)\n            return i;\n    }\n    return -1;\n}\n
        worst_best_time_complexity.kt
        /* Создать массив с элементами { 1, 2, ..., n } в случайном порядке */\nfun randomNumbers(n: Int): Array<Int?> {\n    val nums = IntArray(n)\n    // Создать массив nums = { 1, 2, 3, ..., n }\n    for (i in 0..<n) {\n        nums[i] = i + 1\n    }\n    // Случайно перемешать элементы массива\n    nums.shuffle()\n    val res = arrayOfNulls<Int>(n)\n    for (i in 0..<n) {\n        res[i] = nums[i]\n    }\n    return res\n}\n\n/* Найти индекс числа 1 в массиве nums */\nfun findOne(nums: Array<Int?>): Int {\n    for (i in nums.indices) {\n        // Когда элемент 1 находится в начале массива, достигается лучшая временная сложность O(1)\n        // Когда элемент 1 находится в конце массива, достигается худшая временная сложность O(n)\n        if (nums[i] == 1)\n            return i\n    }\n    return -1\n}\n
        worst_best_time_complexity.rb
        ### Создать массив с элементами: 1, 2, ..., n в случайном порядке ###\ndef random_numbers(n)\n  # Создать массив nums =: 1, 2, 3, ..., n\n  nums = Array.new(n) { |i| i + 1 }\n  # Случайно перемешать элементы массива\n  nums.shuffle!\nend\n\n### Найти индекс числа 1 в массиве nums ###\ndef find_one(nums)\n  for i in 0...nums.length\n    # Когда элемент 1 находится в начале массива, достигается лучшая временная сложность O(1)\n    # Когда элемент 1 находится в конце массива, достигается худшая временная сложность O(n)\n    return i if nums[i] == 1\n  end\n\n  -1\nend\n
        Визуализация кода

        Во весь экран >

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

        Из приведенного выше примера видно, что худшая и лучшая временные сложности возникают только при особых распределениях данных. Вероятность таких случаев может быть низкой, и они не всегда реально отражают эффективность алгоритма. Напротив, средняя временная сложность способна показать эффективность алгоритма на случайных входных данных и обозначается символом \\(\\Theta\\) .

        Для некоторых алгоритмов можно относительно просто вывести средний случай при случайном распределении данных. Например, в приведенном выше примере входной массив перемешан, а вероятность появления элемента \\(1\\) на любом индексе одинакова. Следовательно, среднее число итераций алгоритма равно половине длины массива, то есть \\(n / 2\\) , а средняя временная сложность равна \\(\\Theta(n / 2) = \\Theta(n)\\) .

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

        Почему символ \\(\\Theta\\) встречается так редко?

        Возможно, потому что символ \\(O\\) звучит слишком привычно, и мы часто используем его для обозначения средней временной сложности. Но строго говоря, это некорректно. В этой книге и в других материалах, если встретится выражение вроде «средняя временная сложность \\(O(n)\\)», просто понимай его как \\(\\Theta(n)\\) .

        ","path":["Глава 2. Анализ сложности","2.3   Временная сложность"],"tags":[]},{"location":"chapter_data_structure/","level":1,"title":"Глава 3.   Структуры данных","text":"

        Abstract

        Структуры данных подобны прочному и многообразному каркасу.

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

        ","path":["Глава 3. Структуры данных","Глава 3.   Структуры данных"],"tags":[]},{"location":"chapter_data_structure/#_1","level":2,"title":"Содержание главы","text":"
        • 3.1   Классификация структур данных
        • 3.2   Базовые типы данных
        • 3.3   Кодирование чисел *
        • 3.4   Кодирование символов *
        • 3.5   Резюме
        ","path":["Глава 3. Структуры данных","Глава 3.   Структуры данных"],"tags":[]},{"location":"chapter_data_structure/basic_data_types/","level":1,"title":"3.2   Базовые типы данных","text":"

        Когда речь заходит о данных в компьютере, мы в первую очередь вспоминаем текст, изображения, видео, звук, 3D-модели и многие другие формы представления информации. Хотя способы организации этих данных различаются, все они строятся из базовых типов данных.

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

        • Целочисленные типы byte , short , int , long .
        • Типы с плавающей точкой float , double , используемые для представления дробных чисел.
        • Символьный тип char , используемый для представления букв, знаков препинания и даже эмодзи в разных языках.
        • Логический тип bool , используемый для представления суждений «да» и «нет».

        Базовые типы данных хранятся в компьютере в двоичной форме. Один двоичный разряд равен \\(1\\) биту. В большинстве современных операционных систем \\(1\\) байт (byte) состоит из \\(8\\) битов (bit).

        Диапазон значений базовых типов данных зависит от объема занимаемого ими пространства. Ниже в качестве примера используется Java.

        • Целочисленный тип byte занимает \\(1\\) байт = \\(8\\) бит и может представлять \\(2^{8}\\) чисел.
        • Целочисленный тип int занимает \\(4\\) байта = \\(32\\) бита и может представлять \\(2^{32}\\) чисел.

        В таблице 3-1 перечислены объем памяти, диапазон значений и значения по умолчанию для различных базовых типов данных в Java. Эту таблицу не нужно заучивать наизусть. Достаточно иметь общее представление и при необходимости обращаться к ней.

        Таблица 3-1   Объем памяти и диапазоны значений базовых типов данных

        Тип Обозначение Объем памяти Минимальное значение Максимальное значение Значение по умолчанию Целые byte 1 байт \\(-2^7\\) (\\(-128\\)) \\(2^7 - 1\\) (\\(127\\)) \\(0\\) short 2 байта \\(-2^{15}\\) \\(2^{15} - 1\\) \\(0\\) int 4 байта \\(-2^{31}\\) \\(2^{31} - 1\\) \\(0\\) long 8 байт \\(-2^{63}\\) \\(2^{63} - 1\\) \\(0\\) Вещественные float 4 байта \\(1.175 \\times 10^{-38}\\) \\(3.403 \\times 10^{38}\\) \\(0.0\\text{f}\\) double 8 байт \\(2.225 \\times 10^{-308}\\) \\(1.798 \\times 10^{308}\\) \\(0.0\\) Символы char 2 байта \\(0\\) \\(2^{16} - 1\\) \\(0\\) Логические bool 1 байт \\(\\text{false}\\) \\(\\text{true}\\) \\(\\text{false}\\)

        Обрати внимание: в таблице 3-1 приведены данные именно для базовых типов данных Java. В каждом языке программирования свои определения типов, поэтому объем памяти, диапазон значений и значения по умолчанию могут различаться.

        • В Python целочисленный тип int может иметь произвольный размер, ограниченный только доступной памятью. Тип float является 64-битным числом двойной точности. Типа char нет, а отдельный символ на деле является строкой str длины 1.
        • В C и C++ размер базовых типов данных явно не зафиксирован и зависит от реализации и платформы. В таблице 3-1 приведены данные для модели LP64 data model, используемой в 64-битных Unix-системах, включая Linux и macOS.
        • Размер символа char в C и C++ составляет 1 байт, а в большинстве других языков программирования зависит от конкретного способа кодирования символов. Подробнее это рассматривается в разделе «Кодирование символов».
        • Хотя для представления логического значения достаточно 1 бита ( \\(0\\) или \\(1\\) ), в памяти оно обычно хранится как 1 байт. Это связано с тем, что современные CPU обычно используют 1 байт как минимальную адресуемую единицу памяти.

        Какова же связь между базовыми типами данных и структурами данных? Мы знаем, что структура данных - это способ организации и хранения данных в компьютере. Подлежащее в этой фразе - «структура», а не «данные».

        Если мы хотим представить «ряд чисел», то естественно подумаем об использовании массива. Это связано с тем, что линейная структура массива может выразить отношения соседства и порядка между числами, а то, что именно хранится внутри - целые int , вещественные float или символы char , - к «структуре данных» отношения не имеет.

        Иными словами, базовые типы данных задают «тип содержимого» данных, а структуры данных задают «способ организации» данных. Например, в следующем коде мы используем одну и ту же структуру данных (массив) для хранения и представления различных базовых типов данных, включая int , float , char , bool и т.д.

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
        # Инициализируем массивы с использованием различных базовых типов данных\nnumbers: list[int] = [0] * 5\ndecimals: list[float] = [0.0] * 5\n# В Python символы фактически являются строками длины 1\ncharacters: list[str] = ['0'] * 5\nbools: list[bool] = [False] * 5\n# Списки Python могут свободно хранить различные базовые типы данных и ссылки на объекты\ndata = [0, 0.0, 'a', False, ListNode(0)]\n
        // Инициализируем массивы с использованием различных базовых типов данных\nint numbers[5];\nfloat decimals[5];\nchar characters[5];\nbool bools[5];\n
        // Инициализируем массивы с использованием различных базовых типов данных\nint[] numbers = new int[5];\nfloat[] decimals = new float[5];\nchar[] characters = new char[5];\nboolean[] bools = new boolean[5];\n
        // Инициализируем массивы с использованием различных базовых типов данных\nint[] numbers = new int[5];\nfloat[] decimals = new float[5];\nchar[] characters = new char[5];\nbool[] bools = new bool[5];\n
        // Инициализируем массивы с использованием различных базовых типов данных\nvar numbers = [5]int{}\nvar decimals = [5]float64{}\nvar characters = [5]byte{}\nvar bools = [5]bool{}\n
        // Инициализируем массивы с использованием различных базовых типов данных\nlet numbers = Array(repeating: 0, count: 5)\nlet decimals = Array(repeating: 0.0, count: 5)\nlet characters: [Character] = Array(repeating: \"a\", count: 5)\nlet bools = Array(repeating: false, count: 5)\n
        // Массивы JavaScript могут свободно хранить различные базовые типы данных и объекты\nconst array = [0, 0.0, 'a', false];\n
        // Инициализируем массивы с использованием различных базовых типов данных\nconst numbers: number[] = [];\nconst characters: string[] = [];\nconst bools: boolean[] = [];\n
        // Инициализируем массивы с использованием различных базовых типов данных\nList<int> numbers = List.filled(5, 0);\nList<double> decimals = List.filled(5, 0.0);\nList<String> characters = List.filled(5, 'a');\nList<bool> bools = List.filled(5, false);\n
        // Инициализируем массивы с использованием различных базовых типов данных\nlet numbers: Vec<i32> = vec![0; 5];\nlet decimals: Vec<f32> = vec![0.0; 5];\nlet characters: Vec<char> = vec!['0'; 5];\nlet bools: Vec<bool> = vec![false; 5];\n
        // Инициализируем массивы с использованием различных базовых типов данных\nint numbers[10];\nfloat decimals[10];\nchar characters[10];\nbool bools[10];\n
        // Инициализируем массивы с использованием различных базовых типов данных\nval numbers = IntArray(5)\nval decinals = FloatArray(5)\nval characters = CharArray(5)\nval bools = BooleanArray(5)\n
        # Списки Ruby могут свободно хранить различные базовые типы данных и ссылки на объекты\ndata = [0, 0.0, 'a', false, ListNode(0)]\n
        Визуализация выполнения

        https://pythontutor.com/render.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%D1%81%D0%B2%D1%8F%D0%B7%D0%BD%D1%8B%D0%B9%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%D1%83%D0%B7%D0%B5%D0%BB%D0%BA%D0%BB%D0%B0%D1%81%D1%81%22%22%22%0A%20%20%20%20def%20__init__%28self%2C%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%D0%97%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%D1%83%D0%B7%D0%BB%D0%B0%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%D0%A1%D1%81%D1%8B%D0%BB%D0%BA%D0%B0%20%D0%BD%D0%B0%20%D1%81%D0%BB%D0%B5%D0%B4%D1%83%D1%8E%D1%89%D0%B8%D0%B9%20%D1%83%D0%B7%D0%B5%D0%BB%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D0%BC%D0%B0%D1%81%D1%81%D0%B8%D0%B2%20%D1%81%20%D0%B8%D1%81%D0%BF%D0%BE%D0%BB%D1%8C%D0%B7%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5%D0%BC%20%D0%BD%D0%B5%D1%81%D0%BA%D0%BE%D0%BB%D1%8C%D0%BA%D0%B8%D1%85%20%D0%B1%D0%B0%D0%B7%D0%BE%D0%B2%D1%8B%D1%85%20%D1%82%D0%B8%D0%BF%D0%BE%D0%B2%20%D0%B4%D0%B0%D0%BD%D0%BD%D1%8B%D1%85%0A%20%20%20%20numbers%20%3D%20%5B0%5D%20%2A%205%0A%20%20%20%20decimals%20%3D%20%5B0.0%5D%20%2A%205%0A%20%20%20%20%23%20%D0%92%20Python%20%D1%81%D0%B8%D0%BC%D0%B2%D0%BE%D0%BB%D1%8B%20%D0%BD%D0%B0%20%D1%81%D0%B0%D0%BC%D0%BE%D0%BC%20%D0%B4%D0%B5%D0%BB%D0%B5%20%D1%8F%D0%B2%D0%BB%D1%8F%D1%8E%D1%82%D1%81%D1%8F%20%D1%81%D1%82%D1%80%D0%BE%D0%BA%D0%B0%D0%BC%D0%B8%20%D0%B4%D0%BB%D0%B8%D0%BD%D1%8B%201%0A%20%20%20%20characters%20%3D%20%5B%270%27%5D%20%2A%205%0A%20%20%20%20bools%20%3D%20%5BFalse%5D%20%2A%205%0A%20%20%20%20%23%20%D0%A1%D0%BF%D0%B8%D1%81%D0%BA%D0%B8%20%D0%B2%20Python%20%D0%BC%D0%BE%D0%B3%D1%83%D1%82%20%D1%81%D0%B2%D0%BE%D0%B1%D0%BE%D0%B4%D0%BD%D0%BE%20%D1%85%D1%80%D0%B0%D0%BD%D0%B8%D1%82%D1%8C%20%D1%80%D0%B0%D0%B7%D0%BB%D0%B8%D1%87%D0%BD%D1%8B%D0%B5%20%D0%B1%D0%B0%D0%B7%D0%BE%D0%B2%D1%8B%D0%B5%20%D1%82%D0%B8%D0%BF%D1%8B%20%D0%B4%D0%B0%D0%BD%D0%BD%D1%8B%D1%85%20%D0%B8%20%D1%81%D1%81%D1%8B%D0%BB%D0%BA%D0%B8%20%D0%BD%D0%B0%20%D0%BE%D0%B1%D1%8A%D0%B5%D0%BA%D1%82%D1%8B%0A%20%20%20%20data%20%3D%20%5B0%2C%200.0%2C%20%27a%27%2C%20False%2C%20ListNode%280%29%5D&cumulative=false&curInstr=12&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

        ","path":["Глава 3. Структуры данных","3.2   Базовые типы данных"],"tags":[]},{"location":"chapter_data_structure/character_encoding/","level":1,"title":"3.4   Кодирование символов *","text":"

        В компьютере все данные хранятся в виде двоичных чисел, и символьный тип данных char не является исключением. Для представления символов необходимо задать «таблицу символов», которая устанавливает взаимно-однозначное соответствие между каждым символом и двоичным числом. С помощью этой таблицы компьютер может преобразовывать двоичные числа в символы.

        ","path":["Глава 3. Структуры данных","3.4   Кодирование символов *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#341-ascii","level":2,"title":"3.4.1   Таблица символов ASCII","text":"

        Код ASCII - это самая ранняя таблица символов. Ее полное название - American Standard Code for Information Interchange (американский стандартный код обмена информацией). Для представления символов в ней используются 7 двоичных битов (нижние 7 битов одного байта), что позволяет закодировать до 128 различных символов. Как показано на рисунке 3-6, ASCII включает заглавные и строчные буквы английского алфавита, цифры 0 ~ 9, некоторые знаки препинания, а также некоторые управляющие символы (например перевод строки и табуляцию).

        Рисунок 3-6   Таблица ASCII

        Однако код ASCII может представлять только английский язык. С развитием компьютерных технологий появилась таблица символов EASCII, способная охватывать больше языков. Она расширяет 7-битную основу ASCII до 8 битов и может представлять 256 различных символов.

        Во всем мире постепенно появились разные таблицы EASCII, подходящие для разных регионов. Первые 128 символов в этих таблицах одинаковы и соответствуют ASCII, а последние 128 символов определяются по-разному, чтобы удовлетворять потребностям разных языков.

        ","path":["Глава 3. Структуры данных","3.4   Кодирование символов *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#342-gbk","level":2,"title":"3.4.2   Таблица символов GBK","text":"

        Позже люди обнаружили, что кодов EASCII все равно недостаточно для количества символов во многих языках. Например, китайских иероглифов существует почти сто тысяч, а в повседневном употреблении нужны тысячи. В 1980 году Государственное управление стандартов Китая выпустило таблицу символов GB2312, включающую 6763 иероглифа, что в основном удовлетворило потребности компьютерной обработки китайского текста.

        Однако GB2312 не умеет работать с некоторыми редкими иероглифами и традиционными формами письма. Таблица символов GBK представляет собой расширение GB2312 и в общей сложности содержит 21886 иероглифов. В схеме кодирования GBK символы ASCII представляются одним байтом, а китайские иероглифы - двумя байтами.

        ","path":["Глава 3. Структуры данных","3.4   Кодирование символов *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#343-unicode","level":2,"title":"3.4.3   Таблица символов Unicode","text":"

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

        Исследователи той эпохи задумались: если создать достаточно полную таблицу символов, которая включит все языки и знаки мира, разве это не решит проблемы многоязычной среды и искаженного текста? Под влиянием этой идеи и появилась большая и всеобъемлющая таблица символов Unicode.

        Unicode по-китайски называется «единый код» и теоретически способен вместить более миллиона символов. Его цель - собрать символы со всего мира в единую таблицу символов, предоставить универсальный стандарт для обработки и отображения текстов на разных языках и уменьшить количество проблем с искажением текста, вызванных различиями стандартов кодирования. С момента публикации в 1991 году Unicode непрерывно расширялся, добавляя новые языки и символы. По состоянию на сентябрь 2022 года Unicode уже включал 149186 символов, в том числе буквы разных языков, знаки, а также эмодзи.

        Как универсальный набор символов, Unicode по сути присваивает каждому символу уникальную «кодовую точку» (числовой идентификатор символа), диапазон которой составляет от U+0000 до U+10FFFF, образуя единое пространство нумерации символов. Однако Unicode не определяет, как именно хранить эти кодовые точки в компьютере. Тут неизбежно возникает вопрос: если в одном тексте одновременно встречаются кодовые точки Unicode разной длины, как система должна разбирать символы? Например, если дан код длиной 2 байта, как понять, является ли это одним 2-байтовым символом или двумя 1-байтовыми?

        Для этой проблемы прямолинейное решение состоит в том, чтобы хранить все символы в кодировке одинаковой длины. Как показано на рисунке 3-7, каждый символ в «Hello» занимает 1 байт, а каждый символ в «алгоритм» занимает 2 байта. Мы можем дополнить старшие биты нулями и закодировать все символы в «Hello алгоритм» в виде 2-байтовых единиц. Тогда система сможет считывать по одному символу каждые 2 байта и восстановить эту фразу.

        Рисунок 3-7   Пример кодирования Unicode

        Однако ASCII уже показал нам, что для кодирования английского текста достаточно 1 байта. Если использовать описанную выше схему, английский текст будет занимать вдвое больше памяти, чем при ASCII, а это очень неэффективно. Поэтому нам нужен более эффективный способ кодирования Unicode.

        ","path":["Глава 3. Структуры данных","3.4   Кодирование символов *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#344-utf-8","level":2,"title":"3.4.4   Кодировка UTF-8","text":"

        Сегодня UTF-8 стала самым широко используемым способом кодирования Unicode в мире. Это кодировка переменной длины, использующая от 1 до 4 байт на символ в зависимости от его сложности. Символам ASCII нужен только 1 байт, латинским и греческим буквам - 2 байта, часто используемым китайским символам - 3 байта, а некоторым редким символам - 4 байта.

        Правила кодирования UTF-8 не слишком сложны и делятся на два случая.

        • Для символов длиной 1 байт старший бит устанавливается в \\(0\\) , а оставшиеся 7 битов содержат кодовую точку Unicode. Стоит отметить, что символы ASCII занимают первые 128 кодовых точек в наборе Unicode. Иными словами, кодировка UTF-8 обратно совместима с ASCII. Это означает, что мы можем использовать UTF-8 для разбора очень старых ASCII-текстов.
        • Для символов длиной \\(n\\) байт (где \\(n > 1\\)) старшие \\(n\\) битов первого байта устанавливаются в \\(1\\) , а \\((n + 1)\\)-й бит устанавливается в \\(0\\). Начиная со второго байта, старшие 2 бита каждого байта устанавливаются в \\(10\\). Все остальные биты используются для заполнения кодовой точки Unicode соответствующего символа.

        На рисунке 3-8 показана UTF-8-кодировка для строки «Hello алгоритм». Можно заметить, что поскольку старшие \\(n\\) битов установлены в \\(1\\) , система может определить длину символа как \\(n\\) , подсчитав число ведущих единиц.

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

        Причина выбора \\(10\\) в качестве контрольного маркера в том, что по правилам UTF-8 символ не может иметь старшие два бита, равные \\(10\\) . Это можно доказать от противного: если предположить, что у некоторого символа старшие два бита равны \\(10\\) , то длина такого символа должна быть 1 байт, то есть это ASCII. Но у ASCII старший бит обязан быть \\(0\\) , что противоречит предположению.

        Рисунок 3-8   Пример кодировки UTF-8

        Помимо UTF-8, распространены еще два следующих способа кодирования.

        • Кодировка UTF-16: использует 2 или 4 байта для представления символа. Все символы ASCII и часто используемые неанглийские символы представляются 2 байтами. Небольшая часть символов требует 4 байта. Для 2-байтовых символов кодировка UTF-16 совпадает с кодовой точкой Unicode.
        • Кодировка UTF-32: каждый символ занимает 4 байта. Это означает, что UTF-32 требует больше места, чем UTF-8 и UTF-16, особенно в текстах с большой долей ASCII-символов.

        С точки зрения занимаемого места UTF-8 очень эффективна для английских символов, потому что им нужен всего 1 байт. А для некоторых неанглийских символов (например китайских) UTF-16 может быть эффективнее, потому что ей требуется только 2 байта, тогда как UTF-8 может потребовать 3 байта.

        С точки зрения совместимости у UTF-8 наилучшая универсальность, и многие инструменты и библиотеки в первую очередь поддерживают именно UTF-8.

        ","path":["Глава 3. Структуры данных","3.4   Кодирование символов *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#345","level":2,"title":"3.4.5   Кодирование символов в языках программирования","text":"

        Для большинства языков программирования прошлого строки во время выполнения программы использовали фиксированные по длине кодировки, такие как UTF-16 или UTF-32. При кодировке фиксированной длины строку можно обрабатывать как массив, и такой подход дает следующие преимущества.

        • Произвольный доступ: к строкам в UTF-16 легко осуществлять произвольный доступ. UTF-8 же является кодировкой переменной длины, поэтому, чтобы найти \\(i\\) -й символ, нужно пройти от начала строки до этого символа, а это требует \\(O(n)\\) времени.
        • Подсчет длины строки: аналогично произвольному доступу, вычисление длины строки в UTF-16 - это операция \\(O(1)\\) . А вот вычисление длины строки в UTF-8 требует обхода всей строки.
        • Строковые операции: многие операции со строками (разделение, конкатенация, вставка, удаление и т.д.) над строками в UTF-16 реализуются проще. При работе с UTF-8 обычно требуются дополнительные вычисления, чтобы не породить некорректную UTF-8-последовательность.

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

        • Тип String в Java использует кодировку UTF-16, и каждый символ занимает 2 байта. Это связано с тем, что на раннем этапе проектирования Java считалось, что 16 битов достаточно для представления всех возможных символов. Но это оказалось неверным предположением. Позднее Unicode вышел за пределы 16 битов, поэтому символы в Java теперь могут представляться парой 16-битных значений (так называемой «суррогатной парой»).
        • Строки в JavaScript и TypeScript используют UTF-16 по причинам, похожим на Java. Когда Netscape впервые выпустила JavaScript в 1995 году, Unicode еще находился на ранней стадии развития, и 16-битного кодирования тогда было достаточно для представления всех символов Unicode.
        • C# использует UTF-16 главным образом потому, что платформа .NET была разработана Microsoft, а многие технологии Microsoft (включая Windows) широко используют именно UTF-16.

        Из-за недооценки общего числа символов перечисленным выше языкам пришлось использовать «суррогатные пары» для представления Unicode-символов длиной больше 16 бит. Это вынужденный компромисс. С одной стороны, в строках с суррогатными парами один символ может занимать 2 байта или 4 байта, из-за чего теряется преимущество кодировки фиксированной длины. С другой стороны, обработка суррогатных пар требует дополнительного кода, что повышает сложность разработки и отладки.

        По этим причинам некоторые языки программирования предложили иные схемы кодирования.

        • str в Python использует Unicode и гибкое строковое представление, где длина хранимого символа зависит от наибольшей кодовой точки Unicode в строке. Если все символы строки принадлежат ASCII, каждый символ занимает 1 байт. Если есть символы за пределами ASCII, но все они лежат в базовой многоязычной плоскости (BMP), каждый символ занимает 2 байта. Если встречаются символы за пределами BMP, каждый символ занимает 4 байта.
        • Тип string в Go внутри использует кодировку UTF-8. Язык Go также предоставляет тип rune, предназначенный для представления одной кодовой точки Unicode.
        • Типы str и String в Rust внутри используют UTF-8. В Rust также есть тип char, представляющий одну кодовую точку Unicode.

        Следует помнить, что выше обсуждался способ хранения строк внутри языков программирования, а это не то же самое, что хранение строк в файлах или передача их по сети. При файловом хранении и сетевой передаче мы обычно кодируем строки в формате UTF-8, чтобы получить наилучшую совместимость и эффективность по занимаемому месту.

        ","path":["Глава 3. Структуры данных","3.4   Кодирование символов *"],"tags":[]},{"location":"chapter_data_structure/classification_of_data_structure/","level":1,"title":"3.1   Классификация структур данных","text":"

        К распространенным структурам данных относятся массивы, связные списки, стеки, очереди, хеш-таблицы, деревья, кучи и графы. Их можно классифицировать по двум измерениям: логической структуре и физической структуре.

        ","path":["Глава 3. Структуры данных","3.1   Классификация структур данных"],"tags":[]},{"location":"chapter_data_structure/classification_of_data_structure/#311","level":2,"title":"3.1.1   Логическая структура: линейная и нелинейная","text":"

        Логическая структура раскрывает логические отношения между элементами данных. В массивах и связных списках данные расположены в определенном порядке, что отражает линейные отношения между элементами. В деревьях данные расположены по уровням сверху вниз, что демонстрирует отношения «предок» и «потомок». Графы состоят из вершин и ребер, отражая сложные сетевые отношения.

        Как показано на рисунке 3-1, логические структуры делятся на две большие категории: линейные и нелинейные. Линейные структуры более интуитивны, поскольку в них данные расположены линейно и логически связаны. Нелинейные структуры, напротив, представляют собой нелинейное расположение элементов данных.

        • Линейные структуры данных: массивы, связные списки, стеки, очереди, хеш-таблицы, в которых элементы связаны отношением «один к одному».
        • Нелинейные структуры данных: деревья, кучи, графы, хеш-таблицы.

        Нелинейные структуры данных можно дополнительно разделить на древовидные и сетевые.

        • Древовидные структуры: деревья, кучи, хеш-таблицы, в которых элементы связаны отношением «один ко многим».
        • Сетевые структуры: графы, в которых элементы связаны отношением «многие ко многим».

        Рисунок 3-1   Линейные и нелинейные структуры данных

        ","path":["Глава 3. Структуры данных","3.1   Классификация структур данных"],"tags":[]},{"location":"chapter_data_structure/classification_of_data_structure/#312","level":2,"title":"3.1.2   Физическая структура: непрерывная и разрозненная","text":"

        Во время выполнения программы обрабатываемые данные в основном хранятся в памяти. На рисунке 3-2 показан модуль оперативной памяти компьютера, где каждый черный блок содержит определенный участок памяти. Память можно представить как огромную таблицу Excel, в которой каждая ячейка способна хранить данные определенного размера.

        Система обращается к данным по адресам памяти соответствующих позиций. Как показано на рисунке 3-2, компьютер по определенным правилам присваивает каждой ячейке в этой таблице номер, чтобы каждый участок памяти имел уникальный адрес. Благодаря этим адресам программа получает доступ к данным, находящимся в памяти.

        Рисунок 3-2   Планка памяти, участок памяти и адрес памяти

        Tip

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

        Память - общий ресурс для всех программ. Когда некоторый участок памяти занят одной программой, другие программы обычно не могут использовать его одновременно. Поэтому при проектировании структур данных и алгоритмов память занимает важное место. Например, пиковое потребление памяти алгоритмом не должно превышать объем доступной свободной памяти системы. Если не хватает непрерывных крупных участков памяти, выбранная структура данных должна уметь размещаться в разрозненных областях памяти.

        Как показано на рисунке 3-3, физическая структура отражает способ хранения данных в памяти компьютера. Ее можно разделить на хранение в непрерывном пространстве (массивы) и хранение в разрозненном пространстве (связные списки). Физическая структура на низком уровне определяет способы доступа к данным, их обновления, вставки и удаления. Эти два типа физических структур взаимно дополняют друг друга по временной и пространственной эффективности.

        Рисунок 3-3   Хранение в непрерывном и разрозненном пространстве

        Стоит отметить, что все структуры данных реализуются на основе массивов, связных списков или их комбинации. Например, стек и очередь можно реализовать как с помощью массивов, так и с помощью связных списков. Реализация хеш-таблицы также может одновременно включать массивы и связные списки.

        • Можно реализовать на основе массивов: стеки, очереди, хеш-таблицы, деревья, кучи, графы, матрицы, тензоры (массивы размерности \\(\\geq 3\\) ) и т.д.
        • Можно реализовать на основе связных списков: стеки, очереди, хеш-таблицы, деревья, кучи, графы и т.д.

        После инициализации длину связного списка все еще можно изменять во время выполнения программы, поэтому его также называют «динамической структурой данных». Длина массива после инициализации неизменна, поэтому его также называют «статической структурой данных». Стоит отметить, что массив может изменять длину за счет повторного выделения памяти, тем самым приобретая определенную «динамичность».

        Tip

        Если тебе пока трудно понять физическую структуру, рекомендуется сначала прочитать следующую главу, а затем вернуться к этому разделу.

        ","path":["Глава 3. Структуры данных","3.1   Классификация структур данных"],"tags":[]},{"location":"chapter_data_structure/number_encoding/","level":1,"title":"3.3   Кодирование чисел *","text":"

        Tip

        В этой книге разделы, помеченные символом *, относятся к дополнительному чтению. Если у тебя мало времени или материал кажется трудным, можно сначала пропустить их и вернуться после изучения обязательных разделов.

        ","path":["Глава 3. Структуры данных","3.3   Кодирование чисел *"],"tags":[]},{"location":"chapter_data_structure/number_encoding/#331","level":2,"title":"3.3.1   Прямой, обратный и дополнительный коды","text":"

        В таблице из предыдущего раздела можно заметить, что все целочисленные типы могут представлять на одно отрицательное число больше, чем положительных. Например, диапазон byte равен \\([-128, 127]\\) . Это выглядит не слишком интуитивно, и внутренняя причина связана с прямым, обратным и дополнительным кодами.

        Прежде всего нужно отметить, что числа хранятся в компьютере в виде «дополнительного кода». Прежде чем разбирать причины такого решения, сначала дадим определения всем трем способам представления.

        • Прямой код: старший бит двоичного представления числа рассматривается как знаковый, где \\(0\\) означает положительное число, а \\(1\\) - отрицательное. Остальные биты представляют значение числа.
        • Обратный код: для положительного числа обратный код совпадает с прямым. Для отрицательного числа он получается инверсией всех битов прямого кода, кроме знакового бита.
        • Дополнительный код: для положительного числа дополнительный код совпадает с прямым. Для отрицательного числа он получается добавлением \\(1\\) к его обратному коду.

        На рисунке 3-4 показаны способы преобразования между прямым, обратным и дополнительным кодами.

        Рисунок 3-4   Преобразования между прямым, обратным и дополнительным кодами

        Прямой код (sign-magnitude), хотя и является самым наглядным, имеет определенные ограничения. С одной стороны, прямой код отрицательных чисел нельзя напрямую использовать в вычислениях. Например, при вычислении \\(1 + (-2)\\) в прямом коде результатом будет \\(-3\\) , что, очевидно, неверно.

        \\[ \\begin{aligned} & 1 + (-2) \\newline & \\rightarrow 0000 \\. 0001 + 1000 \\. 0010 \\newline & = 1000 \\. 0011 \\newline & \\rightarrow -3 \\end{aligned} \\]

        Чтобы решить эту проблему, компьютеры ввели обратный код (1's complement). Если сначала преобразовать прямой код в обратный и выполнить вычисление \\(1 + (-2)\\) в обратном коде, а затем перевести результат обратно в прямой код, то получится правильный результат \\(-1\\) .

        \\[ \\begin{aligned} & 1 + (-2) \\newline & \\rightarrow 0000 \\. 0001 \\. \\text{(прямой код)} + 1000 \\. 0010 \\. \\text{(прямой код)} \\newline & = 0000 \\. 0001 \\. \\text{(обратный код)} + 1111 \\. 1101 \\. \\text{(обратный код)} \\newline & = 1111 \\. 1110 \\. \\text{(обратный код)} \\newline & = 1000 \\. 0001 \\. \\text{(прямой код)} \\newline & \\rightarrow -1 \\end{aligned} \\]

        С другой стороны, **в прямом коде у нуля есть два представления: \\(+0\\) и \\(-0\\) **. Это означает, что числу ноль соответствуют два разных двоичных кода, что может приводить к неоднозначности. Например, если в условном выражении не различать положительный и отрицательный ноль, можно получить ошибочный результат. А если специально обрабатывать такую неоднозначность, придется вводить дополнительные проверки, что может снизить вычислительную эффективность компьютера.

        \\[ \\begin{aligned} +0 & \\rightarrow 0000 \\. 0000 \\newline -0 & \\rightarrow 1000 \\. 0000 \\end{aligned} \\]

        Как и прямой код, обратный код тоже страдает от неоднозначности положительного и отрицательного нуля, поэтому компьютеры ввели дополнительный код (2's complement). Сначала посмотрим на процесс преобразования отрицательного нуля из прямого кода в обратный, а затем в дополнительный:

        \\[ \\begin{aligned} -0 \\rightarrow \\. & 1000 \\. 0000 \\. \\text{(прямой код)} \\newline = \\. & 1111 \\. 1111 \\. \\text{(обратный код)} \\newline = 1 \\. & 0000 \\. 0000 \\. \\text{(дополнительный код)} \\newline \\end{aligned} \\]

        При добавлении \\(1\\) к обратному коду отрицательного нуля возникает перенос, но длина типа byte составляет всего 8 бит, поэтому переполнившаяся в 9-й бит единица отбрасывается. Иными словами, дополнительный код отрицательного нуля равен \\(0000 \\. 0000\\) и совпадает с дополнительным кодом положительного нуля. Значит, в представлении дополнительного кода существует только один ноль, и проблема неоднозначности положительного и отрицательного нуля тем самым устраняется.

        Остается последний вопрос: диапазон типа byte равен \\([-128, 127]\\) , откуда берется лишнее отрицательное число \\(-128\\) ? Мы замечаем, что у всех целых чисел из интервала \\([-127, +127]\\) есть соответствующие прямой, обратный и дополнительный коды, а прямой и дополнительный коды можно преобразовывать друг в друга.

        Однако дополнительный код \\(1000 \\. 0000\\) является исключением: у него нет соответствующего прямого кода. Согласно правилу преобразования, прямой код для этого дополнительного кода должен быть равен \\(0000 \\. 0000\\) . Это очевидное противоречие, потому что такой прямой код обозначает число \\(0\\) , а его дополнительный код должен совпадать с ним самим. Компьютер просто определяет, что этот особый дополнительный код \\(1000 \\. 0000\\) представляет число \\(-128\\) . На самом деле результат вычисления \\((-1) + (-127)\\) в дополнительном коде как раз и равен \\(-128\\) .

        \\[ \\begin{aligned} & (-127) + (-1) \\newline & \\rightarrow 1111 \\. 1111 \\. \\text{(прямой код)} + 1000 \\. 0001 \\. \\text{(прямой код)} \\newline & = 1000 \\. 0000 \\. \\text{(обратный код)} + 1111 \\. 1110 \\. \\text{(обратный код)} \\newline & = 1000 \\. 0001 \\. \\text{(дополнительный код)} + 1111 \\. 1111 \\. \\text{(дополнительный код)} \\newline & = 1000 \\. 0000 \\. \\text{(дополнительный код)} \\newline & \\rightarrow -128 \\end{aligned} \\]

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

        Обрати внимание: это не означает, что компьютер умеет только складывать. Комбинируя сложение с некоторыми базовыми логическими операциями, компьютер может реализовать и другие математические операции. Например, вычитание \\(a - b\\) можно преобразовать в сложение \\(a + (-b)\\). Умножение и деление можно свести к многократному сложению или вычитанию.

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

        Идея дополнительного кода очень изящна. Из-за ограничений по объему мы на этом остановимся. Если тебе интересно, стоит изучить эту тему глубже.

        ","path":["Глава 3. Структуры данных","3.3   Кодирование чисел *"],"tags":[]},{"location":"chapter_data_structure/number_encoding/#332","level":2,"title":"3.3.2   Кодирование чисел с плавающей точкой","text":"

        Внимательный читатель может заметить: int и float имеют одинаковую длину, по 4 байта , но почему диапазон значений у float намного больше, чем у int ? Это выглядит парадоксально, ведь float должен еще представлять дробные числа, а значит диапазон вроде бы должен быть меньше.

        На самом деле это связано с тем, что число с плавающей точкой float использует другой способ представления. Обозначим двоичное число длиной 32 бита как:

        \\[ b_{31} b_{30} b_{29} \\ldots b_2 b_1 b_0 \\]

        Согласно стандарту IEEE 754, 32-битный float состоит из следующих трех частей.

        • Бит знака \\(\\mathrm{S}\\) : занимает 1 бит и соответствует \\(b_{31}\\) .
        • Биты экспоненты \\(\\mathrm{E}\\) : занимают 8 бит и соответствуют \\(b_{30} b_{29} \\ldots b_{23}\\) .
        • Биты мантиссы \\(\\mathrm{N}\\) : занимают 23 бита и соответствуют \\(b_{22} b_{21} \\ldots b_0\\) .

        Формула вычисления значения, соответствующего двоичному числу float, имеет вид:

        \\[ \\text {val} = (-1)^{b_{31}} \\times 2^{\\left(b_{30} b_{29} \\ldots b_{23}\\right)_2-127} \\times\\left(1 . b_{22} b_{21} \\ldots b_0\\right)_2 \\]

        Если перейти к десятичной записи, формула вычисления будет такой:

        \\[ \\text {val}=(-1)^{\\mathrm{S}} \\times 2^{\\mathrm{E} -127} \\times (1 + \\mathrm{N}) \\]

        Диапазоны значений соответствующих частей таковы:

        \\[ \\begin{aligned} \\mathrm{S} \\in & \\{ 0, 1\\}, \\quad \\mathrm{E} \\in \\{ 1, 2, \\dots, 254 \\} \\newline (1 + \\mathrm{N}) = & (1 + \\sum_{i=1}^{23} b_{23-i} 2^{-i}) \\subset [1, 2 - 2^{-23}] \\end{aligned} \\]

        Рисунок 3-5   Пример вычисления float по стандарту IEEE 754

        Как видно на рисунке 3-5, если взять пример \\(\\mathrm{S} = 0\\) , \\(\\mathrm{E} = 124\\) , \\(\\mathrm{N} = 2^{-2} + 2^{-3} = 0.375\\) , то получим:

        \\[ \\text { val } = (-1)^0 \\times 2^{124 - 127} \\times (1 + 0.375) = 0.171875 \\]

        Теперь мы можем ответить на исходный вопрос: в представлении float присутствуют биты экспоненты, поэтому его диапазон значений намного больше, чем у int. Согласно приведенным выше вычислениям, максимально возможное положительное число для float равно \\(2^{254 - 127} \\times (2 - 2^{-23}) \\approx 3.4 \\times 10^{38}\\). Если изменить бит знака, получим минимальное отрицательное число.

        Хотя число с плавающей точкой float расширяет диапазон значений, побочным эффектом становится потеря точности. Целочисленный тип int использует все 32 бита для представления числа, и числа распределены равномерно. А из-за существования битов экспоненты у float чем больше число, тем больше обычно становится разница между двумя соседними представимыми значениями.

        Как показано в таблице 3-2, значения экспоненты \\(\\mathrm{E} = 0\\) и \\(\\mathrm{E} = 255\\) имеют специальный смысл и используются для представления нуля, бесконечности, \\(\\mathrm{NaN}\\) и т.д.

        Таблица 3-2   Значение поля экспоненты

        Поле экспоненты E Поле мантиссы \\(\\mathrm{N} = 0\\) Поле мантиссы \\(\\mathrm{N} \\ne 0\\) Формула вычисления \\(0\\) \\(\\pm 0\\) Денормализованное число \\((-1)^{\\mathrm{S}} \\times 2^{-126} \\times (0.\\mathrm{N})\\) \\(1, 2, \\dots, 254\\) Нормализованное число Нормализованное число \\((-1)^{\\mathrm{S}} \\times 2^{(\\mathrm{E} -127)} \\times (1.\\mathrm{N})\\) \\(255\\) \\(\\pm \\infty\\) \\(\\mathrm{NaN}\\)

        Стоит отметить, что денормализованные числа заметно повышают точность чисел с плавающей точкой. Наименьшее положительное нормализованное число равно \\(2^{-126}\\) , а наименьшее положительное денормализованное число равно \\(2^{-126} \\times 2^{-23}\\) .

        Двойная точность double использует способ представления, аналогичный float , поэтому здесь мы не будем подробно останавливаться на нем.

        ","path":["Глава 3. Структуры данных","3.3   Кодирование чисел *"],"tags":[]},{"location":"chapter_data_structure/summary/","level":1,"title":"3.5   Резюме","text":"","path":["Глава 3. Структуры данных","3.5   Резюме"],"tags":[]},{"location":"chapter_data_structure/summary/#1","level":3,"title":"1.   Ключевые выводы","text":"
        • Структуры данных можно классифицировать с точки зрения логической и физической структуры. Логическая структура описывает логические отношения между элементами данных, а физическая структура описывает способ хранения данных в памяти компьютера.
        • К распространенным логическим структурам относятся линейные, древовидные и сетевые. Обычно структуры данных делятся на линейные (массивы, связные списки, стеки, очереди) и нелинейные (деревья, графы, кучи). Реализация хеш-таблицы может включать как линейные, так и нелинейные структуры данных.
        • При выполнении программы данные хранятся в памяти компьютера. Каждый участок памяти имеет соответствующий адрес, с помощью которого программа получает доступ к данным.
        • Физическая структура делится на хранение в непрерывном пространстве (массивы) и хранение в разрозненном пространстве (связные списки). Все структуры данных реализуются на основе массивов, связных списков или их комбинации.
        • Базовые типы данных в компьютере включают целые byte , short , int , long , числа с плавающей точкой float , double , символы char и логический тип bool . Их диапазон значений зависит от объема занимаемого пространства и способа представления.
        • Прямой код, обратный код и дополнительный код - это три способа кодирования чисел в компьютере, между которыми можно выполнять взаимные преобразования. В прямом коде старший бит целого числа является знаковым, а остальные биты представляют значение числа.
        • Целые числа в компьютере хранятся в виде дополнительного кода. В таком представлении компьютер может одинаково обрабатывать сложение положительных и отрицательных чисел без специальной аппаратной схемы для вычитания, и при этом исчезает неоднозначность положительного и отрицательного нуля.
        • Кодирование числа с плавающей точкой состоит из 1 бита знака, 8 битов экспоненты и 23 битов мантиссы. Благодаря наличию экспоненты диапазон значений у чисел с плавающей точкой намного больше, чем у целых, но это достигается ценой потери точности.
        • ASCII - это самый ранний набор английских символов длиной 1 байт, включающий в общей сложности 127 символов. Набор GBK - распространенный китайский набор символов, включающий более двадцати тысяч иероглифов. Unicode стремится предоставить единый полный стандарт набора символов, включающий символы всех языков мира, чтобы решить проблемы искаженного текста, вызванные несовместимыми способами кодирования.
        • UTF-8 - самый популярный способ кодирования Unicode, обладающий очень хорошей универсальностью. Это кодировка переменной длины, хорошо расширяемая и эффективно использующая память. UTF-16 и UTF-32 относятся к кодировкам фиксированной длины. При кодировании китайского текста UTF-16 занимает меньше места, чем UTF-8. Такие языки программирования, как Java и C#, по умолчанию используют UTF-16.
        ","path":["Глава 3. Структуры данных","3.5   Резюме"],"tags":[]},{"location":"chapter_data_structure/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

        Q: Почему хеш-таблица одновременно включает линейные и нелинейные структуры данных?

        В основе хеш-таблицы лежит массив, а для разрешения коллизий мы можем использовать «цепочки адресации» (об этом будет рассказано в последующем разделе «Хеш-коллизии»): каждый бакет массива указывает на связный список, а если длина списка превышает некоторый порог, он может быть преобразован в дерево (обычно в красно-черное дерево).

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

        Q: Длина типа char равна 1 байту?

        Длина типа char определяется используемым в языке программирования способом кодирования. Например, Java, JavaScript, TypeScript и C# используют кодировку UTF-16 (для хранения кодовых точек Unicode), поэтому длина char у них равна 2 байтам.

        Q: Не является ли двусмысленным утверждение, что структуры данных, реализованные на основе массива, также называются «статическими структурами данных»? Ведь стек тоже поддерживает операции push и pop, а они явно «динамические».

        Стек действительно может поддерживать динамические операции над данными, но сама структура данных при этом остается «статической» (ее длина неизменна). Хотя структуры на основе массива могут динамически добавлять и удалять элементы, их емкость фиксирована. Если количество данных превышает заранее выделенный размер, приходится создавать новый, более крупный массив и копировать в него содержимое старого.

        Q: При построении стека (очереди) его размер не задается явно, почему же его относят к «статическим структурам данных»?

        В языках высокого уровня нам не нужно вручную задавать начальную емкость стека (очереди): это автоматически делает сама реализация класса. Например, начальная емкость ArrayList в Java обычно равна 10. Кроме того, автоматом реализуется и расширение емкости. Подробнее это рассматривается в последующем разделе о «списках».

        Q: Если метод преобразования из прямого кода в дополнительный - это «сначала инвертировать, затем прибавить 1», то обратное преобразование из дополнительного кода в прямой, по идее, должно быть обратной операцией «сначала вычесть 1, затем инвертировать». Почему же дополнительный код также можно перевести в прямой тем же способом «сначала инвертировать, затем прибавить 1»?

        Это связано с тем, что взаимное преобразование прямого и дополнительного кодов по сути является вычислением «дополнения». Сначала дадим определение дополнения: если \\(a + b = c\\) , то говорят, что \\(a\\) является дополнением числа \\(b\\) до \\(c\\). Аналогично, \\(b\\) является дополнением числа \\(a\\) до \\(c\\) .

        Для двоичного числа длины \\(n = 4\\) со значением \\(0010\\) , если рассматривать его как прямой код (не учитывая знаковый бит), то его дополнительный код получается правилом «сначала инвертировать, затем прибавить 1»:

        \\[ 0010 \\rightarrow 1101 \\rightarrow 1110 \\]

        Мы видим, что сумма прямого и дополнительного кодов равна \\(0010 + 1110 = 10000\\) , то есть дополнительный код \\(1110\\) является «дополнением» прямого кода \\(0010\\) до \\(10000\\) . **Это означает, что описанная выше операция «сначала инвертировать, затем прибавить 1» на самом деле вычисляет дополнение до \\(10000\\) **.

        Тогда чему равно «дополнение» дополнительного кода \\(1110\\) до \\(10000\\) ? Мы снова можем получить его правилом «сначала инвертировать, затем прибавить 1»:

        \\[ 1110 \\rightarrow 0001 \\rightarrow 0010 \\]

        Иначе говоря, прямой и дополнительный коды являются взаимными «дополнениями» друг друга до \\(10000\\) , поэтому и «прямой код -> дополнительный код», и «дополнительный код -> прямой код» можно реализовать одной и той же операцией (сначала инвертировать, затем прибавить 1).

        Разумеется, можно получить прямой код из дополнительного кода \\(1110\\) и обратной операцией, то есть «сначала вычесть 1, затем инвертировать»:

        \\[ 1110 \\rightarrow 1101 \\rightarrow 0010 \\]

        В итоге и «сначала инвертировать, затем прибавить 1», и «сначала вычесть 1, затем инвертировать» - это два эквивалентных способа вычисления дополнения до \\(10000\\) .

        По сути операция «инвертировать» сама по себе вычисляет дополнение до \\(1111\\) (потому что всегда выполняется прямой код + обратный код = 1111 ). А дополнительный код, получающийся после добавления 1 к обратному коду, и есть дополнение до \\(10000\\) .

        Приведенный выше пример использовал \\(n = 4\\) , но его можно обобщить на двоичные числа любой длины.

        ","path":["Глава 3. Структуры данных","3.5   Резюме"],"tags":[]},{"location":"chapter_divide_and_conquer/","level":1,"title":"Глава 12.   Разделяй и властвуй","text":"

        Abstract

        Сложная задача раскладывается слой за слоем, и каждое новое разбиение делает ее проще.

        Принцип «разделяй и властвуй» показывает важный факт: если начать с простого, многое перестает быть сложным.

        ","path":["Глава 12. Разделяй и властвуй","Глава 12.   Разделяй и властвуй"],"tags":[]},{"location":"chapter_divide_and_conquer/#_1","level":2,"title":"Содержание главы","text":"
        • 12.1   Стратегия разделяй и властвуй
        • 12.2   Поисковая стратегия разделяй и властвуй
        • 12.3   Задача построения двоичного дерева
        • 12.4   Задача о Ханойской башне
        • 12.5   Резюме
        ","path":["Глава 12. Разделяй и властвуй","Глава 12.   Разделяй и властвуй"],"tags":[]},{"location":"chapter_divide_and_conquer/binary_search_recur/","level":1,"title":"12.2   Поисковая стратегия разделяй и властвуй","text":"

        Мы уже знаем, что алгоритмы поиска делятся на две большие категории.

        • Полный перебор: реализуется через обход структуры данных, временная сложность равна \\(O(n)\\) .
        • Адаптивный поиск: использует особую организацию данных или априорную информацию, временная сложность может достигать \\(O(\\log n)\\) и даже \\(O(1)\\) .

        На практике алгоритмы поиска с временной сложностью \\(O(\\log n)\\) обычно реализуются на основе стратегии «разделяй и властвуй», например двоичный поиск и деревья.

        • На каждом шаге двоичный поиск раскладывает задачу (поиск целевого элемента в массиве) на более мелкую задачу (поиск целевого элемента в одной половине массива), и этот процесс продолжается, пока массив не станет пустым или пока не будет найден целевой элемент.
        • Деревья являются типичными представителями идей «разделяй и властвуй». В таких структурах данных, как двоичное дерево поиска, AVL-дерево и куча, временная сложность различных операций равна \\(O(\\log n)\\) .

        Стратегия «разделяй и властвуй» для двоичного поиска выглядит следующим образом.

        • Задача раскладывается на части: двоичный поиск рекурсивно разбивает исходную задачу (поиск в массиве) на подзадачу (поиск в одной половине массива), и это достигается сравнением среднего элемента с целевым значением.
        • Подзадачи независимы: в двоичном поиске на каждом шаге обрабатывается только одна подзадача, и она не зависит от других подзадач.
        • Решения подзадач не нужно объединять: двоичный поиск нацелен на поиск конкретного элемента, поэтому объединять решения подзадач не требуется. Как только подзадача решена, одновременно считается решенной и исходная задача.

        Иными словами, стратегия «разделяй и властвуй» повышает эффективность поиска потому, что при полном переборе за один шаг удается исключить только один вариант, тогда как при поиске на основе «разделяй и властвуй» за один шаг можно исключить половину вариантов.

        ","path":["Глава 12. Разделяй и властвуй","12.2   Поисковая стратегия разделяй и властвуй"],"tags":[]},{"location":"chapter_divide_and_conquer/binary_search_recur/#1","level":3,"title":"1.   Реализация двоичного поиска на основе «разделяй и властвуй»","text":"

        В предыдущих главах двоичный поиск реализовывался через итерацию. Теперь реализуем его с помощью стратегии «разделяй и властвуй», то есть через рекурсию.

        Question

        Дан отсортированный массив nums длины \\(n\\) , в котором все элементы уникальны. Найдите элемент target .

        С точки зрения стратегии «разделяй и властвуй» обозначим подзадачу, соответствующую интервалу поиска \\([i, j]\\) , через \\(f(i, j)\\) .

        Начиная с исходной задачи \\(f(0, n-1)\\) , выполняем двоичный поиск по следующим шагам.

        1. Вычислить середину \\(m\\) интервала поиска \\([i, j]\\) и с ее помощью исключить половину интервала.
        2. Рекурсивно решить подзадачу вдвое меньшего размера. Это может быть либо \\(f(i, m-1)\\) , либо \\(f(m+1, j)\\) .
        3. Повторять шаг 1. и шаг 2. , пока не будет найден target или пока интервал не станет пустым.

        На рисунке 12-4 показан процесс применения стратегии «разделяй и властвуй» для поиска элемента \\(6\\) в массиве.

        Рисунок 12-4   Процесс двоичного поиска в стиле разделяй и властвуй

        В реализации кода мы объявляем рекурсивную функцию dfs() для решения задачи \\(f(i, j)\\) :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_recur.py
        def dfs(nums: list[int], target: int, i: int, j: int) -> int:\n    \"\"\"Бинарный поиск: задача f(i, j)\"\"\"\n    # Если интервал пуст, целевой элемент отсутствует, вернуть -1\n    if i > j:\n        return -1\n    # Вычислить индекс середины m\n    m = (i + j) // 2\n    if nums[m] < target:\n        # Рекурсивная подзадача f(m+1, j)\n        return dfs(nums, target, m + 1, j)\n    elif nums[m] > target:\n        # Рекурсивная подзадача f(i, m-1)\n        return dfs(nums, target, i, m - 1)\n    else:\n        # Целевой элемент найден, вернуть его индекс\n        return m\n\ndef binary_search(nums: list[int], target: int) -> int:\n    \"\"\"Бинарный поиск\"\"\"\n    n = len(nums)\n    # Решить задачу f(0, n-1)\n    return dfs(nums, target, 0, n - 1)\n
        binary_search_recur.cpp
        /* Бинарный поиск: задача f(i, j) */\nint dfs(vector<int> &nums, int target, int i, int j) {\n    // Если интервал пуст, целевой элемент отсутствует, вернуть -1\n    if (i > j) {\n        return -1;\n    }\n    // Вычислить индекс середины m\n    int m = (i + j) / 2;\n    if (nums[m] < target) {\n        // Рекурсивная подзадача f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // Рекурсивная подзадача f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // Целевой элемент найден, вернуть его индекс\n        return m;\n    }\n}\n\n/* Бинарный поиск */\nint binarySearch(vector<int> &nums, int target) {\n    int n = nums.size();\n    // Решить задачу f(0, n-1)\n    return dfs(nums, target, 0, n - 1);\n}\n
        binary_search_recur.java
        /* Бинарный поиск: задача f(i, j) */\nint dfs(int[] nums, int target, int i, int j) {\n    // Если интервал пуст, целевой элемент отсутствует, вернуть -1\n    if (i > j) {\n        return -1;\n    }\n    // Вычислить индекс середины m\n    int m = (i + j) / 2;\n    if (nums[m] < target) {\n        // Рекурсивная подзадача f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // Рекурсивная подзадача f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // Целевой элемент найден, вернуть его индекс\n        return m;\n    }\n}\n\n/* Бинарный поиск */\nint binarySearch(int[] nums, int target) {\n    int n = nums.length;\n    // Решить задачу f(0, n-1)\n    return dfs(nums, target, 0, n - 1);\n}\n
        binary_search_recur.cs
        /* Бинарный поиск: задача f(i, j) */\nint DFS(int[] nums, int target, int i, int j) {\n    // Если интервал пуст, целевой элемент отсутствует, вернуть -1\n    if (i > j) {\n        return -1;\n    }\n    // Вычислить индекс середины m\n    int m = (i + j) / 2;\n    if (nums[m] < target) {\n        // Рекурсивная подзадача f(m+1, j)\n        return DFS(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // Рекурсивная подзадача f(i, m-1)\n        return DFS(nums, target, i, m - 1);\n    } else {\n        // Целевой элемент найден, вернуть его индекс\n        return m;\n    }\n}\n\n/* Бинарный поиск */\nint BinarySearch(int[] nums, int target) {\n    int n = nums.Length;\n    // Решить задачу f(0, n-1)\n    return DFS(nums, target, 0, n - 1);\n}\n
        binary_search_recur.go
        /* Бинарный поиск: задача f(i, j) */\nfunc dfs(nums []int, target, i, j int) int {\n    // Если интервал пуст, это означает отсутствие целевого элемента, вернуть -1\n    if i > j {\n        return -1\n    }\n    // Вычислить средний индекс\n    m := i + ((j - i) >> 1)\n    // Сравнить середину и целевой элемент\n    if nums[m] < target {\n        // Если меньше, рекурсивно обрабатывать правую половину массива\n        // Рекурсивная подзадача f(m+1, j)\n        return dfs(nums, target, m+1, j)\n    } else if nums[m] > target {\n        // Если больше, рекурсивно обработать левую половину массива\n        // Рекурсивная подзадача f(i, m-1)\n        return dfs(nums, target, i, m-1)\n    } else {\n        // Целевой элемент найден, вернуть его индекс\n        return m\n    }\n}\n\n/* Бинарный поиск */\nfunc binarySearch(nums []int, target int) int {\n    n := len(nums)\n    return dfs(nums, target, 0, n-1)\n}\n
        binary_search_recur.swift
        /* Бинарный поиск: задача f(i, j) */\nfunc dfs(nums: [Int], target: Int, i: Int, j: Int) -> Int {\n    // Если интервал пуст, целевой элемент отсутствует, вернуть -1\n    if i > j {\n        return -1\n    }\n    // Вычислить индекс середины m\n    let m = (i + j) / 2\n    if nums[m] < target {\n        // Рекурсивная подзадача f(m+1, j)\n        return dfs(nums: nums, target: target, i: m + 1, j: j)\n    } else if nums[m] > target {\n        // Рекурсивная подзадача f(i, m-1)\n        return dfs(nums: nums, target: target, i: i, j: m - 1)\n    } else {\n        // Целевой элемент найден, вернуть его индекс\n        return m\n    }\n}\n\n/* Бинарный поиск */\nfunc binarySearch(nums: [Int], target: Int) -> Int {\n    // Решить задачу f(0, n-1)\n    dfs(nums: nums, target: target, i: nums.startIndex, j: nums.endIndex - 1)\n}\n
        binary_search_recur.js
        /* Бинарный поиск: задача f(i, j) */\nfunction dfs(nums, target, i, j) {\n    // Если интервал пуст, целевой элемент отсутствует, вернуть -1\n    if (i > j) {\n        return -1;\n    }\n    // Вычислить индекс середины m\n    const m = i + ((j - i) >> 1);\n    if (nums[m] < target) {\n        // Рекурсивная подзадача f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // Рекурсивная подзадача f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // Целевой элемент найден, вернуть его индекс\n        return m;\n    }\n}\n\n/* Бинарный поиск */\nfunction binarySearch(nums, target) {\n    const n = nums.length;\n    // Решить задачу f(0, n-1)\n    return dfs(nums, target, 0, n - 1);\n}\n
        binary_search_recur.ts
        /* Бинарный поиск: задача f(i, j) */\nfunction dfs(nums: number[], target: number, i: number, j: number): number {\n    // Если интервал пуст, целевой элемент отсутствует, вернуть -1\n    if (i > j) {\n        return -1;\n    }\n    // Вычислить индекс середины m\n    const m = i + ((j - i) >> 1);\n    if (nums[m] < target) {\n        // Рекурсивная подзадача f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // Рекурсивная подзадача f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // Целевой элемент найден, вернуть его индекс\n        return m;\n    }\n}\n\n/* Бинарный поиск */\nfunction binarySearch(nums: number[], target: number): number {\n    const n = nums.length;\n    // Решить задачу f(0, n-1)\n    return dfs(nums, target, 0, n - 1);\n}\n
        binary_search_recur.dart
        /* Бинарный поиск: задача f(i, j) */\nint dfs(List<int> nums, int target, int i, int j) {\n  // Если интервал пуст, целевой элемент отсутствует, вернуть -1\n  if (i > j) {\n    return -1;\n  }\n  // Вычислить индекс середины m\n  int m = (i + j) ~/ 2;\n  if (nums[m] < target) {\n    // Рекурсивная подзадача f(m+1, j)\n    return dfs(nums, target, m + 1, j);\n  } else if (nums[m] > target) {\n    // Рекурсивная подзадача f(i, m-1)\n    return dfs(nums, target, i, m - 1);\n  } else {\n    // Целевой элемент найден, вернуть его индекс\n    return m;\n  }\n}\n\n/* Бинарный поиск */\nint binarySearch(List<int> nums, int target) {\n  int n = nums.length;\n  // Решить задачу f(0, n-1)\n  return dfs(nums, target, 0, n - 1);\n}\n
        binary_search_recur.rs
        /* Бинарный поиск: задача f(i, j) */\nfn dfs(nums: &[i32], target: i32, i: i32, j: i32) -> i32 {\n    // Если интервал пуст, целевой элемент отсутствует, вернуть -1\n    if i > j {\n        return -1;\n    }\n    let m: i32 = i + (j - i) / 2;\n    if nums[m as usize] < target {\n        // Рекурсивная подзадача f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if nums[m as usize] > target {\n        // Рекурсивная подзадача f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // Целевой элемент найден, вернуть его индекс\n        return m;\n    }\n}\n\n/* Бинарный поиск */\nfn binary_search(nums: &[i32], target: i32) -> i32 {\n    let n = nums.len() as i32;\n    // Решить задачу f(0, n-1)\n    dfs(nums, target, 0, n - 1)\n}\n
        binary_search_recur.c
        /* Бинарный поиск: задача f(i, j) */\nint dfs(int nums[], int target, int i, int j) {\n    // Если интервал пуст, целевой элемент отсутствует, вернуть -1\n    if (i > j) {\n        return -1;\n    }\n    // Вычислить индекс середины m\n    int m = (i + j) / 2;\n    if (nums[m] < target) {\n        // Рекурсивная подзадача f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // Рекурсивная подзадача f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // Целевой элемент найден, вернуть его индекс\n        return m;\n    }\n}\n\n/* Бинарный поиск */\nint binarySearch(int nums[], int target, int numsSize) {\n    int n = numsSize;\n    // Решить задачу f(0, n-1)\n    return dfs(nums, target, 0, n - 1);\n}\n
        binary_search_recur.kt
        /* Бинарный поиск: задача f(i, j) */\nfun dfs(\n    nums: IntArray,\n    target: Int,\n    i: Int,\n    j: Int\n): Int {\n    // Если интервал пуст, целевой элемент отсутствует, вернуть -1\n    if (i > j) {\n        return -1\n    }\n    // Вычислить индекс середины m\n    val m = (i + j) / 2\n    return if (nums[m] < target) {\n        // Рекурсивная подзадача f(m+1, j)\n        dfs(nums, target, m + 1, j)\n    } else if (nums[m] > target) {\n        // Рекурсивная подзадача f(i, m-1)\n        dfs(nums, target, i, m - 1)\n    } else {\n        // Целевой элемент найден, вернуть его индекс\n        m\n    }\n}\n\n/* Бинарный поиск */\nfun binarySearch(nums: IntArray, target: Int): Int {\n    val n = nums.size\n    // Решить задачу f(0, n-1)\n    return dfs(nums, target, 0, n - 1)\n}\n
        binary_search_recur.rb
        ### Бинарный поиск: задача f(i, j) ###\ndef dfs(nums, target, i, j)\n  # Если интервал пуст, целевой элемент отсутствует, вернуть -1\n  return -1 if i > j\n\n  # Вычислить индекс середины m\n  m = (i + j) / 2\n\n  if nums[m] < target\n    # Рекурсивная подзадача f(m+1, j)\n    return dfs(nums, target, m + 1, j)\n  elsif nums[m] > target\n    # Рекурсивная подзадача f(i, m-1)\n    return dfs(nums, target, i, m - 1)\n  else\n    # Целевой элемент найден, вернуть его индекс\n    return m\n  end\nend\n\n### Бинарный поиск ###\ndef binary_search(nums, target)\n  n = nums.length\n  # Решить задачу f(0, n-1)\n  dfs(nums, target, 0, n - 1)\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 12. Разделяй и властвуй","12.2   Поисковая стратегия разделяй и властвуй"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/","level":1,"title":"12.3   Задача построения двоичного дерева","text":"

        Question

        Даны прямой обход preorder и симметричный обход inorder некоторого двоичного дерева. Постройте по ним двоичное дерево и верните его корневой узел. Предполагается, что в дереве нет узлов с одинаковыми значениями (как показано на рисунке 12-5).

        Рисунок 12-5   Пример данных для построения двоичного дерева

        ","path":["Глава 12. Разделяй и властвуй","12.3   Задача построения двоичного дерева"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#1","level":3,"title":"1.   Проверка, является ли это задачей «разделяй и властвуй»","text":"

        Исходная задача - построить двоичное дерево по preorder и inorder - является типичной задачей для стратегии «разделяй и властвуй».

        • Задача раскладывается на части: если смотреть с точки зрения стратегии «разделяй и властвуй», исходную задачу можно разбить на две подзадачи: построение левого поддерева и построение правого поддерева, плюс одно действие: инициализация корневого узла. Для каждого поддерева (подзадачи) можно использовать тот же способ разбиения, пока не будет достигнута наименьшая подзадача (пустое поддерево).
        • Подзадачи независимы: левое и правое поддеревья независимы друг от друга и не пересекаются. При построении левого поддерева нам нужно смотреть только на ту часть прямого и симметричного обходов, которая соответствует левому поддереву. Для правого поддерева рассуждение аналогично.
        • Решения подзадач можно объединить: когда левое и правое поддеревья (решения подзадач) уже построены, их можно присоединить к корневому узлу и тем самым получить решение исходной задачи.
        ","path":["Глава 12. Разделяй и властвуй","12.3   Задача построения двоичного дерева"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#2","level":3,"title":"2.   Как разделить поддеревья","text":"

        Из анализа выше видно, что эта задача действительно решается через «разделяй и властвуй», но как именно, имея прямой обход preorder и симметричный обход inorder, отделить левое и правое поддеревья?

        По определению и preorder , и inorder можно разбить на три части.

        • Прямой обход: [ корневой узел | левое поддерево | правое поддерево ] , например для дерева на рисунке 12-5 это [ 3 | 9 | 2 1 7 ] .
        • Симметричный обход: [ левое поддерево | корневой узел | правое поддерево ] , например для дерева на рисунке 12-5 это [ 9 | 3 | 1 2 7 ] .

        На примере данных на рисунке 12-5 разбиение можно выполнить по шагам, показанным на рисунке 12-6.

        1. Первый элемент прямого обхода, равный 3, является значением корневого узла.
        2. Найти индекс корневого узла 3 в inorder. Используя этот индекс, можно разбить inorder на [ 9 | 3 | 1 2 7 ] .
        3. По результату разбиения inorder нетрудно определить, что число узлов в левом и правом поддеревьях равно 1 и 3 соответственно, а значит, preorder можно разбить как [ 3 | 9 | 2 1 7 ] .

        Рисунок 12-6   Разбиение поддеревьев в прямом и симметричном обходах

        ","path":["Глава 12. Разделяй и властвуй","12.3   Задача построения двоичного дерева"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#3","level":3,"title":"3.   Описание интервалов поддеревьев через переменные","text":"

        Согласно описанному выше способу разбиения, мы уже получили интервалы индексов корневого узла, левого и правого поддеревьев в preorder и inorder. Чтобы описывать эти интервалы, нам понадобится несколько указателей-переменных.

        • Обозначим индекс корневого узла текущего дерева в preorder через \\(i\\) .
        • Обозначим индекс корневого узла текущего дерева в inorder через \\(m\\) .
        • Обозначим интервал индексов текущего дерева в inorder через \\([l, r]\\) .

        Как показано в таблице 12-1, этих переменных достаточно для описания индекса корневого узла в preorder и интервалов поддеревьев в inorder .

        Таблица 12-1   Индексы корневого узла и поддеревьев в прямом и симметричном обходах

        Индекс корневого узла в preorder Интервал индексов поддерева в inorder Текущее дерево \\(i\\) \\([l, r]\\) Левое поддерево \\(i + 1\\) \\([l, m-1]\\) Правое поддерево \\(i + 1 + (m - l)\\) \\([m+1, r]\\)

        Стоит отметить, что \\((m-l)\\) в индексе корневого узла правого поддерева означает число узлов в левом поддереве. Лучше всего понять это выражение, сопоставив его с тем, что показано на рисунке 12-7.

        Рисунок 12-7   Представление индексных интервалов корня и поддеревьев

        ","path":["Глава 12. Разделяй и властвуй","12.3   Задача построения двоичного дерева"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#4","level":3,"title":"4.   Реализация кода","text":"

        Чтобы ускорить поиск \\(m\\) , мы используем хеш-таблицу hmap для хранения отображения значений массива inorder в индексы:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby build_tree.py
        def dfs(\n    preorder: list[int],\n    inorder_map: dict[int, int],\n    i: int,\n    l: int,\n    r: int,\n) -> TreeNode | None:\n    \"\"\"Построить двоичное дерево: разделяй и властвуй\"\"\"\n    # Завершить при пустом диапазоне поддерева\n    if r - l < 0:\n        return None\n    # Инициализировать корневой узел\n    root = TreeNode(preorder[i])\n    # Найти m, чтобы разделить левое и правое поддеревья\n    m = inorder_map[preorder[i]]\n    # Подзадача: построить левое поддерево\n    root.left = dfs(preorder, inorder_map, i + 1, l, m - 1)\n    # Подзадача: построить правое поддерево\n    root.right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r)\n    # Вернуть корневой узел\n    return root\n\ndef build_tree(preorder: list[int], inorder: list[int]) -> TreeNode | None:\n    \"\"\"Построить двоичное дерево\"\"\"\n    # Инициализировать хеш-таблицу для хранения соответствия элементов inorder их индексам\n    inorder_map = {val: i for i, val in enumerate(inorder)}\n    root = dfs(preorder, inorder_map, 0, 0, len(inorder) - 1)\n    return root\n
        build_tree.cpp
        /* Построить двоичное дерево: разделяй и властвуй */\nTreeNode *dfs(vector<int> &preorder, unordered_map<int, int> &inorderMap, int i, int l, int r) {\n    // Завершить при пустом диапазоне поддерева\n    if (r - l < 0)\n        return NULL;\n    // Инициализировать корневой узел\n    TreeNode *root = new TreeNode(preorder[i]);\n    // Найти m, чтобы разделить левое и правое поддеревья\n    int m = inorderMap[preorder[i]];\n    // Подзадача: построить левое поддерево\n    root->left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n    // Подзадача: построить правое поддерево\n    root->right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // Вернуть корневой узел\n    return root;\n}\n\n/* Построить двоичное дерево */\nTreeNode *buildTree(vector<int> &preorder, vector<int> &inorder) {\n    // Инициализировать хеш-таблицу для хранения соответствия элементов inorder их индексам\n    unordered_map<int, int> inorderMap;\n    for (int i = 0; i < inorder.size(); i++) {\n        inorderMap[inorder[i]] = i;\n    }\n    TreeNode *root = dfs(preorder, inorderMap, 0, 0, inorder.size() - 1);\n    return root;\n}\n
        build_tree.java
        /* Построить двоичное дерево: разделяй и властвуй */\nTreeNode dfs(int[] preorder, Map<Integer, Integer> inorderMap, int i, int l, int r) {\n    // Завершить при пустом диапазоне поддерева\n    if (r - l < 0)\n        return null;\n    // Инициализировать корневой узел\n    TreeNode root = new TreeNode(preorder[i]);\n    // Найти m, чтобы разделить левое и правое поддеревья\n    int m = inorderMap.get(preorder[i]);\n    // Подзадача: построить левое поддерево\n    root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n    // Подзадача: построить правое поддерево\n    root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // Вернуть корневой узел\n    return root;\n}\n\n/* Построить двоичное дерево */\nTreeNode buildTree(int[] preorder, int[] inorder) {\n    // Инициализировать хеш-таблицу для хранения соответствия элементов inorder их индексам\n    Map<Integer, Integer> inorderMap = new HashMap<>();\n    for (int i = 0; i < inorder.length; i++) {\n        inorderMap.put(inorder[i], i);\n    }\n    TreeNode root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);\n    return root;\n}\n
        build_tree.cs
        /* Построить двоичное дерево: разделяй и властвуй */\nTreeNode? DFS(int[] preorder, Dictionary<int, int> inorderMap, int i, int l, int r) {\n    // Завершить при пустом диапазоне поддерева\n    if (r - l < 0)\n        return null;\n    // Инициализировать корневой узел\n    TreeNode root = new(preorder[i]);\n    // Найти m, чтобы разделить левое и правое поддеревья\n    int m = inorderMap[preorder[i]];\n    // Подзадача: построить левое поддерево\n    root.left = DFS(preorder, inorderMap, i + 1, l, m - 1);\n    // Подзадача: построить правое поддерево\n    root.right = DFS(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // Вернуть корневой узел\n    return root;\n}\n\n/* Построить двоичное дерево */\nTreeNode? BuildTree(int[] preorder, int[] inorder) {\n    // Инициализировать хеш-таблицу для хранения соответствия элементов inorder их индексам\n    Dictionary<int, int> inorderMap = [];\n    for (int i = 0; i < inorder.Length; i++) {\n        inorderMap.TryAdd(inorder[i], i);\n    }\n    TreeNode? root = DFS(preorder, inorderMap, 0, 0, inorder.Length - 1);\n    return root;\n}\n
        build_tree.go
        /* Построить двоичное дерево: разделяй и властвуй */\nfunc dfsBuildTree(preorder []int, inorderMap map[int]int, i, l, r int) *TreeNode {\n    // Завершить при пустом диапазоне поддерева\n    if r-l < 0 {\n        return nil\n    }\n    // Инициализировать корневой узел\n    root := NewTreeNode(preorder[i])\n    // Найти m, чтобы разделить левое и правое поддеревья\n    m := inorderMap[preorder[i]]\n    // Подзадача: построить левое поддерево\n    root.Left = dfsBuildTree(preorder, inorderMap, i+1, l, m-1)\n    // Подзадача: построить правое поддерево\n    root.Right = dfsBuildTree(preorder, inorderMap, i+1+m-l, m+1, r)\n    // Вернуть корневой узел\n    return root\n}\n\n/* Построить двоичное дерево */\nfunc buildTree(preorder, inorder []int) *TreeNode {\n    // Инициализировать хеш-таблицу для хранения соответствия элементов inorder их индексам\n    inorderMap := make(map[int]int, len(inorder))\n    for i := 0; i < len(inorder); i++ {\n        inorderMap[inorder[i]] = i\n    }\n\n    root := dfsBuildTree(preorder, inorderMap, 0, 0, len(inorder)-1)\n    return root\n}\n
        build_tree.swift
        /* Построить двоичное дерево: разделяй и властвуй */\nfunc dfs(preorder: [Int], inorderMap: [Int: Int], i: Int, l: Int, r: Int) -> TreeNode? {\n    // Завершить при пустом диапазоне поддерева\n    if r - l < 0 {\n        return nil\n    }\n    // Инициализировать корневой узел\n    let root = TreeNode(x: preorder[i])\n    // Найти m, чтобы разделить левое и правое поддеревья\n    let m = inorderMap[preorder[i]]!\n    // Подзадача: построить левое поддерево\n    root.left = dfs(preorder: preorder, inorderMap: inorderMap, i: i + 1, l: l, r: m - 1)\n    // Подзадача: построить правое поддерево\n    root.right = dfs(preorder: preorder, inorderMap: inorderMap, i: i + 1 + m - l, l: m + 1, r: r)\n    // Вернуть корневой узел\n    return root\n}\n\n/* Построить двоичное дерево */\nfunc buildTree(preorder: [Int], inorder: [Int]) -> TreeNode? {\n    // Инициализировать хеш-таблицу для хранения соответствия элементов inorder их индексам\n    let inorderMap = inorder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset }\n    return dfs(preorder: preorder, inorderMap: inorderMap, i: inorder.startIndex, l: inorder.startIndex, r: inorder.endIndex - 1)\n}\n
        build_tree.js
        /* Построить двоичное дерево: разделяй и властвуй */\nfunction dfs(preorder, inorderMap, i, l, r) {\n    // Завершить при пустом диапазоне поддерева\n    if (r - l < 0) return null;\n    // Инициализировать корневой узел\n    const root = new TreeNode(preorder[i]);\n    // Найти m, чтобы разделить левое и правое поддеревья\n    const m = inorderMap.get(preorder[i]);\n    // Подзадача: построить левое поддерево\n    root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n    // Подзадача: построить правое поддерево\n    root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // Вернуть корневой узел\n    return root;\n}\n\n/* Построить двоичное дерево */\nfunction buildTree(preorder, inorder) {\n    // Инициализировать хеш-таблицу для хранения соответствия элементов inorder их индексам\n    let inorderMap = new Map();\n    for (let i = 0; i < inorder.length; i++) {\n        inorderMap.set(inorder[i], i);\n    }\n    const root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);\n    return root;\n}\n
        build_tree.ts
        /* Построить двоичное дерево: разделяй и властвуй */\nfunction dfs(\n    preorder: number[],\n    inorderMap: Map<number, number>,\n    i: number,\n    l: number,\n    r: number\n): TreeNode | null {\n    // Завершить при пустом диапазоне поддерева\n    if (r - l < 0) return null;\n    // Инициализировать корневой узел\n    const root: TreeNode = new TreeNode(preorder[i]);\n    // Найти m, чтобы разделить левое и правое поддеревья\n    const m = inorderMap.get(preorder[i]);\n    // Подзадача: построить левое поддерево\n    root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n    // Подзадача: построить правое поддерево\n    root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // Вернуть корневой узел\n    return root;\n}\n\n/* Построить двоичное дерево */\nfunction buildTree(preorder: number[], inorder: number[]): TreeNode | null {\n    // Инициализировать хеш-таблицу для хранения соответствия элементов inorder их индексам\n    let inorderMap = new Map<number, number>();\n    for (let i = 0; i < inorder.length; i++) {\n        inorderMap.set(inorder[i], i);\n    }\n    const root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);\n    return root;\n}\n
        build_tree.dart
        /* Построить двоичное дерево: разделяй и властвуй */\nTreeNode? dfs(\n  List<int> preorder,\n  Map<int, int> inorderMap,\n  int i,\n  int l,\n  int r,\n) {\n  // Завершить при пустом диапазоне поддерева\n  if (r - l < 0) {\n    return null;\n  }\n  // Инициализировать корневой узел\n  TreeNode? root = TreeNode(preorder[i]);\n  // Найти m, чтобы разделить левое и правое поддеревья\n  int m = inorderMap[preorder[i]]!;\n  // Подзадача: построить левое поддерево\n  root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n  // Подзадача: построить правое поддерево\n  root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n  // Вернуть корневой узел\n  return root;\n}\n\n/* Построить двоичное дерево */\nTreeNode? buildTree(List<int> preorder, List<int> inorder) {\n  // Инициализировать хеш-таблицу для хранения соответствия элементов inorder их индексам\n  Map<int, int> inorderMap = {};\n  for (int i = 0; i < inorder.length; i++) {\n    inorderMap[inorder[i]] = i;\n  }\n  TreeNode? root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);\n  return root;\n}\n
        build_tree.rs
        /* Построить двоичное дерево: разделяй и властвуй */\nfn dfs(\n    preorder: &[i32],\n    inorder_map: &HashMap<i32, i32>,\n    i: i32,\n    l: i32,\n    r: i32,\n) -> Option<Rc<RefCell<TreeNode>>> {\n    // Завершить при пустом диапазоне поддерева\n    if r - l < 0 {\n        return None;\n    }\n    // Инициализировать корневой узел\n    let root = TreeNode::new(preorder[i as usize]);\n    // Найти m, чтобы разделить левое и правое поддеревья\n    let m = inorder_map.get(&preorder[i as usize]).unwrap();\n    // Подзадача: построить левое поддерево\n    root.borrow_mut().left = dfs(preorder, inorder_map, i + 1, l, m - 1);\n    // Подзадача: построить правое поддерево\n    root.borrow_mut().right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r);\n    // Вернуть корневой узел\n    Some(root)\n}\n\n/* Построить двоичное дерево */\nfn build_tree(preorder: &[i32], inorder: &[i32]) -> Option<Rc<RefCell<TreeNode>>> {\n    // Инициализировать хеш-таблицу для хранения соответствия элементов inorder их индексам\n    let mut inorder_map: HashMap<i32, i32> = HashMap::new();\n    for i in 0..inorder.len() {\n        inorder_map.insert(inorder[i], i as i32);\n    }\n    let root = dfs(preorder, &inorder_map, 0, 0, inorder.len() as i32 - 1);\n    root\n}\n
        build_tree.c
        /* Построить двоичное дерево: разделяй и властвуй */\nTreeNode *dfs(int *preorder, int *inorderMap, int i, int l, int r, int size) {\n    // Завершить при пустом диапазоне поддерева\n    if (r - l < 0)\n        return NULL;\n    // Инициализировать корневой узел\n    TreeNode *root = (TreeNode *)malloc(sizeof(TreeNode));\n    root->val = preorder[i];\n    root->left = NULL;\n    root->right = NULL;\n    // Найти m, чтобы разделить левое и правое поддеревья\n    int m = inorderMap[preorder[i]];\n    // Подзадача: построить левое поддерево\n    root->left = dfs(preorder, inorderMap, i + 1, l, m - 1, size);\n    // Подзадача: построить правое поддерево\n    root->right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r, size);\n    // Вернуть корневой узел\n    return root;\n}\n\n/* Построить двоичное дерево */\nTreeNode *buildTree(int *preorder, int preorderSize, int *inorder, int inorderSize) {\n    // Инициализировать хеш-таблицу для хранения соответствия элементов inorder их индексам\n    int *inorderMap = (int *)malloc(sizeof(int) * MAX_SIZE);\n    for (int i = 0; i < inorderSize; i++) {\n        inorderMap[inorder[i]] = i;\n    }\n    TreeNode *root = dfs(preorder, inorderMap, 0, 0, inorderSize - 1, inorderSize);\n    free(inorderMap);\n    return root;\n}\n
        build_tree.kt
        /* Построить двоичное дерево: разделяй и властвуй */\nfun dfs(\n    preorder: IntArray,\n    inorderMap: Map<Int?, Int?>,\n    i: Int,\n    l: Int,\n    r: Int\n): TreeNode? {\n    // Завершить при пустом диапазоне поддерева\n    if (r - l < 0) return null\n    // Инициализировать корневой узел\n    val root = TreeNode(preorder[i])\n    // Найти m, чтобы разделить левое и правое поддеревья\n    val m = inorderMap[preorder[i]]!!\n    // Подзадача: построить левое поддерево\n    root.left = dfs(preorder, inorderMap, i + 1, l, m - 1)\n    // Подзадача: построить правое поддерево\n    root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r)\n    // Вернуть корневой узел\n    return root\n}\n\n/* Построить двоичное дерево */\nfun buildTree(preorder: IntArray, inorder: IntArray): TreeNode? {\n    // Инициализировать хеш-таблицу для хранения соответствия элементов inorder их индексам\n    val inorderMap = HashMap<Int?, Int?>()\n    for (i in inorder.indices) {\n        inorderMap[inorder[i]] = i\n    }\n    val root = dfs(preorder, inorderMap, 0, 0, inorder.size - 1)\n    return root\n}\n
        build_tree.rb
        ### Построить двоичное дерево: разделяй и властвуй ###\ndef dfs(preorder, inorder_map, i, l, r)\n  # Завершить при пустом диапазоне поддерева\n  return if r - l < 0\n\n  # Инициализировать корневой узел\n  root = TreeNode.new(preorder[i])\n  # Найти m, чтобы разделить левое и правое поддеревья\n  m = inorder_map[preorder[i]]\n  # Подзадача: построить левое поддерево\n  root.left = dfs(preorder, inorder_map, i + 1, l, m - 1)\n  # Подзадача: построить правое поддерево\n  root.right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r)\n\n  # Вернуть корневой узел\n  root\nend\n\n### Построить двоичное дерево ###\ndef build_tree(preorder, inorder)\n  # Инициализировать хеш-таблицу для хранения соответствия элементов inorder их индексам\n  inorder_map = {}\n  inorder.each_with_index { |val, i| inorder_map[val] = i }\n  dfs(preorder, inorder_map, 0, 0, inorder.length - 1)\nend\n
        Визуализация кода

        Во весь экран >

        На рисунке 12-8 показан рекурсивный процесс построения двоичного дерева: каждый узел создается в фазе «спуска», а каждое ребро (ссылка) формируется в фазе «подъема».

        <1><2><3><4><5><6><7><8><9>

        Рисунок 12-8   Рекурсивный процесс построения двоичного дерева

        Результаты разбиения preorder и inorder внутри каждого рекурсивного вызова показаны на рисунке 12-9.

        Рисунок 12-9   Результаты разбиения в каждом рекурсивном вызове

        Пусть число узлов дерева равно \\(n\\). Инициализация каждого узла (то есть выполнение одного рекурсивного вызова dfs() ) занимает \\(O(1)\\) времени. Следовательно, общая временная сложность равна \\(O(n)\\) .

        Хеш-таблица хранит отображение значений inorder в индексы, поэтому ее пространственная сложность равна \\(O(n)\\) . В худшем случае, когда двоичное дерево вырождается в связный список, глубина рекурсии достигает \\(n\\) и требует \\(O(n)\\) памяти стека. Следовательно, общая пространственная сложность также равна \\(O(n)\\) .

        ","path":["Глава 12. Разделяй и властвуй","12.3   Задача построения двоичного дерева"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/","level":1,"title":"12.1   Стратегия разделяй и властвуй","text":"

        Разделяй и властвуй (divide and conquer) - это очень важная и широко используемая стратегия построения алгоритмов. Обычно она реализуется через рекурсию и включает два этапа: «разделение» и «объединение».

        1. Разделение (этап декомпозиции): рекурсивно разбить исходную задачу на две или более подзадачи, пока не будет достигнута наименьшая подзадача.
        2. Объединение (этап синтеза): начиная с уже известных решений наименьших подзадач, снизу вверх объединять решения подзадач и тем самым получать решение исходной задачи.

        Как показано на рисунке 12-1, «сортировка слиянием» является одним из типичных примеров применения стратегии «разделяй и властвуй».

        1. Разделение: рекурсивно разделить исходный массив (исходную задачу) на два подмассива (подзадачи), пока в подмассиве не останется только один элемент (наименьшая подзадача).
        2. Объединение: снизу вверх объединять упорядоченные подмассивы (решения подзадач), чтобы получить упорядоченный исходный массив (решение исходной задачи).

        Рисунок 12-1   Стратегия разделяй и властвуй в сортировке слиянием

        ","path":["Глава 12. Разделяй и властвуй","12.1   Стратегия разделяй и властвуй"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1211","level":2,"title":"12.1.1   Как определить задачу «разделяй и властвуй»","text":"

        Чтобы понять, подходит ли задача для решения методом «разделяй и властвуй», обычно можно ориентироваться на следующие критерии.

        1. Задача раскладывается на части: исходную задачу можно разбить на более мелкие и похожие подзадачи, причем такое разбиение можно применять рекурсивно.
        2. Подзадачи независимы: подзадачи не пересекаются, не зависят друг от друга и могут решаться независимо.
        3. Решения подзадач можно объединить: решение исходной задачи получается объединением решений подзадач.

        Очевидно, что сортировка слиянием удовлетворяет всем трем критериям.

        1. Задача раскладывается на части: массив (исходная задача) рекурсивно делится на два подмассива (подзадачи).
        2. Подзадачи независимы: каждый подмассив можно сортировать отдельно (то есть каждую подзадачу можно решать независимо).
        3. Решения подзадач можно объединить: два упорядоченных подмассива (решения подзадач) можно объединить в один упорядоченный массив (решение исходной задачи).
        ","path":["Глава 12. Разделяй и властвуй","12.1   Стратегия разделяй и властвуй"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1212","level":2,"title":"12.1.2   Повышение эффективности с помощью «разделяй и властвуй»","text":"

        Стратегия «разделяй и властвуй» не только позволяет эффективно решать алгоритмические задачи, но и часто повышает эффективность самих алгоритмов. Именно поэтому быстрая сортировка, сортировка слиянием и пирамидальная сортировка обычно работают быстрее, чем сортировка выбором, пузырьком и вставками.

        Тогда возникает естественный вопрос: почему стратегия «разделяй и властвуй» повышает эффективность алгоритма и какова внутренняя логика этого подхода? Иными словами, почему разбиение большой задачи на несколько подзадач, решение этих подзадач и последующее объединение их решений оказывается эффективнее, чем прямое решение исходной задачи? Этот вопрос можно рассмотреть с двух сторон: через число операций и через параллельные вычисления.

        ","path":["Глава 12. Разделяй и властвуй","12.1   Стратегия разделяй и властвуй"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1","level":3,"title":"1.   Оптимизация числа операций","text":"

        Рассмотрим «сортировку пузырьком»: для массива длины \\(n\\) ей требуется \\(O(n^2)\\) времени. Предположим, что мы разделим массив на два подмассива в середине, как показано на рисунке 12-2. Тогда само разбиение потребует \\(O(n)\\) времени, сортировка каждого подмассива займет \\(O((n / 2)^2)\\) времени, а объединение двух подмассивов потребует еще \\(O(n)\\) времени. Общая временная сложность будет равна:

        \\[ O(n + (\\frac{n}{2})^2 \\times 2 + n) = O(\\frac{n^2}{2} + 2n) \\]

        Рисунок 12-2   Сортировка пузырьком до и после разбиения массива

        Теперь рассмотрим следующее неравенство, в котором левая и правая части обозначают общее число операций до разбиения и после него:

        \\[ \\begin{aligned} n^2 & > \\frac{n^2}{2} + 2n \\newline n^2 - \\frac{n^2}{2} - 2n & > 0 \\newline n(n - 4) & > 0 \\end{aligned} \\]

        Это означает, что при \\(n > 4\\) число операций после разбиения становится меньше, а значит, сортировка должна работать быстрее. При этом важно заметить, что временная сложность после разбиения все еще остается квадратичной, то есть \\(O(n^2)\\). Уменьшается лишь константный множитель.

        Если пойти дальше и продолжать делить каждый подмассив пополам, пока в нем не останется только один элемент, то мы фактически получим «сортировку слиянием», чья временная сложность равна \\(O(n \\log n)\\) .

        Можно пойти еще дальше и спросить: что если задать несколько точек разделения и равномерно разбить исходный массив на \\(k\\) подмассивов? Такая ситуация очень похожа на блочную сортировку, которая особенно хорошо подходит для сортировки очень больших объемов данных и теоретически может достигать временной сложности \\(O(n + k)\\) .

        ","path":["Глава 12. Разделяй и властвуй","12.1   Стратегия разделяй и властвуй"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#2","level":3,"title":"2.   Оптимизация параллельных вычислений","text":"

        Мы знаем, что подзадачи, порождаемые стратегией «разделяй и властвуй», являются независимыми, а значит, их обычно можно решать параллельно. Иначе говоря, «разделяй и властвуй» не только может уменьшить временную сложность алгоритма, но и хорошо сочетается с параллельной оптимизацией на уровне системы.

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

        Например, в «блочной сортировке», показанной на рисунке 12-3, большой объем данных равномерно распределяется по блокам. Тогда сортировку каждого блока можно поручить отдельным вычислительным единицам, а после завершения просто объединить результаты.

        Рисунок 12-3   Параллельные вычисления в блочной сортировке

        ","path":["Глава 12. Разделяй и властвуй","12.1   Стратегия разделяй и властвуй"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1213","level":2,"title":"12.1.3   Типичные применения стратегии «разделяй и властвуй»","text":"

        С одной стороны, стратегию «разделяй и властвуй» можно использовать для решения многих классических алгоритмических задач.

        • Поиск ближайшей пары точек: сначала множество точек делится на две части, затем ищется ближайшая пара в каждой части, а затем ближайшая пара, пересекающая границу между двумя частями.
        • Умножение больших чисел: например, алгоритм Карацубы, который раскладывает умножение больших чисел на несколько умножений и сложений меньших чисел.
        • Умножение матриц: например, алгоритм Штрассена, который раскладывает умножение больших матриц на несколько умножений и сложений матриц меньшего размера.
        • Задача о Ханойской башне: задача о Ханойской башне решается рекурсивно и является типичным примером применения стратегии «разделяй и властвуй».
        • Подсчет инверсий: если в последовательности предыдущее число больше следующего, то такая пара образует инверсию. Эту задачу можно решить с помощью идей «разделяй и властвуй», опираясь на сортировку слиянием.

        С другой стороны, стратегия «разделяй и властвуй» очень широко применяется при проектировании алгоритмов и структур данных.

        • Двоичный поиск: двоичный поиск делит отсортированный массив на две части по индексу середины, а затем, в зависимости от результата сравнения целевого значения со средним элементом, исключает одну из половин и повторяет ту же операцию на оставшемся интервале.
        • Сортировка слиянием: она уже была рассмотрена в начале этого раздела, поэтому не будем повторяться.
        • Быстрая сортировка: в ней выбирается опорное значение, после чего массив делится на два подмассива: один содержит элементы меньше опорного, а другой - больше. Затем такая же операция повторяется для обеих частей, пока в подмассиве не останется один элемент.
        • Блочная сортировка: ее основная идея заключается в распределении данных по нескольким блокам, сортировке элементов внутри каждого блока и последующем последовательном извлечении элементов из блоков для построения отсортированного массива.
        • Деревья: например, двоичные деревья поиска, AVL-деревья, красно-черные деревья, B-деревья, B+ деревья и т.д. Их операции поиска, вставки и удаления можно рассматривать как применение стратегии «разделяй и властвуй».
        • Кучи: куча является особым видом полного двоичного дерева, а такие операции, как вставка, удаление и упорядочивание, по сути содержат идеи «разделяй и властвуй».
        • Хеш-таблицы: хотя хеш-таблицы напрямую не используют стратегию «разделяй и властвуй», некоторые способы разрешения коллизий косвенно опираются на эту идею. Например, длинные цепочки в методе цепочек могут преобразовываться в красно-черные деревья для повышения эффективности поиска.

        Нетрудно заметить, что «разделяй и властвуй» - это «тихая» алгоритмическая идея, скрыто присутствующая внутри самых разных алгоритмов и структур данных.

        ","path":["Глава 12. Разделяй и властвуй","12.1   Стратегия разделяй и властвуй"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/","level":1,"title":"12.4   Задача о Ханойской башне","text":"

        В задачах сортировки слиянием и построения двоичного дерева мы делили исходную задачу на две подзадачи, каждая из которых имела размер, равный примерно половине исходной задачи. Однако для задачи о Ханойской башне используется другая стратегия разбиения.

        Question

        Даны три стержня, обозначенные как A , B и C . В начальном состоянии на стержне A находятся \\(n\\) дисков, расположенных сверху вниз в порядке от меньшего к большему. Нужно переместить эти \\(n\\) дисков на стержень C , сохранив их исходный порядок (как показано на рисунке 12-10). Во время перемещения дисков необходимо соблюдать следующие правила.

        1. Диск можно снять только с вершины одного стержня и положить только на вершину другого стержня.
        2. За один раз можно перемещать только один диск.
        3. Меньший диск всегда должен лежать на большем.

        Рисунок 12-10   Пример задачи о Ханойской башне

        Обозначим задачу о Ханойской башне размера \\(i\\) как \\(f(i)\\) . Например, \\(f(3)\\) означает задачу перемещения 3 дисков со стержня A на стержень C .

        ","path":["Глава 12. Разделяй и властвуй","12.4   Задача о Ханойской башне"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/#1","level":3,"title":"1.   Рассмотрим базовые случаи","text":"

        Как показано на рисунке 12-11, для задачи \\(f(1)\\) , то есть когда имеется только один диск, достаточно просто переместить его напрямую со стержня A на стержень C .

        <1><2>

        Рисунок 12-11   Решение задачи размера 1

        Как показано на рисунке 12-12, для задачи \\(f(2)\\) , то есть когда есть два диска, поскольку меньший диск все время должен лежать на большем, приходится использовать B как вспомогательный стержень.

        1. Сначала переместить верхний маленький диск с A на B .
        2. Затем переместить большой диск с A на C .
        3. Наконец, переместить маленький диск с B на C .
        <1><2><3><4>

        Рисунок 12-12   Решение задачи размера 2

        Процесс решения задачи \\(f(2)\\) можно кратко описать так: переместить два диска с A на C с помощью B . Здесь C называется целевым стержнем, а B - буферным стержнем.

        ","path":["Глава 12. Разделяй и властвуй","12.4   Задача о Ханойской башне"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/#2","level":3,"title":"2.   Разбиение на подзадачи","text":"

        Для задачи \\(f(3)\\) , то есть когда имеется три диска, ситуация становится сложнее.

        Поскольку решения \\(f(1)\\) и \\(f(2)\\) уже известны, можно подойти к задаче с точки зрения стратегии «разделяй и властвуй» и рассматривать два верхних диска на A как единое целое, выполняя шаги, показанные на рисунке 12-13. Так три диска успешно перемещаются с A на C .

        1. Сделать B целевым стержнем, а C буферным, и переместить два диска с A на B .
        2. Переместить оставшийся один диск с A напрямую на C .
        3. Сделать C целевым стержнем, а A буферным, и переместить два диска с B на C .
        <1><2><3><4>

        Рисунок 12-13   Решение задачи размера 3

        Иначе говоря, мы разбиваем задачу \\(f(3)\\) на две подзадачи \\(f(2)\\) и одну подзадачу \\(f(1)\\) . Если последовательно решить эти три подзадачи, исходная задача тоже будет решена. Это показывает, что подзадачи независимы и что их решения можно объединить.

        Таким образом, можно сформулировать показанную на рисунке 12-14 стратегию «разделяй и властвуй» для задачи о Ханойской башне: исходная задача \\(f(n)\\) разбивается на две подзадачи \\(f(n-1)\\) и одну подзадачу \\(f(1)\\) , которые затем решаются в следующем порядке.

        1. Переместить \\(n-1\\) дисков с A на B с помощью C .
        2. Переместить оставшийся \\(1\\) диск напрямую с A на C .
        3. Переместить \\(n-1\\) дисков с B на C с помощью A .

        Для двух подзадач \\(f(n-1)\\) можно применять тот же способ рекурсивного разбиения, пока не будет достигнута наименьшая подзадача \\(f(1)\\) . А решение для \\(f(1)\\) уже известно и требует всего одного перемещения.

        Рисунок 12-14   Стратегия разделяй и властвуй для решения задачи о Ханойской башне

        ","path":["Глава 12. Разделяй и властвуй","12.4   Задача о Ханойской башне"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/#3","level":3,"title":"3.   Реализация кода","text":"

        В коде мы объявляем рекурсивную функцию dfs(i, src, buf, tar) , которая перемещает \\(i\\) верхних дисков со стержня src на целевой стержень tar с помощью буферного стержня buf :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hanota.py
        def move(src: list[int], tar: list[int]):\n    \"\"\"Переместить один диск\"\"\"\n    # Снять диск с вершины src\n    pan = src.pop()\n    # Положить диск на вершину tar\n    tar.append(pan)\n\ndef dfs(i: int, src: list[int], buf: list[int], tar: list[int]):\n    \"\"\"Решить задачу Ханойской башни f(i)\"\"\"\n    # Если в src остался только один диск, сразу переместить его в tar\n    if i == 1:\n        move(src, tar)\n        return\n    # Подзадача f(i-1): переместить верхние i-1 дисков из src в buf с помощью tar\n    dfs(i - 1, src, tar, buf)\n    # Подзадача f(1): переместить оставшийся один диск из src в tar\n    move(src, tar)\n    # Подзадача f(i-1): переместить верхние i-1 дисков из buf в tar с помощью src\n    dfs(i - 1, buf, src, tar)\n\ndef solve_hanota(A: list[int], B: list[int], C: list[int]):\n    \"\"\"Решить задачу Ханойской башни\"\"\"\n    n = len(A)\n    # Переместить верхние n дисков из A в C с помощью B\n    dfs(n, A, B, C)\n
        hanota.cpp
        /* Переместить один диск */\nvoid move(vector<int> &src, vector<int> &tar) {\n    // Снять диск с вершины src\n    int pan = src.back();\n    src.pop_back();\n    // Положить диск на вершину tar\n    tar.push_back(pan);\n}\n\n/* Решить задачу Ханойской башни f(i) */\nvoid dfs(int i, vector<int> &src, vector<int> &buf, vector<int> &tar) {\n    // Если в src остался только один диск, сразу переместить его в tar\n    if (i == 1) {\n        move(src, tar);\n        return;\n    }\n    // Подзадача f(i-1): переместить верхние i-1 дисков из src в buf с помощью tar\n    dfs(i - 1, src, tar, buf);\n    // Подзадача f(1): переместить оставшийся один диск из src в tar\n    move(src, tar);\n    // Подзадача f(i-1): переместить верхние i-1 дисков из buf в tar с помощью src\n    dfs(i - 1, buf, src, tar);\n}\n\n/* Решить задачу Ханойской башни */\nvoid solveHanota(vector<int> &A, vector<int> &B, vector<int> &C) {\n    int n = A.size();\n    // Переместить верхние n дисков из A в C с помощью B\n    dfs(n, A, B, C);\n}\n
        hanota.java
        /* Переместить один диск */\nvoid move(List<Integer> src, List<Integer> tar) {\n    // Снять диск с вершины src\n    Integer pan = src.remove(src.size() - 1);\n    // Положить диск на вершину tar\n    tar.add(pan);\n}\n\n/* Решить задачу Ханойской башни f(i) */\nvoid dfs(int i, List<Integer> src, List<Integer> buf, List<Integer> tar) {\n    // Если в src остался только один диск, сразу переместить его в tar\n    if (i == 1) {\n        move(src, tar);\n        return;\n    }\n    // Подзадача f(i-1): переместить верхние i-1 дисков из src в buf с помощью tar\n    dfs(i - 1, src, tar, buf);\n    // Подзадача f(1): переместить оставшийся один диск из src в tar\n    move(src, tar);\n    // Подзадача f(i-1): переместить верхние i-1 дисков из buf в tar с помощью src\n    dfs(i - 1, buf, src, tar);\n}\n\n/* Решить задачу Ханойской башни */\nvoid solveHanota(List<Integer> A, List<Integer> B, List<Integer> C) {\n    int n = A.size();\n    // Переместить верхние n дисков из A в C с помощью B\n    dfs(n, A, B, C);\n}\n
        hanota.cs
        /* Переместить один диск */\nvoid Move(List<int> src, List<int> tar) {\n    // Снять диск с вершины src\n    int pan = src[^1];\n    src.RemoveAt(src.Count - 1);\n    // Положить диск на вершину tar\n    tar.Add(pan);\n}\n\n/* Решить задачу Ханойской башни f(i) */\nvoid DFS(int i, List<int> src, List<int> buf, List<int> tar) {\n    // Если в src остался только один диск, сразу переместить его в tar\n    if (i == 1) {\n        Move(src, tar);\n        return;\n    }\n    // Подзадача f(i-1): переместить верхние i-1 дисков из src в buf с помощью tar\n    DFS(i - 1, src, tar, buf);\n    // Подзадача f(1): переместить оставшийся один диск из src в tar\n    Move(src, tar);\n    // Подзадача f(i-1): переместить верхние i-1 дисков из buf в tar с помощью src\n    DFS(i - 1, buf, src, tar);\n}\n\n/* Решить задачу Ханойской башни */\nvoid SolveHanota(List<int> A, List<int> B, List<int> C) {\n    int n = A.Count;\n    // Переместить верхние n дисков из A в C с помощью B\n    DFS(n, A, B, C);\n}\n
        hanota.go
        /* Переместить один диск */\nfunc move(src, tar *list.List) {\n    // Снять диск с вершины src\n    pan := src.Back()\n    // Положить диск на вершину tar\n    tar.PushBack(pan.Value)\n    // Убрать верхний диск из src\n    src.Remove(pan)\n}\n\n/* Решить задачу Ханойской башни f(i) */\nfunc dfsHanota(i int, src, buf, tar *list.List) {\n    // Если в src остался только один диск, сразу переместить его в tar\n    if i == 1 {\n        move(src, tar)\n        return\n    }\n    // Подзадача f(i-1): переместить верхние i-1 дисков из src в buf с помощью tar\n    dfsHanota(i-1, src, tar, buf)\n    // Подзадача f(1): переместить оставшийся один диск из src в tar\n    move(src, tar)\n    // Подзадача f(i-1): переместить верхние i-1 дисков из buf в tar с помощью src\n    dfsHanota(i-1, buf, src, tar)\n}\n\n/* Решить задачу Ханойской башни */\nfunc solveHanota(A, B, C *list.List) {\n    n := A.Len()\n    // Переместить верхние n дисков из A в C с помощью B\n    dfsHanota(n, A, B, C)\n}\n
        hanota.swift
        /* Переместить один диск */\nfunc move(src: inout [Int], tar: inout [Int]) {\n    // Снять диск с вершины src\n    let pan = src.popLast()!\n    // Положить диск на вершину tar\n    tar.append(pan)\n}\n\n/* Решить задачу Ханойской башни f(i) */\nfunc dfs(i: Int, src: inout [Int], buf: inout [Int], tar: inout [Int]) {\n    // Если в src остался только один диск, сразу переместить его в tar\n    if i == 1 {\n        move(src: &src, tar: &tar)\n        return\n    }\n    // Подзадача f(i-1): переместить верхние i-1 дисков из src в buf с помощью tar\n    dfs(i: i - 1, src: &src, buf: &tar, tar: &buf)\n    // Подзадача f(1): переместить оставшийся один диск из src в tar\n    move(src: &src, tar: &tar)\n    // Подзадача f(i-1): переместить верхние i-1 дисков из buf в tar с помощью src\n    dfs(i: i - 1, src: &buf, buf: &src, tar: &tar)\n}\n\n/* Решить задачу Ханойской башни */\nfunc solveHanota(A: inout [Int], B: inout [Int], C: inout [Int]) {\n    let n = A.count\n    // Хвост списка соответствует вершине столбца\n    // Переместить верхние n дисков из src в C с помощью B\n    dfs(i: n, src: &A, buf: &B, tar: &C)\n}\n
        hanota.js
        /* Переместить один диск */\nfunction move(src, tar) {\n    // Снять диск с вершины src\n    const pan = src.pop();\n    // Положить диск на вершину tar\n    tar.push(pan);\n}\n\n/* Решить задачу Ханойской башни f(i) */\nfunction dfs(i, src, buf, tar) {\n    // Если в src остался только один диск, сразу переместить его в tar\n    if (i === 1) {\n        move(src, tar);\n        return;\n    }\n    // Подзадача f(i-1): переместить верхние i-1 дисков из src в buf с помощью tar\n    dfs(i - 1, src, tar, buf);\n    // Подзадача f(1): переместить оставшийся один диск из src в tar\n    move(src, tar);\n    // Подзадача f(i-1): переместить верхние i-1 дисков из buf в tar с помощью src\n    dfs(i - 1, buf, src, tar);\n}\n\n/* Решить задачу Ханойской башни */\nfunction solveHanota(A, B, C) {\n    const n = A.length;\n    // Переместить верхние n дисков из A в C с помощью B\n    dfs(n, A, B, C);\n}\n
        hanota.ts
        /* Переместить один диск */\nfunction move(src: number[], tar: number[]): void {\n    // Снять диск с вершины src\n    const pan = src.pop();\n    // Положить диск на вершину tar\n    tar.push(pan);\n}\n\n/* Решить задачу Ханойской башни f(i) */\nfunction dfs(i: number, src: number[], buf: number[], tar: number[]): void {\n    // Если в src остался только один диск, сразу переместить его в tar\n    if (i === 1) {\n        move(src, tar);\n        return;\n    }\n    // Подзадача f(i-1): переместить верхние i-1 дисков из src в buf с помощью tar\n    dfs(i - 1, src, tar, buf);\n    // Подзадача f(1): переместить оставшийся один диск из src в tar\n    move(src, tar);\n    // Подзадача f(i-1): переместить верхние i-1 дисков из buf в tar с помощью src\n    dfs(i - 1, buf, src, tar);\n}\n\n/* Решить задачу Ханойской башни */\nfunction solveHanota(A: number[], B: number[], C: number[]): void {\n    const n = A.length;\n    // Переместить верхние n дисков из A в C с помощью B\n    dfs(n, A, B, C);\n}\n
        hanota.dart
        /* Переместить один диск */\nvoid move(List<int> src, List<int> tar) {\n  // Снять диск с вершины src\n  int pan = src.removeLast();\n  // Положить диск на вершину tar\n  tar.add(pan);\n}\n\n/* Решить задачу Ханойской башни f(i) */\nvoid dfs(int i, List<int> src, List<int> buf, List<int> tar) {\n  // Если в src остался только один диск, сразу переместить его в tar\n  if (i == 1) {\n    move(src, tar);\n    return;\n  }\n  // Подзадача f(i-1): переместить верхние i-1 дисков из src в buf с помощью tar\n  dfs(i - 1, src, tar, buf);\n  // Подзадача f(1): переместить оставшийся один диск из src в tar\n  move(src, tar);\n  // Подзадача f(i-1): переместить верхние i-1 дисков из buf в tar с помощью src\n  dfs(i - 1, buf, src, tar);\n}\n\n/* Решить задачу Ханойской башни */\nvoid solveHanota(List<int> A, List<int> B, List<int> C) {\n  int n = A.length;\n  // Переместить верхние n дисков из A в C с помощью B\n  dfs(n, A, B, C);\n}\n
        hanota.rs
        /* Переместить один диск */\nfn move_pan(src: &mut Vec<i32>, tar: &mut Vec<i32>) {\n    // Снять диск с вершины src\n    let pan = src.pop().unwrap();\n    // Положить диск на вершину tar\n    tar.push(pan);\n}\n\n/* Решить задачу Ханойской башни f(i) */\nfn dfs(i: i32, src: &mut Vec<i32>, buf: &mut Vec<i32>, tar: &mut Vec<i32>) {\n    // Если в src остался только один диск, сразу переместить его в tar\n    if i == 1 {\n        move_pan(src, tar);\n        return;\n    }\n    // Подзадача f(i-1): переместить верхние i-1 дисков из src в buf с помощью tar\n    dfs(i - 1, src, tar, buf);\n    // Подзадача f(1): переместить оставшийся один диск из src в tar\n    move_pan(src, tar);\n    // Подзадача f(i-1): переместить верхние i-1 дисков из buf в tar с помощью src\n    dfs(i - 1, buf, src, tar);\n}\n\n/* Решить задачу Ханойской башни */\nfn solve_hanota(A: &mut Vec<i32>, B: &mut Vec<i32>, C: &mut Vec<i32>) {\n    let n = A.len() as i32;\n    // Переместить верхние n дисков из A в C с помощью B\n    dfs(n, A, B, C);\n}\n
        hanota.c
        /* Переместить один диск */\nvoid move(int *src, int *srcSize, int *tar, int *tarSize) {\n    // Снять диск с вершины src\n    int pan = src[*srcSize - 1];\n    src[*srcSize - 1] = 0;\n    (*srcSize)--;\n    // Положить диск на вершину tar\n    tar[*tarSize] = pan;\n    (*tarSize)++;\n}\n\n/* Решить задачу Ханойской башни f(i) */\nvoid dfs(int i, int *src, int *srcSize, int *buf, int *bufSize, int *tar, int *tarSize) {\n    // Если в src остался только один диск, сразу переместить его в tar\n    if (i == 1) {\n        move(src, srcSize, tar, tarSize);\n        return;\n    }\n    // Подзадача f(i-1): переместить верхние i-1 дисков из src в buf с помощью tar\n    dfs(i - 1, src, srcSize, tar, tarSize, buf, bufSize);\n    // Подзадача f(1): переместить оставшийся один диск из src в tar\n    move(src, srcSize, tar, tarSize);\n    // Подзадача f(i-1): переместить верхние i-1 дисков из buf в tar с помощью src\n    dfs(i - 1, buf, bufSize, src, srcSize, tar, tarSize);\n}\n\n/* Решить задачу Ханойской башни */\nvoid solveHanota(int *A, int *ASize, int *B, int *BSize, int *C, int *CSize) {\n    // Переместить верхние n дисков из A в C с помощью B\n    dfs(*ASize, A, ASize, B, BSize, C, CSize);\n}\n
        hanota.kt
        /* Переместить один диск */\nfun move(src: MutableList<Int>, tar: MutableList<Int>) {\n    // Снять диск с вершины src\n    val pan = src.removeAt(src.size - 1)\n    // Положить диск на вершину tar\n    tar.add(pan)\n}\n\n/* Решить задачу Ханойской башни f(i) */\nfun dfs(i: Int, src: MutableList<Int>, buf: MutableList<Int>, tar: MutableList<Int>) {\n    // Если в src остался только один диск, сразу переместить его в tar\n    if (i == 1) {\n        move(src, tar)\n        return\n    }\n    // Подзадача f(i-1): переместить верхние i-1 дисков из src в buf с помощью tar\n    dfs(i - 1, src, tar, buf)\n    // Подзадача f(1): переместить оставшийся один диск из src в tar\n    move(src, tar)\n    // Подзадача f(i-1): переместить верхние i-1 дисков из buf в tar с помощью src\n    dfs(i - 1, buf, src, tar)\n}\n\n/* Решить задачу Ханойской башни */\nfun solveHanota(A: MutableList<Int>, B: MutableList<Int>, C: MutableList<Int>) {\n    val n = A.size\n    // Переместить верхние n дисков из A в C с помощью B\n    dfs(n, A, B, C)\n}\n
        hanota.rb
        ### Переместить один диск ###\ndef move(src, tar)\n  # Снять диск с вершины src\n  pan = src.pop\n  # Положить диск на вершину tar\n  tar << pan\nend\n\n### Решить задачу Ханойской башни f(i) ###\ndef dfs(i, src, buf, tar)\n  # Если в src остался только один диск, сразу переместить его в tar\n  if i == 1\n    move(src, tar)\n    return\n  end\n\n  # Подзадача f(i-1): переместить верхние i-1 дисков из src в buf с помощью tar\n  dfs(i - 1, src, tar, buf)\n  # Подзадача f(1): переместить оставшийся один диск из src в tar\n  move(src, tar)\n  # Подзадача f(i-1): переместить верхние i-1 дисков из buf в tar с помощью src\n  dfs(i - 1, buf, src, tar)\nend\n\n### Решить задачу Ханойской башни ###\ndef solve_hanota(_A, _B, _C)\n  n = _A.length\n  # Переместить верхние n дисков из A в C с помощью B\n  dfs(n, _A, _B, _C)\nend\n
        Визуализация кода

        Во весь экран >

        Как показано на рисунке 12-15, задача о Ханойской башне формирует дерево рекурсии высоты \\(n\\) , в котором каждый узел представляет подзадачу и соответствует одному открытому вызову dfs(). Поэтому временная сложность равна \\(O(2^n)\\) , а пространственная сложность равна \\(O(n)\\) .

        Рисунок 12-15   Дерево рекурсии задачи о Ханойской башне

        Quote

        Задача о Ханойской башне происходит из древней легенды. В одном из храмов древней Индии монахи имели три высоких алмазных стержня и \\(64\\) золотых диска разного размера. Монахи непрерывно перекладывали диски и верили, что в тот момент, когда последний диск будет правильно перенесен, мир подойдет к концу.

        Однако даже если бы монахи перемещали по одному диску в секунду, им понадобилось бы примерно \\(2^{64} \\approx 1.84×10^{19}\\) секунд, то есть около \\(585\\) миллиардов лет, что намного превышает текущую оценку возраста Вселенной. Поэтому, если легенда и верна, нам, вероятно, пока не о чем беспокоиться.

        ","path":["Глава 12. Разделяй и властвуй","12.4   Задача о Ханойской башне"],"tags":[]},{"location":"chapter_divide_and_conquer/summary/","level":1,"title":"12.5   Резюме","text":"","path":["Глава 12. Разделяй и властвуй","12.5   Резюме"],"tags":[]},{"location":"chapter_divide_and_conquer/summary/#1","level":3,"title":"1.   Ключевые выводы","text":"
        • «Разделяй и властвуй» - это распространенная стратегия проектирования алгоритмов, которая включает два этапа: разделение (декомпозицию) и объединение (синтез), и обычно реализуется с помощью рекурсии.
        • Критерии применимости этой стратегии к задаче включают: возможность разложения задачи, независимость подзадач и возможность объединения их решений.
        • Сортировка слиянием является типичным применением стратегии «разделяй и властвуй»: она рекурсивно делит массив на два равных по длине подмассива, пока не останется массив из одного элемента, после чего начинает поэтапное объединение.
        • Использование стратегии «разделяй и властвуй» часто позволяет повысить эффективность алгоритма. С одной стороны, она уменьшает число операций. С другой - после разбиения способствует параллельной оптимизации на уровне системы.
        • «Разделяй и властвуй» не только помогает решать многие алгоритмические задачи, но и широко используется при проектировании структур данных и алгоритмов, поэтому его можно встретить буквально повсюду.
        • По сравнению с полным перебором адаптивный поиск работает эффективнее. Алгоритмы поиска со сложностью \\(O(\\log n)\\) обычно реализуются на основе стратегии «разделяй и властвуй».
        • Двоичный поиск - еще одно типичное применение стратегии «разделяй и властвуй», в котором отсутствует шаг объединения решений подзадач. Его можно реализовать рекурсивно, опираясь на эту стратегию.
        • В задаче построения двоичного дерева исходная задача построения дерева может быть разбита на две подзадачи: построение левого и правого поддеревьев, а реализуется это через разбиение индексных интервалов прямого и симметричного обходов.
        • В задаче о Ханойской башне задача размера \\(n\\) разбивается на две подзадачи размера \\(n-1\\) и одну подзадачу размера \\(1\\) . После последовательного решения этих трех подзадач исходная задача также оказывается решенной.
        ","path":["Глава 12. Разделяй и властвуй","12.5   Резюме"],"tags":[]},{"location":"chapter_dynamic_programming/","level":1,"title":"Глава 14.   Динамическое программирование","text":"

        Abstract

        Ручьи впадают в реки, а реки вливаются в море.

        Динамическое программирование собирает решения малых задач в ответ на большую задачу и шаг за шагом ведет нас к ее решению.

        ","path":["Глава 14. Динамическое программирование","Глава 14.   Динамическое программирование"],"tags":[]},{"location":"chapter_dynamic_programming/#_1","level":2,"title":"Содержание главы","text":"
        • 14.1   Первое знакомство с динамическим программированием
        • 14.2   Свойства задач динамического программирования
        • 14.3   Подход к решению задач динамического программирования
        • 14.4   Задача о рюкзаке 0-1
        • 14.5   Задача о полном рюкзаке
        • 14.6   Задача о расстоянии редактирования
        • 14.7   Резюме
        ","path":["Глава 14. Динамическое программирование","Глава 14.   Динамическое программирование"],"tags":[]},{"location":"chapter_dynamic_programming/dp_problem_features/","level":1,"title":"14.2   Свойства задач динамического программирования","text":"

        В предыдущем разделе мы увидели, как динамическое программирование решает исходную задачу через разложение на подзадачи. На самом деле разложение на подзадачи - это общий алгоритмический подход, но в методе «разделяй и властвуй», динамическом программировании и поиске с возвратом акценты расставлены по-разному.

        • Алгоритмы «разделяй и властвуй» рекурсивно раскладывают исходную задачу на несколько независимых подзадач, пока не будет достигнута наименьшая подзадача, а затем в процессе возврата объединяют решения подзадач в решение исходной задачи.
        • Динамическое программирование тоже раскладывает задачу рекурсивно, но его главное отличие от метода «разделяй и властвуй» в том, что подзадачи здесь зависят друг от друга и в процессе разложения возникает много перекрывающихся подзадач.
        • Алгоритм поиска с возвратом перебирает все возможные решения через попытки и откат и с помощью обрезки избегает ненужных ветвей поиска. Решение исходной задачи состоит из последовательности решений, и подзадачей можно считать префикс этой последовательности решений.

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

        ","path":["Глава 14. Динамическое программирование","14.2   Свойства задач динамического программирования"],"tags":[]},{"location":"chapter_dynamic_programming/dp_problem_features/#1421","level":2,"title":"14.2.1   Оптимальная подструктура","text":"

        Немного изменим задачу о подъеме по лестнице, чтобы нагляднее показать понятие оптимальной подструктуры.

        Минимальная стоимость подъема по лестнице

        Дана лестница, по которой можно подниматься на \\(1\\) или на \\(2\\) ступени за раз. На каждой ступени указано неотрицательное целое число, обозначающее цену попадания на эту ступень. Дан массив неотрицательных целых чисел \\(cost\\) , где \\(cost[i]\\) - это цена для ступени \\(i\\) , а \\(cost[0]\\) соответствует земле (начальной позиции). Найдите минимальную суммарную стоимость, необходимую для достижения вершины.

        Как показано на рисунке 14-6, если цены для ступеней \\(1\\) , \\(2\\) и \\(3\\) равны соответственно \\(1\\) , \\(10\\) и \\(1\\) , то минимальная стоимость подъема с земли на третью ступень равна \\(2\\) .

        Рисунок 14-6   Минимальная стоимость подъема на 3-ю ступень

        Пусть \\(dp[i]\\) обозначает накопленную стоимость подъема на ступень \\(i\\) . Поскольку на ступень \\(i\\) можно прийти только со ступени \\(i - 1\\) или со ступени \\(i - 2\\) , значение \\(dp[i]\\) может быть либо \\(dp[i - 1] + cost[i]\\) , либо \\(dp[i - 2] + cost[i]\\) . Чтобы минимизировать стоимость, нужно выбрать меньший из этих двух вариантов:

        \\[ dp[i] = \\min(dp[i-1], dp[i-2]) + cost[i] \\]

        Отсюда и возникает смысл оптимальной подструктуры: оптимальное решение исходной задачи строится из оптимальных решений подзадач.

        Очевидно, что эта задача обладает оптимальной подструктурой: мы берем лучшее из двух оптимальных решений подзадач \\(dp[i-1]\\) и \\(dp[i-2]\\) и на его основе строим оптимальное решение исходной задачи \\(dp[i]\\) .

        А обладает ли оптимальной подструктурой исходная задача о числе способов подъема по лестнице из прошлого раздела? Формально она не про оптимум, а про подсчет количества. Но если переформулировать ее как «найдите максимальное количество способов», мы неожиданно увидим, что хотя исходная задача осталась по сути той же, оптимальная подструктура стала явной: максимальное число способов добраться до ступени \\(n\\) равно сумме максимальных чисел способов добраться до ступеней \\(n-1\\) и \\(n-2\\) . То есть объяснение оптимальной подструктуры в разных задачах может быть довольно гибким.

        Зная уравнение перехода состояния, а также начальные состояния \\(dp[1] = cost[1]\\) и \\(dp[2] = cost[2]\\) , мы можем сразу написать код динамического программирования:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_cost_climbing_stairs_dp.py
        def min_cost_climbing_stairs_dp(cost: list[int]) -> int:\n    \"\"\"Минимальная стоимость подъема по лестнице: динамическое программирование\"\"\"\n    n = len(cost) - 1\n    if n == 1 or n == 2:\n        return cost[n]\n    # Инициализация таблицы dp для хранения решений подзадач\n    dp = [0] * (n + 1)\n    # Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1], dp[2] = cost[1], cost[2]\n    # Переход состояний: постепенное решение больших подзадач через меньшие\n    for i in range(3, n + 1):\n        dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]\n    return dp[n]\n
        min_cost_climbing_stairs_dp.cpp
        /* Минимальная стоимость подъема по лестнице: динамическое программирование */\nint minCostClimbingStairsDP(vector<int> &cost) {\n    int n = cost.size() - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    // Инициализация таблицы dp для хранения решений подзадач\n    vector<int> dp(n + 1);\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (int i = 3; i <= n; i++) {\n        dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    return dp[n];\n}\n
        min_cost_climbing_stairs_dp.java
        /* Минимальная стоимость подъема по лестнице: динамическое программирование */\nint minCostClimbingStairsDP(int[] cost) {\n    int n = cost.length - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    // Инициализация таблицы dp для хранения решений подзадач\n    int[] dp = new int[n + 1];\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (int i = 3; i <= n; i++) {\n        dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    return dp[n];\n}\n
        min_cost_climbing_stairs_dp.cs
        /* Минимальная стоимость подъема по лестнице: динамическое программирование */\nint MinCostClimbingStairsDP(int[] cost) {\n    int n = cost.Length - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    // Инициализация таблицы dp для хранения решений подзадач\n    int[] dp = new int[n + 1];\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (int i = 3; i <= n; i++) {\n        dp[i] = Math.Min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    return dp[n];\n}\n
        min_cost_climbing_stairs_dp.go
        /* Минимальная стоимость подъема по лестнице: динамическое программирование */\nfunc minCostClimbingStairsDP(cost []int) int {\n    n := len(cost) - 1\n    if n == 1 || n == 2 {\n        return cost[n]\n    }\n    min := func(a, b int) int {\n        if a < b {\n            return a\n        }\n        return b\n    }\n    // Инициализация таблицы dp для хранения решений подзадач\n    dp := make([]int, n+1)\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1] = cost[1]\n    dp[2] = cost[2]\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for i := 3; i <= n; i++ {\n        dp[i] = min(dp[i-1], dp[i-2]) + cost[i]\n    }\n    return dp[n]\n}\n
        min_cost_climbing_stairs_dp.swift
        /* Минимальная стоимость подъема по лестнице: динамическое программирование */\nfunc minCostClimbingStairsDP(cost: [Int]) -> Int {\n    let n = cost.count - 1\n    if n == 1 || n == 2 {\n        return cost[n]\n    }\n    // Инициализация таблицы dp для хранения решений подзадач\n    var dp = Array(repeating: 0, count: n + 1)\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1] = cost[1]\n    dp[2] = cost[2]\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for i in 3 ... n {\n        dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]\n    }\n    return dp[n]\n}\n
        min_cost_climbing_stairs_dp.js
        /* Минимальная стоимость подъема по лестнице: динамическое программирование */\nfunction minCostClimbingStairsDP(cost) {\n    const n = cost.length - 1;\n    if (n === 1 || n === 2) {\n        return cost[n];\n    }\n    // Инициализация таблицы dp для хранения решений подзадач\n    const dp = new Array(n + 1);\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (let i = 3; i <= n; i++) {\n        dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    return dp[n];\n}\n
        min_cost_climbing_stairs_dp.ts
        /* Минимальная стоимость подъема по лестнице: динамическое программирование */\nfunction minCostClimbingStairsDP(cost: Array<number>): number {\n    const n = cost.length - 1;\n    if (n === 1 || n === 2) {\n        return cost[n];\n    }\n    // Инициализация таблицы dp для хранения решений подзадач\n    const dp = new Array(n + 1);\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (let i = 3; i <= n; i++) {\n        dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    return dp[n];\n}\n
        min_cost_climbing_stairs_dp.dart
        /* Минимальная стоимость подъема по лестнице: динамическое программирование */\nint minCostClimbingStairsDP(List<int> cost) {\n  int n = cost.length - 1;\n  if (n == 1 || n == 2) return cost[n];\n  // Инициализация таблицы dp для хранения решений подзадач\n  List<int> dp = List.filled(n + 1, 0);\n  // Начальное состояние: заранее задать решения наименьших подзадач\n  dp[1] = cost[1];\n  dp[2] = cost[2];\n  // Переход состояний: постепенное решение больших подзадач через меньшие\n  for (int i = 3; i <= n; i++) {\n    dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];\n  }\n  return dp[n];\n}\n
        min_cost_climbing_stairs_dp.rs
        /* Минимальная стоимость подъема по лестнице: динамическое программирование */\nfn min_cost_climbing_stairs_dp(cost: &[i32]) -> i32 {\n    let n = cost.len() - 1;\n    if n == 1 || n == 2 {\n        return cost[n];\n    }\n    // Инициализация таблицы dp для хранения решений подзадач\n    let mut dp = vec![-1; n + 1];\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for i in 3..=n {\n        dp[i] = cmp::min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    dp[n]\n}\n
        min_cost_climbing_stairs_dp.c
        /* Минимальная стоимость подъема по лестнице: динамическое программирование */\nint minCostClimbingStairsDP(int cost[], int costSize) {\n    int n = costSize - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    // Инициализация таблицы dp для хранения решений подзадач\n    int *dp = calloc(n + 1, sizeof(int));\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (int i = 3; i <= n; i++) {\n        dp[i] = myMin(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    int res = dp[n];\n    // Освободить память\n    free(dp);\n    return res;\n}\n
        min_cost_climbing_stairs_dp.kt
        /* Минимальная стоимость подъема по лестнице: динамическое программирование */\nfun minCostClimbingStairsDP(cost: IntArray): Int {\n    val n = cost.size - 1\n    if (n == 1 || n == 2) return cost[n]\n    // Инициализация таблицы dp для хранения решений подзадач\n    val dp = IntArray(n + 1)\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1] = cost[1]\n    dp[2] = cost[2]\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (i in 3..n) {\n        dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]\n    }\n    return dp[n]\n}\n
        min_cost_climbing_stairs_dp.rb
        ### Минимальная стоимость подъема по лестнице: динамическое программирование ###\ndef min_cost_climbing_stairs_dp(cost)\n  n = cost.length - 1\n  return cost[n] if n == 1 || n == 2\n  # Инициализация таблицы dp для хранения решений подзадач\n  dp = Array.new(n + 1, 0)\n  # Начальное состояние: заранее задать решения наименьших подзадач\n  dp[1], dp[2] = cost[1], cost[2]\n  # Переход состояний: постепенное решение больших подзадач через меньшие\n  (3...(n + 1)).each { |i| dp[i] = [dp[i - 1], dp[i - 2]].min + cost[i] }\n  dp[n]\nend\n
        Визуализация кода

        Во весь экран >

        На рисунке 14-7 показан процесс динамического программирования для этой задачи.

        Рисунок 14-7   Процесс динамического программирования для минимальной стоимости подъема

        В этой задаче тоже можно оптимизировать пространство, сжав одномерное состояние в нулевое измерение и тем самым уменьшив пространственную сложность с \\(O(n)\\) до \\(O(1)\\) :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_cost_climbing_stairs_dp.py
        def min_cost_climbing_stairs_dp_comp(cost: list[int]) -> int:\n    \"\"\"Минимальная стоимость подъема по лестнице: динамическое программирование с оптимизацией памяти\"\"\"\n    n = len(cost) - 1\n    if n == 1 or n == 2:\n        return cost[n]\n    a, b = cost[1], cost[2]\n    for i in range(3, n + 1):\n        a, b = b, min(a, b) + cost[i]\n    return b\n
        min_cost_climbing_stairs_dp.cpp
        /* Минимальная стоимость подъема по лестнице: динамическое программирование с оптимизацией памяти */\nint minCostClimbingStairsDPComp(vector<int> &cost) {\n    int n = cost.size() - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    int a = cost[1], b = cost[2];\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
        min_cost_climbing_stairs_dp.java
        /* Минимальная стоимость подъема по лестнице: динамическое программирование с оптимизацией памяти */\nint minCostClimbingStairsDPComp(int[] cost) {\n    int n = cost.length - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    int a = cost[1], b = cost[2];\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = Math.min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
        min_cost_climbing_stairs_dp.cs
        /* Минимальная стоимость подъема по лестнице: динамическое программирование с оптимизацией памяти */\nint MinCostClimbingStairsDPComp(int[] cost) {\n    int n = cost.Length - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    int a = cost[1], b = cost[2];\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = Math.Min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
        min_cost_climbing_stairs_dp.go
        /* Минимальная стоимость подъема по лестнице: динамическое программирование с оптимизацией памяти */\nfunc minCostClimbingStairsDPComp(cost []int) int {\n    n := len(cost) - 1\n    if n == 1 || n == 2 {\n        return cost[n]\n    }\n    min := func(a, b int) int {\n        if a < b {\n            return a\n        }\n        return b\n    }\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    a, b := cost[1], cost[2]\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for i := 3; i <= n; i++ {\n        tmp := b\n        b = min(a, tmp) + cost[i]\n        a = tmp\n    }\n    return b\n}\n
        min_cost_climbing_stairs_dp.swift
        /* Минимальная стоимость подъема по лестнице: динамическое программирование с оптимизацией памяти */\nfunc minCostClimbingStairsDPComp(cost: [Int]) -> Int {\n    let n = cost.count - 1\n    if n == 1 || n == 2 {\n        return cost[n]\n    }\n    var (a, b) = (cost[1], cost[2])\n    for i in 3 ... n {\n        (a, b) = (b, min(a, b) + cost[i])\n    }\n    return b\n}\n
        min_cost_climbing_stairs_dp.js
        /* Минимальная стоимость подъема по лестнице: динамическое программирование с оптимизацией памяти */\nfunction minCostClimbingStairsDPComp(cost) {\n    const n = cost.length - 1;\n    if (n === 1 || n === 2) {\n        return cost[n];\n    }\n    let a = cost[1],\n        b = cost[2];\n    for (let i = 3; i <= n; i++) {\n        const tmp = b;\n        b = Math.min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
        min_cost_climbing_stairs_dp.ts
        /* Минимальная стоимость подъема по лестнице: динамическое программирование с оптимизацией памяти */\nfunction minCostClimbingStairsDPComp(cost: Array<number>): number {\n    const n = cost.length - 1;\n    if (n === 1 || n === 2) {\n        return cost[n];\n    }\n    let a = cost[1],\n        b = cost[2];\n    for (let i = 3; i <= n; i++) {\n        const tmp = b;\n        b = Math.min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
        min_cost_climbing_stairs_dp.dart
        /* Минимальная стоимость подъема по лестнице: динамическое программирование с оптимизацией памяти */\nint minCostClimbingStairsDPComp(List<int> cost) {\n  int n = cost.length - 1;\n  if (n == 1 || n == 2) return cost[n];\n  int a = cost[1], b = cost[2];\n  for (int i = 3; i <= n; i++) {\n    int tmp = b;\n    b = min(a, tmp) + cost[i];\n    a = tmp;\n  }\n  return b;\n}\n
        min_cost_climbing_stairs_dp.rs
        /* Минимальная стоимость подъема по лестнице: динамическое программирование с оптимизацией памяти */\nfn min_cost_climbing_stairs_dp_comp(cost: &[i32]) -> i32 {\n    let n = cost.len() - 1;\n    if n == 1 || n == 2 {\n        return cost[n];\n    };\n    let (mut a, mut b) = (cost[1], cost[2]);\n    for i in 3..=n {\n        let tmp = b;\n        b = cmp::min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    b\n}\n
        min_cost_climbing_stairs_dp.c
        /* Минимальная стоимость подъема по лестнице: динамическое программирование с оптимизацией памяти */\nint minCostClimbingStairsDPComp(int cost[], int costSize) {\n    int n = costSize - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    int a = cost[1], b = cost[2];\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = myMin(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
        min_cost_climbing_stairs_dp.kt
        /* Минимальная стоимость подъема по лестнице: динамическое программирование с оптимизацией памяти */\nfun minCostClimbingStairsDPComp(cost: IntArray): Int {\n    val n = cost.size - 1\n    if (n == 1 || n == 2) return cost[n]\n    var a = cost[1]\n    var b = cost[2]\n    for (i in 3..n) {\n        val tmp = b\n        b = min(a, tmp) + cost[i]\n        a = tmp\n    }\n    return b\n}\n
        min_cost_climbing_stairs_dp.rb
        ### Минимальная стоимость подъема по лестнице: динамическое программирование ###\ndef min_cost_climbing_stairs_dp(cost)\n  n = cost.length - 1\n  return cost[n] if n == 1 || n == 2\n  # Инициализация таблицы dp для хранения решений подзадач\n  dp = Array.new(n + 1, 0)\n  # Начальное состояние: заранее задать решения наименьших подзадач\n  dp[1], dp[2] = cost[1], cost[2]\n  # Переход состояний: постепенное решение больших подзадач через меньшие\n  (3...(n + 1)).each { |i| dp[i] = [dp[i - 1], dp[i - 2]].min + cost[i] }\n  dp[n]\nend\n\n# Минимальная стоимость подъема по лестнице: динамическое программирование с оптимизацией памяти\ndef min_cost_climbing_stairs_dp_comp(cost)\n  n = cost.length - 1\n  return cost[n] if n == 1 || n == 2\n  a, b = cost[1], cost[2]\n  (3...(n + 1)).each { |i| a, b = b, [a, b].min + cost[i] }\n  b\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 14. Динамическое программирование","14.2   Свойства задач динамического программирования"],"tags":[]},{"location":"chapter_dynamic_programming/dp_problem_features/#1422","level":2,"title":"14.2.2   Отсутствие последствий","text":"

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

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

        Однако если добавить в задачу дополнительное ограничение, ситуация изменится.

        Подъем по лестнице с ограничением

        Дана лестница из \\(n\\) ступеней. За один шаг можно подняться на \\(1\\) или на \\(2\\) ступени, но нельзя два раунда подряд прыгать на \\(1\\) ступень. Сколькими способами можно добраться до вершины?

        Как показано на рисунке 14-8, на третью ступень теперь существует только \\(2\\) допустимых способа добраться: вариант с тремя последовательными прыжками на \\(1\\) не удовлетворяет ограничению и потому отбрасывается.

        Рисунок 14-8   Число способов подняться на 3-ю ступень при наличии ограничения

        В этой задаче, если в предыдущем раунде был сделан прыжок на \\(1\\) ступень, то в следующем раунде уже обязательно нужно прыгнуть на \\(2\\) ступени. Иными словами, следующий выбор уже нельзя определить только по текущему состоянию (текущему номеру ступени) - он зависит еще и от предыдущего состояния (с какой ступени мы пришли в прошлый раз).

        Нетрудно заметить, что в таком виде задача больше не удовлетворяет свойству отсутствия последствий, а уравнение перехода состояния \\(dp[i] = dp[i-1] + dp[i-2]\\) перестает работать, потому что \\(dp[i-1]\\) соответствует прыжку на \\(1\\) ступень, но при этом включает множество вариантов, где предыдущий раунд тоже был прыжком на \\(1\\) ступень. Такие варианты уже нельзя напрямую учитывать в \\(dp[i]\\) , если мы хотим соблюдать ограничение.

        Поэтому нам нужно расширить определение состояния: состояние \\([i, j]\\) означает, что мы находимся на ступени \\(i\\) и в предыдущем раунде прыгнули на \\(j\\) ступеней, где \\(j \\in \\{1, 2\\}\\) . Такое определение состояния эффективно различает, был ли в прошлом раунде прыжок на \\(1\\) или на \\(2\\) ступени, и позволяет корректно определить, откуда произошло текущее состояние.

        • Если в предыдущем раунде был прыжок на \\(1\\) ступень, то в раунде перед ним мог быть только прыжок на \\(2\\) ступени, то есть \\(dp[i, 1]\\) может перейти только из \\(dp[i-1, 2]\\) .
        • Если в предыдущем раунде был прыжок на \\(2\\) ступени, то еще шагом раньше можно было прыгнуть либо на \\(1\\) , либо на \\(2\\) ступени, то есть \\(dp[i, 2]\\) может переходить из \\(dp[i-2, 1]\\) или из \\(dp[i-2, 2]\\) .

        Как показано на рисунке 14-9, при таком определении \\(dp[i, j]\\) обозначает число способов для состояния \\([i, j]\\) . Тогда уравнение перехода состояния имеет вид:

        \\[ \\begin{cases} dp[i, 1] = dp[i-1, 2] \\\\ dp[i, 2] = dp[i-2, 1] + dp[i-2, 2] \\end{cases} \\]

        Рисунок 14-9   Рекуррентная связь с учетом ограничения

        В конце достаточно вернуть \\(dp[n, 1] + dp[n, 2]\\). Эта сумма и представляет общее число способов добраться до ступени \\(n\\) :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_constraint_dp.py
        def climbing_stairs_constraint_dp(n: int) -> int:\n    \"\"\"Подъем по лестнице с ограничениями: динамическое программирование\"\"\"\n    if n == 1 or n == 2:\n        return 1\n    # Инициализация таблицы dp для хранения решений подзадач\n    dp = [[0] * 3 for _ in range(n + 1)]\n    # Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1][1], dp[1][2] = 1, 0\n    dp[2][1], dp[2][2] = 0, 1\n    # Переход состояний: постепенное решение больших подзадач через меньшие\n    for i in range(3, n + 1):\n        dp[i][1] = dp[i - 1][2]\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2]\n    return dp[n][1] + dp[n][2]\n
        climbing_stairs_constraint_dp.cpp
        /* Подъем по лестнице с ограничениями: динамическое программирование */\nint climbingStairsConstraintDP(int n) {\n    if (n == 1 || n == 2) {\n        return 1;\n    }\n    // Инициализация таблицы dp для хранения решений подзадач\n    vector<vector<int>> dp(n + 1, vector<int>(3, 0));\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (int i = 3; i <= n; i++) {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    return dp[n][1] + dp[n][2];\n}\n
        climbing_stairs_constraint_dp.java
        /* Подъем по лестнице с ограничениями: динамическое программирование */\nint climbingStairsConstraintDP(int n) {\n    if (n == 1 || n == 2) {\n        return 1;\n    }\n    // Инициализация таблицы dp для хранения решений подзадач\n    int[][] dp = new int[n + 1][3];\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (int i = 3; i <= n; i++) {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    return dp[n][1] + dp[n][2];\n}\n
        climbing_stairs_constraint_dp.cs
        /* Подъем по лестнице с ограничениями: динамическое программирование */\nint ClimbingStairsConstraintDP(int n) {\n    if (n == 1 || n == 2) {\n        return 1;\n    }\n    // Инициализация таблицы dp для хранения решений подзадач\n    int[,] dp = new int[n + 1, 3];\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1, 1] = 1;\n    dp[1, 2] = 0;\n    dp[2, 1] = 0;\n    dp[2, 2] = 1;\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (int i = 3; i <= n; i++) {\n        dp[i, 1] = dp[i - 1, 2];\n        dp[i, 2] = dp[i - 2, 1] + dp[i - 2, 2];\n    }\n    return dp[n, 1] + dp[n, 2];\n}\n
        climbing_stairs_constraint_dp.go
        /* Подъем по лестнице с ограничениями: динамическое программирование */\nfunc climbingStairsConstraintDP(n int) int {\n    if n == 1 || n == 2 {\n        return 1\n    }\n    // Инициализация таблицы dp для хранения решений подзадач\n    dp := make([][3]int, n+1)\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1][1] = 1\n    dp[1][2] = 0\n    dp[2][1] = 0\n    dp[2][2] = 1\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for i := 3; i <= n; i++ {\n        dp[i][1] = dp[i-1][2]\n        dp[i][2] = dp[i-2][1] + dp[i-2][2]\n    }\n    return dp[n][1] + dp[n][2]\n}\n
        climbing_stairs_constraint_dp.swift
        /* Подъем по лестнице с ограничениями: динамическое программирование */\nfunc climbingStairsConstraintDP(n: Int) -> Int {\n    if n == 1 || n == 2 {\n        return 1\n    }\n    // Инициализация таблицы dp для хранения решений подзадач\n    var dp = Array(repeating: Array(repeating: 0, count: 3), count: n + 1)\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1][1] = 1\n    dp[1][2] = 0\n    dp[2][1] = 0\n    dp[2][2] = 1\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for i in 3 ... n {\n        dp[i][1] = dp[i - 1][2]\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2]\n    }\n    return dp[n][1] + dp[n][2]\n}\n
        climbing_stairs_constraint_dp.js
        /* Подъем по лестнице с ограничениями: динамическое программирование */\nfunction climbingStairsConstraintDP(n) {\n    if (n === 1 || n === 2) {\n        return 1;\n    }\n    // Инициализация таблицы dp для хранения решений подзадач\n    const dp = Array.from(new Array(n + 1), () => new Array(3));\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (let i = 3; i <= n; i++) {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    return dp[n][1] + dp[n][2];\n}\n
        climbing_stairs_constraint_dp.ts
        /* Подъем по лестнице с ограничениями: динамическое программирование */\nfunction climbingStairsConstraintDP(n: number): number {\n    if (n === 1 || n === 2) {\n        return 1;\n    }\n    // Инициализация таблицы dp для хранения решений подзадач\n    const dp = Array.from({ length: n + 1 }, () => new Array(3));\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (let i = 3; i <= n; i++) {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    return dp[n][1] + dp[n][2];\n}\n
        climbing_stairs_constraint_dp.dart
        /* Подъем по лестнице с ограничениями: динамическое программирование */\nint climbingStairsConstraintDP(int n) {\n  if (n == 1 || n == 2) {\n    return 1;\n  }\n  // Инициализация таблицы dp для хранения решений подзадач\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(3, 0));\n  // Начальное состояние: заранее задать решения наименьших подзадач\n  dp[1][1] = 1;\n  dp[1][2] = 0;\n  dp[2][1] = 0;\n  dp[2][2] = 1;\n  // Переход состояний: постепенное решение больших подзадач через меньшие\n  for (int i = 3; i <= n; i++) {\n    dp[i][1] = dp[i - 1][2];\n    dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n  }\n  return dp[n][1] + dp[n][2];\n}\n
        climbing_stairs_constraint_dp.rs
        /* Подъем по лестнице с ограничениями: динамическое программирование */\nfn climbing_stairs_constraint_dp(n: usize) -> i32 {\n    if n == 1 || n == 2 {\n        return 1;\n    };\n    // Инициализация таблицы dp для хранения решений подзадач\n    let mut dp = vec![vec![-1; 3]; n + 1];\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for i in 3..=n {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    dp[n][1] + dp[n][2]\n}\n
        climbing_stairs_constraint_dp.c
        /* Подъем по лестнице с ограничениями: динамическое программирование */\nint climbingStairsConstraintDP(int n) {\n    if (n == 1 || n == 2) {\n        return 1;\n    }\n    // Инициализация таблицы dp для хранения решений подзадач\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(3, sizeof(int));\n    }\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (int i = 3; i <= n; i++) {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    int res = dp[n][1] + dp[n][2];\n    // Освободить память\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    free(dp);\n    return res;\n}\n
        climbing_stairs_constraint_dp.kt
        /* Подъем по лестнице с ограничениями: динамическое программирование */\nfun climbingStairsConstraintDP(n: Int): Int {\n    if (n == 1 || n == 2) {\n        return 1\n    }\n    // Инициализация таблицы dp для хранения решений подзадач\n    val dp = Array(n + 1) { IntArray(3) }\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1][1] = 1\n    dp[1][2] = 0\n    dp[2][1] = 0\n    dp[2][2] = 1\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (i in 3..n) {\n        dp[i][1] = dp[i - 1][2]\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2]\n    }\n    return dp[n][1] + dp[n][2]\n}\n
        climbing_stairs_constraint_dp.rb
        ### Подъем по лестнице с ограничениями: динамическое программирование ###\ndef climbing_stairs_constraint_dp(n)\n  return 1 if n == 1 || n == 2\n\n  # Инициализация таблицы dp для хранения решений подзадач\n  dp = Array.new(n + 1) { Array.new(3, 0) }\n  # Начальное состояние: заранее задать решения наименьших подзадач\n  dp[1][1], dp[1][2] = 1, 0\n  dp[2][1], dp[2][2] = 0, 1\n  # Переход состояний: постепенное решение больших подзадач через меньшие\n  for i in 3...(n + 1)\n    dp[i][1] = dp[i - 1][2]\n    dp[i][2] = dp[i - 2][1] + dp[i - 2][2]\n  end\n\n  dp[n][1] + dp[n][2]\nend\n
        Визуализация кода

        Во весь экран >

        В этом примере достаточно дополнительно учитывать только одно предыдущее состояние, поэтому после расширения определения состояния задача снова начинает удовлетворять свойству отсутствия последствий. Однако в некоторых задачах «зависимость от прошлого» бывает гораздо серьезнее.

        Подъем по лестнице с порождением препятствий

        Дана лестница из \\(n\\) ступеней. За один шаг можно подняться на \\(1\\) или на \\(2\\) ступени. При этом, если вы попали на ступень \\(i\\) , система автоматически создает препятствие на ступени \\(2i\\) , и на всех последующих шагах становиться на ступень \\(2i\\) уже нельзя. Например, если в первых двух раундах вы попали на ступени \\(2\\) и \\(3\\) , то после этого нельзя будет попадать на ступени \\(4\\) и \\(6\\) . Сколько существует способов добраться до вершины?

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

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

        ","path":["Глава 14. Динамическое программирование","14.2   Свойства задач динамического программирования"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/","level":1,"title":"14.3   Подход к решению задач динамического программирования","text":"

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

        1. Как определить, является ли некоторая задача задачей динамического программирования?
        2. С чего начинать решение такой задачи и как выглядит полный процесс решения?
        ","path":["Глава 14. Динамическое программирование","14.3   Подход к решению задач динамического программирования"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#1431","level":2,"title":"14.3.1   Определение задачи","text":"

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

        Задачи, подходящие для поиска с возвратом, обычно удовлетворяют «модели дерева решений». Такие задачи можно описать деревом, где каждый узел представляет одно решение, а каждый путь представляет последовательность решений.

        Иначе говоря, если в задаче есть четко выраженные решения и ответ порождается последовательностью таких решений, то она удовлетворяет модели дерева решений и обычно допускает решение через поиск с возвратом.

        Поверх этого у задач динамического программирования есть и некоторые дополнительные «плюсы».

        • В условии задачи фигурируют слова «максимальный», «минимальный», «наибольший», «наименьший» и другие формулировки оптимизации.
        • Состояния задачи можно описать списком, многомерной матрицей или деревом, и между состоянием и соседними состояниями существует рекуррентная зависимость.

        Соответственно, существуют и некоторые «минусы».

        • Цель задачи состоит в поиске всех возможных решений, а не одного оптимального решения.
        • В формулировке явно присутствуют признаки комбинаторного перечисления, и требуется вернуть сразу много конкретных вариантов.

        Если задача удовлетворяет модели дерева решений и имеет достаточно явные «плюсы», мы можем предположить, что это задача динамического программирования, а затем проверить это предположение уже в процессе решения.

        ","path":["Глава 14. Динамическое программирование","14.3   Подход к решению задач динамического программирования"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#1432","level":2,"title":"14.3.2   Этапы решения задачи","text":"

        Конкретный процесс решения задач динамического программирования зависит от природы и сложности задачи, но обычно включает следующие шаги: описание решений, определение состояний, построение таблицы \\(dp\\) , вывод уравнения перехода состояния, определение граничных условий и порядка переходов.

        Чтобы нагляднее показать этот процесс, рассмотрим классическую задачу «минимальная сумма пути».

        Question

        Дана двумерная сетка grid размера \\(n \\times m\\) , в каждой клетке которой записано неотрицательное целое число, означающее стоимость прохождения через эту клетку. Робот стартует из левой верхней клетки и за один шаг может двигаться только вправо или вниз, пока не достигнет правой нижней клетки. Верните минимальную сумму пути от левой верхней клетки до правой нижней.

        На рисунке 14-10 показан пример, в котором минимальная сумма пути равна \\(13\\) .

        Рисунок 14-10   Пример данных для задачи о минимальной сумме пути

        Шаг 1: понять решения на каждом раунде, определить состояние и тем самым получить таблицу \\(dp\\)

        В этой задаче на каждом раунде решение состоит в том, чтобы из текущей клетки сделать один шаг вниз или вправо. Пусть индексы строки и столбца текущей клетки равны \\([i, j]\\). Тогда после шага вниз или вправо индексы становятся равными \\([i+1, j]\\) или \\([i, j+1]\\) . Значит, состояние должно включать два переменных индекса: строки и столбца, то есть состояние обозначается как \\([i, j]\\) .

        Подзадача, соответствующая состоянию \\([i, j]\\) , такова: минимальная сумма пути от стартовой клетки \\([0, 0]\\) до клетки \\([i, j]\\) . Ее решение обозначается через \\(dp[i, j]\\) .

        На этом этапе мы получаем двумерную матрицу \\(dp\\) , показанную на рисунке 14-11, размер которой совпадает с размером входной сетки grid .

        Рисунок 14-11   Определение состояния и таблицы dp

        Note

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

        Каждому состоянию соответствует некоторая подзадача, и для хранения решений всех подзадач мы определяем таблицу \\(dp\\). Каждая независимая переменная состояния становится одним измерением таблицы \\(dp\\) . По сути таблица \\(dp\\) - это отображение от состояния к решению соответствующей подзадачи.

        Шаг 2: найти оптимальную подструктуру и на ее основе вывести уравнение перехода состояния

        Для состояния \\([i, j]\\) возможны только два источника: клетка сверху \\([i-1, j]\\) и клетка слева \\([i, j-1]\\) . Следовательно, оптимальная подструктура выглядит так: минимальная сумма пути до \\([i, j]\\) определяется меньшим из двух значений - минимальной суммы пути до \\([i-1, j]\\) и минимальной суммы пути до \\([i, j-1]\\) .

        По этому рассуждению получается уравнение перехода состояния, показанное на рисунке 14-12:

        \\[ dp[i, j] = \\min(dp[i-1, j], dp[i, j-1]) + grid[i, j] \\]

        Рисунок 14-12   Оптимальная подструктура и уравнение перехода состояния

        Note

        Опираясь на уже определенную таблицу \\(dp\\) , нужно продумать отношение между исходной задачей и подзадачами и найти способ построить оптимальное решение исходной задачи из оптимальных решений подзадач, то есть найти оптимальную подструктуру.

        Как только оптимальная подструктура найдена, на ее основе можно построить уравнение перехода состояния.

        Шаг 3: определить граничные условия и порядок переходов

        В этой задаче состояния в первой строке могут переходить только из клетки слева, а состояния в первом столбце - только из клетки сверху, поэтому первая строка \\(i = 0\\) и первый столбец \\(j = 0\\) образуют граничные условия.

        Как показано на рисунке 14-13, поскольку каждая клетка получается из клетки слева и клетки сверху, мы можем проходить матрицу циклами: внешний цикл по строкам, внутренний - по столбцам.

        Рисунок 14-13   Граничные условия и порядок перехода состояний

        Note

        В динамическом программировании граничные условия используются для инициализации таблицы \\(dp\\) , а в поиске - для обрезки.

        Смысл порядка перехода состояния в том, чтобы к моменту вычисления текущей подзадачи все более мелкие подзадачи, от которых она зависит, уже были вычислены корректно.

        После этого анализа мы уже можем напрямую написать код динамического программирования. Однако разложение на подзадачи - это мышление «сверху вниз», поэтому с точки зрения мышления более естественно реализовывать задачу в порядке «полный перебор \\(\\rightarrow\\) поиск с мемоизацией \\(\\rightarrow\\) динамическое программирование».

        ","path":["Глава 14. Динамическое программирование","14.3   Подход к решению задач динамического программирования"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#1-1","level":3,"title":"1.   Метод 1: полный перебор","text":"

        Начав со состояния \\([i, j]\\) , мы непрерывно раскладываем его на меньшие состояния \\([i-1, j]\\) и \\([i, j-1]\\) . Рекурсивная функция при этом имеет следующие элементы.

        • Параметры рекурсии: состояние \\([i, j]\\) .
        • Возвращаемое значение: минимальная сумма пути до \\([i, j]\\) , то есть \\(dp[i, j]\\) .
        • Условие завершения: когда \\(i = 0\\) и \\(j = 0\\) , возвращается стоимость \\(grid[0, 0]\\) .
        • Обрезка: если \\(i < 0\\) или \\(j < 0\\) , индекс выходит за границы, и в этом случае возвращается стоимость \\(+\\infty\\) , обозначающая невозможность.

        Код реализации:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py
        def min_path_sum_dfs(grid: list[list[int]], i: int, j: int) -> int:\n    \"\"\"Минимальная сумма пути: полный перебор\"\"\"\n    # Если это верхняя левая ячейка, завершить поиск\n    if i == 0 and j == 0:\n        return grid[0][0]\n    # Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if i < 0 or j < 0:\n        return inf\n    # Вычислить минимальную стоимость пути из левого верхнего угла до (i-1, j) и (i, j-1)\n    up = min_path_sum_dfs(grid, i - 1, j)\n    left = min_path_sum_dfs(grid, i, j - 1)\n    # Вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    return min(left, up) + grid[i][j]\n
        min_path_sum.cpp
        /* Минимальная сумма пути: полный перебор */\nint minPathSumDFS(vector<vector<int>> &grid, int i, int j) {\n    // Если это верхняя левая ячейка, завершить поиск\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if (i < 0 || j < 0) {\n        return INT_MAX;\n    }\n    // Вычислить минимальную стоимость пути из левого верхнего угла до (i-1, j) и (i, j-1)\n    int up = minPathSumDFS(grid, i - 1, j);\n    int left = minPathSumDFS(grid, i, j - 1);\n    // Вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    return min(left, up) != INT_MAX ? min(left, up) + grid[i][j] : INT_MAX;\n}\n
        min_path_sum.java
        /* Минимальная сумма пути: полный перебор */\nint minPathSumDFS(int[][] grid, int i, int j) {\n    // Если это верхняя левая ячейка, завершить поиск\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if (i < 0 || j < 0) {\n        return Integer.MAX_VALUE;\n    }\n    // Вычислить минимальную стоимость пути из левого верхнего угла до (i-1, j) и (i, j-1)\n    int up = minPathSumDFS(grid, i - 1, j);\n    int left = minPathSumDFS(grid, i, j - 1);\n    // Вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    return Math.min(left, up) + grid[i][j];\n}\n
        min_path_sum.cs
        /* Минимальная сумма пути: полный перебор */\nint MinPathSumDFS(int[][] grid, int i, int j) {\n    // Если это верхняя левая ячейка, завершить поиск\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if (i < 0 || j < 0) {\n        return int.MaxValue;\n    }\n    // Вычислить минимальную стоимость пути из левого верхнего угла до (i-1, j) и (i, j-1)\n    int up = MinPathSumDFS(grid, i - 1, j);\n    int left = MinPathSumDFS(grid, i, j - 1);\n    // Вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    return Math.Min(left, up) + grid[i][j];\n}\n
        min_path_sum.go
        /* Минимальная сумма пути: полный перебор */\nfunc minPathSumDFS(grid [][]int, i, j int) int {\n    // Если это верхняя левая ячейка, завершить поиск\n    if i == 0 && j == 0 {\n        return grid[0][0]\n    }\n    // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if i < 0 || j < 0 {\n        return math.MaxInt\n    }\n    // Вычислить минимальную стоимость пути из левого верхнего угла до (i-1, j) и (i, j-1)\n    up := minPathSumDFS(grid, i-1, j)\n    left := minPathSumDFS(grid, i, j-1)\n    // Вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    return int(math.Min(float64(left), float64(up))) + grid[i][j]\n}\n
        min_path_sum.swift
        /* Минимальная сумма пути: полный перебор */\nfunc minPathSumDFS(grid: [[Int]], i: Int, j: Int) -> Int {\n    // Если это верхняя левая ячейка, завершить поиск\n    if i == 0, j == 0 {\n        return grid[0][0]\n    }\n    // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if i < 0 || j < 0 {\n        return .max\n    }\n    // Вычислить минимальную стоимость пути из левого верхнего угла до (i-1, j) и (i, j-1)\n    let up = minPathSumDFS(grid: grid, i: i - 1, j: j)\n    let left = minPathSumDFS(grid: grid, i: i, j: j - 1)\n    // Вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    return min(left, up) + grid[i][j]\n}\n
        min_path_sum.js
        /* Минимальная сумма пути: полный перебор */\nfunction minPathSumDFS(grid, i, j) {\n    // Если это верхняя левая ячейка, завершить поиск\n    if (i === 0 && j === 0) {\n        return grid[0][0];\n    }\n    // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if (i < 0 || j < 0) {\n        return Infinity;\n    }\n    // Вычислить минимальную стоимость пути из левого верхнего угла до (i-1, j) и (i, j-1)\n    const up = minPathSumDFS(grid, i - 1, j);\n    const left = minPathSumDFS(grid, i, j - 1);\n    // Вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    return Math.min(left, up) + grid[i][j];\n}\n
        min_path_sum.ts
        /* Минимальная сумма пути: полный перебор */\nfunction minPathSumDFS(\n    grid: Array<Array<number>>,\n    i: number,\n    j: number\n): number {\n    // Если это верхняя левая ячейка, завершить поиск\n    if (i === 0 && j == 0) {\n        return grid[0][0];\n    }\n    // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if (i < 0 || j < 0) {\n        return Infinity;\n    }\n    // Вычислить минимальную стоимость пути из левого верхнего угла до (i-1, j) и (i, j-1)\n    const up = minPathSumDFS(grid, i - 1, j);\n    const left = minPathSumDFS(grid, i, j - 1);\n    // Вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    return Math.min(left, up) + grid[i][j];\n}\n
        min_path_sum.dart
        /* Минимальная сумма пути: полный перебор */\nint minPathSumDFS(List<List<int>> grid, int i, int j) {\n  // Если это верхняя левая ячейка, завершить поиск\n  if (i == 0 && j == 0) {\n    return grid[0][0];\n  }\n  // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n  if (i < 0 || j < 0) {\n    // В Dart тип int — целое число фиксированного диапазона; значения, представляющего «бесконечность», не существует\n    return BigInt.from(2).pow(31).toInt();\n  }\n  // Вычислить минимальную стоимость пути из левого верхнего угла до (i-1, j) и (i, j-1)\n  int up = minPathSumDFS(grid, i - 1, j);\n  int left = minPathSumDFS(grid, i, j - 1);\n  // Вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n  return min(left, up) + grid[i][j];\n}\n
        min_path_sum.rs
        /* Минимальная сумма пути: полный перебор */\nfn min_path_sum_dfs(grid: &Vec<Vec<i32>>, i: i32, j: i32) -> i32 {\n    // Если это верхняя левая ячейка, завершить поиск\n    if i == 0 && j == 0 {\n        return grid[0][0];\n    }\n    // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if i < 0 || j < 0 {\n        return i32::MAX;\n    }\n    // Вычислить минимальную стоимость пути из левого верхнего угла до (i-1, j) и (i, j-1)\n    let up = min_path_sum_dfs(grid, i - 1, j);\n    let left = min_path_sum_dfs(grid, i, j - 1);\n    // Вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    std::cmp::min(left, up) + grid[i as usize][j as usize]\n}\n
        min_path_sum.c
        /* Минимальная сумма пути: полный перебор */\nint minPathSumDFS(int grid[MAX_SIZE][MAX_SIZE], int i, int j) {\n    // Если это верхняя левая ячейка, завершить поиск\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if (i < 0 || j < 0) {\n        return INT_MAX;\n    }\n    // Вычислить минимальную стоимость пути из левого верхнего угла до (i-1, j) и (i, j-1)\n    int up = minPathSumDFS(grid, i - 1, j);\n    int left = minPathSumDFS(grid, i, j - 1);\n    // Вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    return myMin(left, up) != INT_MAX ? myMin(left, up) + grid[i][j] : INT_MAX;\n}\n
        min_path_sum.kt
        /* Минимальная сумма пути: полный перебор */\nfun minPathSumDFS(grid: Array<IntArray>, i: Int, j: Int): Int {\n    // Если это верхняя левая ячейка, завершить поиск\n    if (i == 0 && j == 0) {\n        return grid[0][0]\n    }\n    // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if (i < 0 || j < 0) {\n        return Int.MAX_VALUE\n    }\n    // Вычислить минимальную стоимость пути из левого верхнего угла до (i-1, j) и (i, j-1)\n    val up = minPathSumDFS(grid, i - 1, j)\n    val left = minPathSumDFS(grid, i, j - 1)\n    // Вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    return min(left, up) + grid[i][j]\n}\n
        min_path_sum.rb
        ### Минимальная сумма пути: полный перебор ###\ndef min_path_sum_dfs(grid, i, j)\n  # Если это верхняя левая ячейка, завершить поиск\n  return grid[i][j] if i == 0 && j == 0\n  # Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n  return Float::INFINITY if i < 0 || j < 0\n  # Вычислить минимальную стоимость пути из левого верхнего угла до (i-1, j) и (i, j-1)\n  up = min_path_sum_dfs(grid, i - 1, j)\n  left = min_path_sum_dfs(grid, i, j - 1)\n  # Вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n  [left, up].min + grid[i][j]\nend\n
        Визуализация кода

        Во весь экран >

        На рисунке 14-14 показано дерево рекурсии с корнем в \\(dp[2, 1]\\). В нем содержатся перекрывающиеся подзадачи, и их число будет резко расти вместе с размером сетки grid .

        По своей сути причина появления перекрывающихся подзадач такова: существует много разных путей от левого верхнего угла до одной и той же клетки.

        Рисунок 14-14   Дерево рекурсии полного перебора

        У каждого состояния есть два выбора - вниз и вправо, а от левого верхнего угла до правого нижнего нужно сделать всего \\(m + n - 2\\) шагов, поэтому худшая временная сложность равна \\(O(2^{m + n})\\) , где \\(n\\) и \\(m\\) - число строк и столбцов сетки соответственно. Заметим, что в этой оценке не учитывается близость к границам сетки: у граничных клеток остается только один выбор, так что фактическое число путей будет несколько меньше.

        ","path":["Глава 14. Динамическое программирование","14.3   Подход к решению задач динамического программирования"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#2-2","level":3,"title":"2.   Метод 2: поиск с мемоизацией","text":"

        Введем список памяти mem того же размера, что и сетка grid , для хранения решений всех подзадач и отсечения перекрывающихся подзадач:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py
        def min_path_sum_dfs_mem(\n    grid: list[list[int]], mem: list[list[int]], i: int, j: int\n) -> int:\n    \"\"\"Минимальная сумма пути: поиск с мемоизацией\"\"\"\n    # Если это верхняя левая ячейка, завершить поиск\n    if i == 0 and j == 0:\n        return grid[0][0]\n    # Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if i < 0 or j < 0:\n        return inf\n    # Если запись уже есть, вернуть сразу\n    if mem[i][j] != -1:\n        return mem[i][j]\n    # Минимальная стоимость пути для левой и верхней ячеек\n    up = min_path_sum_dfs_mem(grid, mem, i - 1, j)\n    left = min_path_sum_dfs_mem(grid, mem, i, j - 1)\n    # Сохранить и вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    mem[i][j] = min(left, up) + grid[i][j]\n    return mem[i][j]\n
        min_path_sum.cpp
        /* Минимальная сумма пути: поиск с мемоизацией */\nint minPathSumDFSMem(vector<vector<int>> &grid, vector<vector<int>> &mem, int i, int j) {\n    // Если это верхняя левая ячейка, завершить поиск\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if (i < 0 || j < 0) {\n        return INT_MAX;\n    }\n    // Если запись уже есть, вернуть сразу\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // Минимальная стоимость пути для левой и верхней ячеек\n    int up = minPathSumDFSMem(grid, mem, i - 1, j);\n    int left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // Сохранить и вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    mem[i][j] = min(left, up) != INT_MAX ? min(left, up) + grid[i][j] : INT_MAX;\n    return mem[i][j];\n}\n
        min_path_sum.java
        /* Минимальная сумма пути: поиск с мемоизацией */\nint minPathSumDFSMem(int[][] grid, int[][] mem, int i, int j) {\n    // Если это верхняя левая ячейка, завершить поиск\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if (i < 0 || j < 0) {\n        return Integer.MAX_VALUE;\n    }\n    // Если запись уже есть, вернуть сразу\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // Минимальная стоимость пути для левой и верхней ячеек\n    int up = minPathSumDFSMem(grid, mem, i - 1, j);\n    int left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // Сохранить и вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    mem[i][j] = Math.min(left, up) + grid[i][j];\n    return mem[i][j];\n}\n
        min_path_sum.cs
        /* Минимальная сумма пути: поиск с мемоизацией */\nint MinPathSumDFSMem(int[][] grid, int[][] mem, int i, int j) {\n    // Если это верхняя левая ячейка, завершить поиск\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if (i < 0 || j < 0) {\n        return int.MaxValue;\n    }\n    // Если запись уже есть, вернуть сразу\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // Минимальная стоимость пути для левой и верхней ячеек\n    int up = MinPathSumDFSMem(grid, mem, i - 1, j);\n    int left = MinPathSumDFSMem(grid, mem, i, j - 1);\n    // Сохранить и вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    mem[i][j] = Math.Min(left, up) + grid[i][j];\n    return mem[i][j];\n}\n
        min_path_sum.go
        /* Минимальная сумма пути: поиск с мемоизацией */\nfunc minPathSumDFSMem(grid, mem [][]int, i, j int) int {\n    // Если это верхняя левая ячейка, завершить поиск\n    if i == 0 && j == 0 {\n        return grid[0][0]\n    }\n    // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if i < 0 || j < 0 {\n        return math.MaxInt\n    }\n    // Если запись уже есть, вернуть сразу\n    if mem[i][j] != -1 {\n        return mem[i][j]\n    }\n    // Минимальная стоимость пути для левой и верхней ячеек\n    up := minPathSumDFSMem(grid, mem, i-1, j)\n    left := minPathSumDFSMem(grid, mem, i, j-1)\n    // Сохранить и вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    mem[i][j] = int(math.Min(float64(left), float64(up))) + grid[i][j]\n    return mem[i][j]\n}\n
        min_path_sum.swift
        /* Минимальная сумма пути: поиск с мемоизацией */\nfunc minPathSumDFSMem(grid: [[Int]], mem: inout [[Int]], i: Int, j: Int) -> Int {\n    // Если это верхняя левая ячейка, завершить поиск\n    if i == 0, j == 0 {\n        return grid[0][0]\n    }\n    // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if i < 0 || j < 0 {\n        return .max\n    }\n    // Если запись уже есть, вернуть сразу\n    if mem[i][j] != -1 {\n        return mem[i][j]\n    }\n    // Минимальная стоимость пути для левой и верхней ячеек\n    let up = minPathSumDFSMem(grid: grid, mem: &mem, i: i - 1, j: j)\n    let left = minPathSumDFSMem(grid: grid, mem: &mem, i: i, j: j - 1)\n    // Сохранить и вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    mem[i][j] = min(left, up) + grid[i][j]\n    return mem[i][j]\n}\n
        min_path_sum.js
        /* Минимальная сумма пути: поиск с мемоизацией */\nfunction minPathSumDFSMem(grid, mem, i, j) {\n    // Если это верхняя левая ячейка, завершить поиск\n    if (i === 0 && j === 0) {\n        return grid[0][0];\n    }\n    // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if (i < 0 || j < 0) {\n        return Infinity;\n    }\n    // Если запись уже есть, вернуть сразу\n    if (mem[i][j] !== -1) {\n        return mem[i][j];\n    }\n    // Минимальная стоимость пути для левой и верхней ячеек\n    const up = minPathSumDFSMem(grid, mem, i - 1, j);\n    const left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // Сохранить и вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    mem[i][j] = Math.min(left, up) + grid[i][j];\n    return mem[i][j];\n}\n
        min_path_sum.ts
        /* Минимальная сумма пути: поиск с мемоизацией */\nfunction minPathSumDFSMem(\n    grid: Array<Array<number>>,\n    mem: Array<Array<number>>,\n    i: number,\n    j: number\n): number {\n    // Если это верхняя левая ячейка, завершить поиск\n    if (i === 0 && j === 0) {\n        return grid[0][0];\n    }\n    // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if (i < 0 || j < 0) {\n        return Infinity;\n    }\n    // Если запись уже есть, вернуть сразу\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // Минимальная стоимость пути для левой и верхней ячеек\n    const up = minPathSumDFSMem(grid, mem, i - 1, j);\n    const left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // Сохранить и вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    mem[i][j] = Math.min(left, up) + grid[i][j];\n    return mem[i][j];\n}\n
        min_path_sum.dart
        /* Минимальная сумма пути: поиск с мемоизацией */\nint minPathSumDFSMem(List<List<int>> grid, List<List<int>> mem, int i, int j) {\n  // Если это верхняя левая ячейка, завершить поиск\n  if (i == 0 && j == 0) {\n    return grid[0][0];\n  }\n  // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n  if (i < 0 || j < 0) {\n    // В Dart тип int — целое число фиксированного диапазона; значения, представляющего «бесконечность», не существует\n    return BigInt.from(2).pow(31).toInt();\n  }\n  // Если запись уже есть, вернуть сразу\n  if (mem[i][j] != -1) {\n    return mem[i][j];\n  }\n  // Минимальная стоимость пути для левой и верхней ячеек\n  int up = minPathSumDFSMem(grid, mem, i - 1, j);\n  int left = minPathSumDFSMem(grid, mem, i, j - 1);\n  // Сохранить и вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n  mem[i][j] = min(left, up) + grid[i][j];\n  return mem[i][j];\n}\n
        min_path_sum.rs
        /* Минимальная сумма пути: поиск с мемоизацией */\nfn min_path_sum_dfs_mem(grid: &Vec<Vec<i32>>, mem: &mut Vec<Vec<i32>>, i: i32, j: i32) -> i32 {\n    // Если это верхняя левая ячейка, завершить поиск\n    if i == 0 && j == 0 {\n        return grid[0][0];\n    }\n    // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if i < 0 || j < 0 {\n        return i32::MAX;\n    }\n    // Если запись уже есть, вернуть сразу\n    if mem[i as usize][j as usize] != -1 {\n        return mem[i as usize][j as usize];\n    }\n    // Минимальная стоимость пути для левой и верхней ячеек\n    let up = min_path_sum_dfs_mem(grid, mem, i - 1, j);\n    let left = min_path_sum_dfs_mem(grid, mem, i, j - 1);\n    // Сохранить и вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    mem[i as usize][j as usize] = std::cmp::min(left, up) + grid[i as usize][j as usize];\n    mem[i as usize][j as usize]\n}\n
        min_path_sum.c
        /* Минимальная сумма пути: поиск с мемоизацией */\nint minPathSumDFSMem(int grid[MAX_SIZE][MAX_SIZE], int mem[MAX_SIZE][MAX_SIZE], int i, int j) {\n    // Если это верхняя левая ячейка, завершить поиск\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if (i < 0 || j < 0) {\n        return INT_MAX;\n    }\n    // Если запись уже есть, вернуть сразу\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // Минимальная стоимость пути для левой и верхней ячеек\n    int up = minPathSumDFSMem(grid, mem, i - 1, j);\n    int left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // Сохранить и вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    mem[i][j] = myMin(left, up) != INT_MAX ? myMin(left, up) + grid[i][j] : INT_MAX;\n    return mem[i][j];\n}\n
        min_path_sum.kt
        /* Минимальная сумма пути: поиск с мемоизацией */\nfun minPathSumDFSMem(\n    grid: Array<IntArray>,\n    mem: Array<IntArray>,\n    i: Int,\n    j: Int\n): Int {\n    // Если это верхняя левая ячейка, завершить поиск\n    if (i == 0 && j == 0) {\n        return grid[0][0]\n    }\n    // Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n    if (i < 0 || j < 0) {\n        return Int.MAX_VALUE\n    }\n    // Если запись уже есть, вернуть сразу\n    if (mem[i][j] != -1) {\n        return mem[i][j]\n    }\n    // Минимальная стоимость пути для левой и верхней ячеек\n    val up = minPathSumDFSMem(grid, mem, i - 1, j)\n    val left = minPathSumDFSMem(grid, mem, i, j - 1)\n    // Сохранить и вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n    mem[i][j] = min(left, up) + grid[i][j]\n    return mem[i][j]\n}\n
        min_path_sum.rb
        ### Минимальная сумма пути: поиск с мемоизацией ###\ndef min_path_sum_dfs_mem(grid, mem, i, j)\n  # Если это верхняя левая ячейка, завершить поиск\n  return grid[0][0] if i == 0 && j == 0\n  # Если индексы строки или столбца выходят за границы, вернуть стоимость +∞\n  return Float::INFINITY if i < 0 || j < 0\n  # Если запись уже есть, вернуть сразу\n  return mem[i][j] if mem[i][j] != -1\n  # Минимальная стоимость пути для левой и верхней ячеек\n  up = min_path_sum_dfs_mem(grid, mem, i - 1, j)\n  left = min_path_sum_dfs_mem(grid, mem, i, j - 1)\n  # Сохранить и вернуть минимальную стоимость пути из левого верхнего угла до (i, j)\n  mem[i][j] = [left, up].min + grid[i][j]\nend\n
        Визуализация кода

        Во весь экран >

        Как показано на рисунке 14-15, после добавления мемоизации решение каждой подзадачи вычисляется только один раз, поэтому временная сложность определяется общим числом состояний, то есть равна \\(O(nm)\\) .

        Рисунок 14-15   Дерево рекурсии для поиска с мемоизацией

        ","path":["Глава 14. Динамическое программирование","14.3   Подход к решению задач динамического программирования"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#3-3","level":3,"title":"3.   Метод 3: динамическое программирование","text":"

        Итеративная реализация динамического программирования выглядит так:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py
        def min_path_sum_dp(grid: list[list[int]]) -> int:\n    \"\"\"Минимальная сумма пути: динамическое программирование\"\"\"\n    n, m = len(grid), len(grid[0])\n    # Инициализация таблицы dp\n    dp = [[0] * m for _ in range(n)]\n    dp[0][0] = grid[0][0]\n    # Переход состояний: первая строка\n    for j in range(1, m):\n        dp[0][j] = dp[0][j - 1] + grid[0][j]\n    # Переход состояний: первый столбец\n    for i in range(1, n):\n        dp[i][0] = dp[i - 1][0] + grid[i][0]\n    # Переход состояний: остальные строки и столбцы\n    for i in range(1, n):\n        for j in range(1, m):\n            dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]\n    return dp[n - 1][m - 1]\n
        min_path_sum.cpp
        /* Минимальная сумма пути: динамическое программирование */\nint minPathSumDP(vector<vector<int>> &grid) {\n    int n = grid.size(), m = grid[0].size();\n    // Инициализация таблицы dp\n    vector<vector<int>> dp(n, vector<int>(m));\n    dp[0][0] = grid[0][0];\n    // Переход состояний: первая строка\n    for (int j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // Переход состояний: первый столбец\n    for (int i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (int i = 1; i < n; i++) {\n        for (int j = 1; j < m; j++) {\n            dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    return dp[n - 1][m - 1];\n}\n
        min_path_sum.java
        /* Минимальная сумма пути: динамическое программирование */\nint minPathSumDP(int[][] grid) {\n    int n = grid.length, m = grid[0].length;\n    // Инициализация таблицы dp\n    int[][] dp = new int[n][m];\n    dp[0][0] = grid[0][0];\n    // Переход состояний: первая строка\n    for (int j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // Переход состояний: первый столбец\n    for (int i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (int i = 1; i < n; i++) {\n        for (int j = 1; j < m; j++) {\n            dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    return dp[n - 1][m - 1];\n}\n
        min_path_sum.cs
        /* Минимальная сумма пути: динамическое программирование */\nint MinPathSumDP(int[][] grid) {\n    int n = grid.Length, m = grid[0].Length;\n    // Инициализация таблицы dp\n    int[,] dp = new int[n, m];\n    dp[0, 0] = grid[0][0];\n    // Переход состояний: первая строка\n    for (int j = 1; j < m; j++) {\n        dp[0, j] = dp[0, j - 1] + grid[0][j];\n    }\n    // Переход состояний: первый столбец\n    for (int i = 1; i < n; i++) {\n        dp[i, 0] = dp[i - 1, 0] + grid[i][0];\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (int i = 1; i < n; i++) {\n        for (int j = 1; j < m; j++) {\n            dp[i, j] = Math.Min(dp[i, j - 1], dp[i - 1, j]) + grid[i][j];\n        }\n    }\n    return dp[n - 1, m - 1];\n}\n
        min_path_sum.go
        /* Минимальная сумма пути: динамическое программирование */\nfunc minPathSumDP(grid [][]int) int {\n    n, m := len(grid), len(grid[0])\n    // Инициализация таблицы dp\n    dp := make([][]int, n)\n    for i := 0; i < n; i++ {\n        dp[i] = make([]int, m)\n    }\n    dp[0][0] = grid[0][0]\n    // Переход состояний: первая строка\n    for j := 1; j < m; j++ {\n        dp[0][j] = dp[0][j-1] + grid[0][j]\n    }\n    // Переход состояний: первый столбец\n    for i := 1; i < n; i++ {\n        dp[i][0] = dp[i-1][0] + grid[i][0]\n    }\n    // Переход состояний: остальные строки и столбцы\n    for i := 1; i < n; i++ {\n        for j := 1; j < m; j++ {\n            dp[i][j] = int(math.Min(float64(dp[i][j-1]), float64(dp[i-1][j]))) + grid[i][j]\n        }\n    }\n    return dp[n-1][m-1]\n}\n
        min_path_sum.swift
        /* Минимальная сумма пути: динамическое программирование */\nfunc minPathSumDP(grid: [[Int]]) -> Int {\n    let n = grid.count\n    let m = grid[0].count\n    // Инициализация таблицы dp\n    var dp = Array(repeating: Array(repeating: 0, count: m), count: n)\n    dp[0][0] = grid[0][0]\n    // Переход состояний: первая строка\n    for j in 1 ..< m {\n        dp[0][j] = dp[0][j - 1] + grid[0][j]\n    }\n    // Переход состояний: первый столбец\n    for i in 1 ..< n {\n        dp[i][0] = dp[i - 1][0] + grid[i][0]\n    }\n    // Переход состояний: остальные строки и столбцы\n    for i in 1 ..< n {\n        for j in 1 ..< m {\n            dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]\n        }\n    }\n    return dp[n - 1][m - 1]\n}\n
        min_path_sum.js
        /* Минимальная сумма пути: динамическое программирование */\nfunction minPathSumDP(grid) {\n    const n = grid.length,\n        m = grid[0].length;\n    // Инициализация таблицы dp\n    const dp = Array.from({ length: n }, () =>\n        Array.from({ length: m }, () => 0)\n    );\n    dp[0][0] = grid[0][0];\n    // Переход состояний: первая строка\n    for (let j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // Переход состояний: первый столбец\n    for (let i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (let i = 1; i < n; i++) {\n        for (let j = 1; j < m; j++) {\n            dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    return dp[n - 1][m - 1];\n}\n
        min_path_sum.ts
        /* Минимальная сумма пути: динамическое программирование */\nfunction minPathSumDP(grid: Array<Array<number>>): number {\n    const n = grid.length,\n        m = grid[0].length;\n    // Инициализация таблицы dp\n    const dp = Array.from({ length: n }, () =>\n        Array.from({ length: m }, () => 0)\n    );\n    dp[0][0] = grid[0][0];\n    // Переход состояний: первая строка\n    for (let j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // Переход состояний: первый столбец\n    for (let i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (let i = 1; i < n; i++) {\n        for (let j: number = 1; j < m; j++) {\n            dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    return dp[n - 1][m - 1];\n}\n
        min_path_sum.dart
        /* Минимальная сумма пути: динамическое программирование */\nint minPathSumDP(List<List<int>> grid) {\n  int n = grid.length, m = grid[0].length;\n  // Инициализация таблицы dp\n  List<List<int>> dp = List.generate(n, (i) => List.filled(m, 0));\n  dp[0][0] = grid[0][0];\n  // Переход состояний: первая строка\n  for (int j = 1; j < m; j++) {\n    dp[0][j] = dp[0][j - 1] + grid[0][j];\n  }\n  // Переход состояний: первый столбец\n  for (int i = 1; i < n; i++) {\n    dp[i][0] = dp[i - 1][0] + grid[i][0];\n  }\n  // Переход состояний: остальные строки и столбцы\n  for (int i = 1; i < n; i++) {\n    for (int j = 1; j < m; j++) {\n      dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n    }\n  }\n  return dp[n - 1][m - 1];\n}\n
        min_path_sum.rs
        /* Минимальная сумма пути: динамическое программирование */\nfn min_path_sum_dp(grid: &Vec<Vec<i32>>) -> i32 {\n    let (n, m) = (grid.len(), grid[0].len());\n    // Инициализация таблицы dp\n    let mut dp = vec![vec![0; m]; n];\n    dp[0][0] = grid[0][0];\n    // Переход состояний: первая строка\n    for j in 1..m {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // Переход состояний: первый столбец\n    for i in 1..n {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // Переход состояний: остальные строки и столбцы\n    for i in 1..n {\n        for j in 1..m {\n            dp[i][j] = std::cmp::min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    dp[n - 1][m - 1]\n}\n
        min_path_sum.c
        /* Минимальная сумма пути: динамическое программирование */\nint minPathSumDP(int grid[MAX_SIZE][MAX_SIZE], int n, int m) {\n    // Инициализация таблицы dp\n    int **dp = malloc(n * sizeof(int *));\n    for (int i = 0; i < n; i++) {\n        dp[i] = calloc(m, sizeof(int));\n    }\n    dp[0][0] = grid[0][0];\n    // Переход состояний: первая строка\n    for (int j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // Переход состояний: первый столбец\n    for (int i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (int i = 1; i < n; i++) {\n        for (int j = 1; j < m; j++) {\n            dp[i][j] = myMin(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    int res = dp[n - 1][m - 1];\n    // Освободить память\n    for (int i = 0; i < n; i++) {\n        free(dp[i]);\n    }\n    return res;\n}\n
        min_path_sum.kt
        /* Минимальная сумма пути: динамическое программирование */\nfun minPathSumDP(grid: Array<IntArray>): Int {\n    val n = grid.size\n    val m = grid[0].size\n    // Инициализация таблицы dp\n    val dp = Array(n) { IntArray(m) }\n    dp[0][0] = grid[0][0]\n    // Переход состояний: первая строка\n    for (j in 1..<m) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j]\n    }\n    // Переход состояний: первый столбец\n    for (i in 1..<n) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0]\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (i in 1..<n) {\n        for (j in 1..<m) {\n            dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]\n        }\n    }\n    return dp[n - 1][m - 1]\n}\n
        min_path_sum.rb
        ### Минимальная сумма пути: динамическое программирование ###\ndef min_path_sum_dp(grid)\n  n, m = grid.length, grid.first.length\n  # Инициализация таблицы dp\n  dp = Array.new(n) { Array.new(m, 0) }\n  dp[0][0] = grid[0][0]\n  # Переход состояний: первая строка\n  (1...m).each { |j| dp[0][j] = dp[0][j - 1] + grid[0][j] }\n  # Переход состояний: первый столбец\n  (1...n).each { |i| dp[i][0] = dp[i - 1][0] + grid[i][0] }\n  # Переход состояний: остальные строки и столбцы\n  for i in 1...n\n    for j in 1...m\n      dp[i][j] = [dp[i][j - 1], dp[i - 1][j]].min + grid[i][j]\n    end\n  end\n  dp[n -1][m -1]\nend\n
        Визуализация кода

        Во весь экран >

        На рисунке 14-16 показан процесс переходов состояний в задаче о минимальной сумме пути. Он проходит по всей сетке, поэтому временная сложность равна \\(O(nm)\\) .

        Размер массива dp равен \\(n \\times m\\) , поэтому пространственная сложность равна \\(O(nm)\\) .

        <1><2><3><4><5><6><7><8><9><10><11><12>

        Рисунок 14-16   Процесс динамического программирования для минимальной суммы пути

        ","path":["Глава 14. Динамическое программирование","14.3   Подход к решению задач динамического программирования"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#4","level":3,"title":"4.   Оптимизация пространства","text":"

        Поскольку каждая клетка зависит только от клетки слева и клетки сверху, таблицу \\(dp\\) можно реализовать с помощью одномерного массива, соответствующего одной строке.

        Обратите внимание: поскольку массив dp теперь может представлять только одну строку состояний, мы не можем заранее инициализировать состояния первого столбца, а должны обновлять их по мере обхода каждой строки:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py
        def min_path_sum_dp_comp(grid: list[list[int]]) -> int:\n    \"\"\"Минимальная сумма пути: динамическое программирование с оптимизацией памяти\"\"\"\n    n, m = len(grid), len(grid[0])\n    # Инициализация таблицы dp\n    dp = [0] * m\n    # Переход состояний: первая строка\n    dp[0] = grid[0][0]\n    for j in range(1, m):\n        dp[j] = dp[j - 1] + grid[0][j]\n    # Переход состояний: остальные строки\n    for i in range(1, n):\n        # Переход состояний: первый столбец\n        dp[0] = dp[0] + grid[i][0]\n        # Переход состояний: остальные столбцы\n        for j in range(1, m):\n            dp[j] = min(dp[j - 1], dp[j]) + grid[i][j]\n    return dp[m - 1]\n
        min_path_sum.cpp
        /* Минимальная сумма пути: динамическое программирование с оптимизацией памяти */\nint minPathSumDPComp(vector<vector<int>> &grid) {\n    int n = grid.size(), m = grid[0].size();\n    // Инициализация таблицы dp\n    vector<int> dp(m);\n    // Переход состояний: первая строка\n    dp[0] = grid[0][0];\n    for (int j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // Переход состояний: остальные строки\n    for (int i = 1; i < n; i++) {\n        // Переход состояний: первый столбец\n        dp[0] = dp[0] + grid[i][0];\n        // Переход состояний: остальные столбцы\n        for (int j = 1; j < m; j++) {\n            dp[j] = min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    return dp[m - 1];\n}\n
        min_path_sum.java
        /* Минимальная сумма пути: динамическое программирование с оптимизацией памяти */\nint minPathSumDPComp(int[][] grid) {\n    int n = grid.length, m = grid[0].length;\n    // Инициализация таблицы dp\n    int[] dp = new int[m];\n    // Переход состояний: первая строка\n    dp[0] = grid[0][0];\n    for (int j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // Переход состояний: остальные строки\n    for (int i = 1; i < n; i++) {\n        // Переход состояний: первый столбец\n        dp[0] = dp[0] + grid[i][0];\n        // Переход состояний: остальные столбцы\n        for (int j = 1; j < m; j++) {\n            dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    return dp[m - 1];\n}\n
        min_path_sum.cs
        /* Минимальная сумма пути: динамическое программирование с оптимизацией памяти */\nint MinPathSumDPComp(int[][] grid) {\n    int n = grid.Length, m = grid[0].Length;\n    // Инициализация таблицы dp\n    int[] dp = new int[m];\n    dp[0] = grid[0][0];\n    // Переход состояний: первая строка\n    for (int j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // Переход состояний: остальные строки\n    for (int i = 1; i < n; i++) {\n        // Переход состояний: первый столбец\n        dp[0] = dp[0] + grid[i][0];\n        // Переход состояний: остальные столбцы\n        for (int j = 1; j < m; j++) {\n            dp[j] = Math.Min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    return dp[m - 1];\n}\n
        min_path_sum.go
        /* Минимальная сумма пути: динамическое программирование с оптимизацией памяти */\nfunc minPathSumDPComp(grid [][]int) int {\n    n, m := len(grid), len(grid[0])\n    // Инициализация таблицы dp\n    dp := make([]int, m)\n    // Переход состояний: первая строка\n    dp[0] = grid[0][0]\n    for j := 1; j < m; j++ {\n        dp[j] = dp[j-1] + grid[0][j]\n    }\n    // Переход состояний: остальные строки и столбцы\n    for i := 1; i < n; i++ {\n        // Переход состояний: первый столбец\n        dp[0] = dp[0] + grid[i][0]\n        // Переход состояний: остальные столбцы\n        for j := 1; j < m; j++ {\n            dp[j] = int(math.Min(float64(dp[j-1]), float64(dp[j]))) + grid[i][j]\n        }\n    }\n    return dp[m-1]\n}\n
        min_path_sum.swift
        /* Минимальная сумма пути: динамическое программирование с оптимизацией памяти */\nfunc minPathSumDPComp(grid: [[Int]]) -> Int {\n    let n = grid.count\n    let m = grid[0].count\n    // Инициализация таблицы dp\n    var dp = Array(repeating: 0, count: m)\n    // Переход состояний: первая строка\n    dp[0] = grid[0][0]\n    for j in 1 ..< m {\n        dp[j] = dp[j - 1] + grid[0][j]\n    }\n    // Переход состояний: остальные строки\n    for i in 1 ..< n {\n        // Переход состояний: первый столбец\n        dp[0] = dp[0] + grid[i][0]\n        // Переход состояний: остальные столбцы\n        for j in 1 ..< m {\n            dp[j] = min(dp[j - 1], dp[j]) + grid[i][j]\n        }\n    }\n    return dp[m - 1]\n}\n
        min_path_sum.js
        /* Минимальная сумма пути: динамическое программирование с оптимизацией памяти */\nfunction minPathSumDPComp(grid) {\n    const n = grid.length,\n        m = grid[0].length;\n    // Инициализация таблицы dp\n    const dp = new Array(m);\n    // Переход состояний: первая строка\n    dp[0] = grid[0][0];\n    for (let j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // Переход состояний: остальные строки\n    for (let i = 1; i < n; i++) {\n        // Переход состояний: первый столбец\n        dp[0] = dp[0] + grid[i][0];\n        // Переход состояний: остальные столбцы\n        for (let j = 1; j < m; j++) {\n            dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    return dp[m - 1];\n}\n
        min_path_sum.ts
        /* Минимальная сумма пути: динамическое программирование с оптимизацией памяти */\nfunction minPathSumDPComp(grid: Array<Array<number>>): number {\n    const n = grid.length,\n        m = grid[0].length;\n    // Инициализация таблицы dp\n    const dp = new Array(m);\n    // Переход состояний: первая строка\n    dp[0] = grid[0][0];\n    for (let j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // Переход состояний: остальные строки\n    for (let i = 1; i < n; i++) {\n        // Переход состояний: первый столбец\n        dp[0] = dp[0] + grid[i][0];\n        // Переход состояний: остальные столбцы\n        for (let j = 1; j < m; j++) {\n            dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    return dp[m - 1];\n}\n
        min_path_sum.dart
        /* Минимальная сумма пути: динамическое программирование с оптимизацией памяти */\nint minPathSumDPComp(List<List<int>> grid) {\n  int n = grid.length, m = grid[0].length;\n  // Инициализация таблицы dp\n  List<int> dp = List.filled(m, 0);\n  dp[0] = grid[0][0];\n  for (int j = 1; j < m; j++) {\n    dp[j] = dp[j - 1] + grid[0][j];\n  }\n  // Переход состояний: остальные строки\n  for (int i = 1; i < n; i++) {\n    // Переход состояний: первый столбец\n    dp[0] = dp[0] + grid[i][0];\n    // Переход состояний: остальные столбцы\n    for (int j = 1; j < m; j++) {\n      dp[j] = min(dp[j - 1], dp[j]) + grid[i][j];\n    }\n  }\n  return dp[m - 1];\n}\n
        min_path_sum.rs
        /* Минимальная сумма пути: динамическое программирование с оптимизацией памяти */\nfn min_path_sum_dp_comp(grid: &Vec<Vec<i32>>) -> i32 {\n    let (n, m) = (grid.len(), grid[0].len());\n    // Инициализация таблицы dp\n    let mut dp = vec![0; m];\n    // Переход состояний: первая строка\n    dp[0] = grid[0][0];\n    for j in 1..m {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // Переход состояний: остальные строки\n    for i in 1..n {\n        // Переход состояний: первый столбец\n        dp[0] = dp[0] + grid[i][0];\n        // Переход состояний: остальные столбцы\n        for j in 1..m {\n            dp[j] = std::cmp::min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    dp[m - 1]\n}\n
        min_path_sum.c
        /* Минимальная сумма пути: динамическое программирование с оптимизацией памяти */\nint minPathSumDPComp(int grid[MAX_SIZE][MAX_SIZE], int n, int m) {\n    // Инициализация таблицы dp\n    int *dp = calloc(m, sizeof(int));\n    // Переход состояний: первая строка\n    dp[0] = grid[0][0];\n    for (int j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // Переход состояний: остальные строки\n    for (int i = 1; i < n; i++) {\n        // Переход состояний: первый столбец\n        dp[0] = dp[0] + grid[i][0];\n        // Переход состояний: остальные столбцы\n        for (int j = 1; j < m; j++) {\n            dp[j] = myMin(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    int res = dp[m - 1];\n    // Освободить память\n    free(dp);\n    return res;\n}\n
        min_path_sum.kt
        /* Минимальная сумма пути: динамическое программирование с оптимизацией памяти */\nfun minPathSumDPComp(grid: Array<IntArray>): Int {\n    val n = grid.size\n    val m = grid[0].size\n    // Инициализация таблицы dp\n    val dp = IntArray(m)\n    // Переход состояний: первая строка\n    dp[0] = grid[0][0]\n    for (j in 1..<m) {\n        dp[j] = dp[j - 1] + grid[0][j]\n    }\n    // Переход состояний: остальные строки\n    for (i in 1..<n) {\n        // Переход состояний: первый столбец\n        dp[0] = dp[0] + grid[i][0]\n        // Переход состояний: остальные столбцы\n        for (j in 1..<m) {\n            dp[j] = min(dp[j - 1], dp[j]) + grid[i][j]\n        }\n    }\n    return dp[m - 1]\n}\n
        min_path_sum.rb
        ### Минимальная сумма пути: динамическое программирование с оптимизацией памяти ###\ndef min_path_sum_dp_comp(grid)\n  n, m = grid.length, grid.first.length\n  # Инициализация таблицы dp\n  dp = Array.new(m, 0)\n  # Переход состояний: первая строка\n  dp[0] = grid[0][0]\n  (1...m).each { |j| dp[j] = dp[j - 1] + grid[0][j] }\n  # Переход состояний: остальные строки\n  for i in 1...n\n    # Переход состояний: первый столбец\n    dp[0] = dp[0] + grid[i][0]\n    # Переход состояний: остальные столбцы\n    (1...m).each { |j| dp[j] = [dp[j - 1], dp[j]].min + grid[i][j] }\n  end\n  dp[m - 1]\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 14. Динамическое программирование","14.3   Подход к решению задач динамического программирования"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/","level":1,"title":"14.6   Задача о расстоянии редактирования","text":"

        Расстояние редактирования, также называемое расстоянием Левенштейна, - это минимальное количество изменений, необходимых для преобразования одной строки в другую. Обычно оно используется для измерения сходства двух последовательностей в информационном поиске и обработке естественного языка.

        Question

        Даны две строки \\(s\\) и \\(t\\) . Верните минимальное число шагов редактирования, необходимое для преобразования \\(s\\) в \\(t\\) .

        Для строки допускаются три операции редактирования: вставка одного символа, удаление одного символа и замена одного символа на произвольный другой символ.

        Как показано на рисунке 14-27, для преобразования kitten в sitting требуется 3 шага редактирования: 2 операции замены и 1 операция вставки. Для преобразования hello в algo также требуется 3 шага: 2 замены и 1 удаление.

        Рисунок 14-27   Пример данных для задачи о расстоянии редактирования

        Задачу о расстоянии редактирования можно естественным образом объяснить с помощью модели дерева решений. Строки соответствуют узлам дерева, а один шаг решения, то есть одна операция редактирования, соответствует одному ребру дерева.

        Как показано на рисунке 14-28, если не ограничивать число операций, то каждый узел может порождать множество ребер, и каждое из них соответствует одному из вариантов преобразования. Это означает, что преобразовать hello в algo можно множеством разных путей.

        С точки зрения дерева решений цель этой задачи - найти кратчайший путь между узлом hello и узлом algo .

        Рисунок 14-28   Представление задачи о расстоянии редактирования через дерево решений

        ","path":["Глава 14. Динамическое программирование","14.6   Задача о расстоянии редактирования"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/#1","level":3,"title":"1.   Идея динамического программирования","text":"

        Шаг 1: продумать решения на каждом раунде, определить состояние и тем самым получить таблицу \\(dp\\)

        На каждом раунде решение состоит в выполнении одной операции редактирования над строкой \\(s\\) .

        Нам нужно, чтобы в ходе выполнения операций размер задачи постепенно уменьшался. Только тогда можно строить подзадачи. Пусть длины строк \\(s\\) и \\(t\\) равны соответственно \\(n\\) и \\(m\\). Сначала рассмотрим последние символы этих строк, то есть \\(s[n-1]\\) и \\(t[m-1]\\) .

        • Если \\(s[n-1]\\) и \\(t[m-1]\\) совпадают, их можно просто пропустить и сразу перейти к сравнению \\(s[n-2]\\) и \\(t[m-2]\\) .
        • Если \\(s[n-1]\\) и \\(t[m-1]\\) различны, нужно выполнить над \\(s\\) одну операцию редактирования (вставку, удаление или замену), чтобы последние символы стали одинаковыми, после чего можно перейти к задаче меньшего размера.

        Иначе говоря, каждый шаг решения, то есть операция редактирования над строкой \\(s\\) , меняет те символы, которые еще необходимо сопоставить в строках \\(s\\) и \\(t\\) . Поэтому состояние определяется текущими позициями рассматриваемых символов в \\(s\\) и \\(t\\) , то есть состоянием \\([i, j]\\) .

        Подзадача, соответствующая состоянию \\([i, j]\\) , такова: минимальное число операций редактирования, необходимое для преобразования первых \\(i\\) символов строки \\(s\\) в первые \\(j\\) символов строки \\(t\\).

        Отсюда получается двумерная таблица \\(dp\\) размера \\((i+1) \\times (j+1)\\) .

        Шаг 2: найти оптимальную подструктуру и на ее основе вывести уравнение перехода состояния

        Рассмотрим подзадачу \\(dp[i, j]\\) . Ее последние символы - это \\(s[i-1]\\) и \\(t[j-1]\\) . В зависимости от операции редактирования возможны три случая, показанные на рисунке 14-29.

        1. Вставить после \\(s[i-1]\\) символ \\(t[j-1]\\). Тогда остается подзадача \\(dp[i, j-1]\\) .
        2. Удалить \\(s[i-1]\\). Тогда остается подзадача \\(dp[i-1, j]\\) .
        3. Заменить \\(s[i-1]\\) на \\(t[j-1]\\). Тогда остается подзадача \\(dp[i-1, j-1]\\) .

        Рисунок 14-29   Переходы состояния в задаче о расстоянии редактирования

        Согласно этому анализу оптимальная подструктура такова: минимальное число шагов редактирования для \\(dp[i, j]\\) равно минимуму из трех значений - \\(dp[i, j-1]\\) , \\(dp[i-1, j]\\) и \\(dp[i-1, j-1]\\) - плюс \\(1\\) шаг за текущее редактирование. Значит, уравнение перехода состояния имеет вид:

        \\[ dp[i, j] = \\min(dp[i, j-1], dp[i-1, j], dp[i-1, j-1]) + 1 \\]

        Заметим, что если символы \\(s[i-1]\\) и \\(t[j-1]\\) совпадают, то редактировать текущий символ не нужно. В этом случае уравнение перехода состояния имеет вид:

        \\[ dp[i, j] = dp[i-1, j-1] \\]

        Шаг 3: определить граничные условия и порядок переходов

        Когда обе строки пусты, число операций редактирования равно \\(0\\) , то есть \\(dp[0, 0] = 0\\) . Когда строка \\(s\\) пуста, а строка \\(t\\) непуста, минимальное число операций равно длине строки \\(t\\) , то есть вся первая строка инициализируется как \\(dp[0, j] = j\\) . Когда строка \\(s\\) непуста, а строка \\(t\\) пуста, минимальное число операций равно длине строки \\(s\\) , то есть весь первый столбец инициализируется как \\(dp[i, 0] = i\\) .

        Из уравнения перехода видно, что решение \\(dp[i, j]\\) зависит от значений слева, сверху и слева сверху, поэтому всю таблицу \\(dp\\) можно обходить двумя вложенными циклами в прямом порядке.

        ","path":["Глава 14. Динамическое программирование","14.6   Задача о расстоянии редактирования"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/#2","level":3,"title":"2.   Реализация кода","text":"PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby edit_distance.py
        def edit_distance_dp(s: str, t: str) -> int:\n    \"\"\"Редакционное расстояние: динамическое программирование\"\"\"\n    n, m = len(s), len(t)\n    dp = [[0] * (m + 1) for _ in range(n + 1)]\n    # Переход состояний: первая строка и первый столбец\n    for i in range(1, n + 1):\n        dp[i][0] = i\n    for j in range(1, m + 1):\n        dp[0][j] = j\n    # Переход состояний: остальные строки и столбцы\n    for i in range(1, n + 1):\n        for j in range(1, m + 1):\n            if s[i - 1] == t[j - 1]:\n                # Если два символа равны, сразу пропустить их\n                dp[i][j] = dp[i - 1][j - 1]\n            else:\n                # Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[i][j] = min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1\n    return dp[n][m]\n
        edit_distance.cpp
        /* Редакционное расстояние: динамическое программирование */\nint editDistanceDP(string s, string t) {\n    int n = s.length(), m = t.length();\n    vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));\n    // Переход состояний: первая строка и первый столбец\n    for (int i = 1; i <= n; i++) {\n        dp[i][0] = i;\n    }\n    for (int j = 1; j <= m; j++) {\n        dp[0][j] = j;\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (int i = 1; i <= n; i++) {\n        for (int j = 1; j <= m; j++) {\n            if (s[i - 1] == t[j - 1]) {\n                // Если два символа равны, сразу пропустить их\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    return dp[n][m];\n}\n
        edit_distance.java
        /* Редакционное расстояние: динамическое программирование */\nint editDistanceDP(String s, String t) {\n    int n = s.length(), m = t.length();\n    int[][] dp = new int[n + 1][m + 1];\n    // Переход состояний: первая строка и первый столбец\n    for (int i = 1; i <= n; i++) {\n        dp[i][0] = i;\n    }\n    for (int j = 1; j <= m; j++) {\n        dp[0][j] = j;\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (int i = 1; i <= n; i++) {\n        for (int j = 1; j <= m; j++) {\n            if (s.charAt(i - 1) == t.charAt(j - 1)) {\n                // Если два символа равны, сразу пропустить их\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[i][j] = Math.min(Math.min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    return dp[n][m];\n}\n
        edit_distance.cs
        /* Редакционное расстояние: динамическое программирование */\nint EditDistanceDP(string s, string t) {\n    int n = s.Length, m = t.Length;\n    int[,] dp = new int[n + 1, m + 1];\n    // Переход состояний: первая строка и первый столбец\n    for (int i = 1; i <= n; i++) {\n        dp[i, 0] = i;\n    }\n    for (int j = 1; j <= m; j++) {\n        dp[0, j] = j;\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (int i = 1; i <= n; i++) {\n        for (int j = 1; j <= m; j++) {\n            if (s[i - 1] == t[j - 1]) {\n                // Если два символа равны, сразу пропустить их\n                dp[i, j] = dp[i - 1, j - 1];\n            } else {\n                // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[i, j] = Math.Min(Math.Min(dp[i, j - 1], dp[i - 1, j]), dp[i - 1, j - 1]) + 1;\n            }\n        }\n    }\n    return dp[n, m];\n}\n
        edit_distance.go
        /* Редакционное расстояние: динамическое программирование */\nfunc editDistanceDP(s string, t string) int {\n    n := len(s)\n    m := len(t)\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, m+1)\n    }\n    // Переход состояний: первая строка и первый столбец\n    for i := 1; i <= n; i++ {\n        dp[i][0] = i\n    }\n    for j := 1; j <= m; j++ {\n        dp[0][j] = j\n    }\n    // Переход состояний: остальные строки и столбцы\n    for i := 1; i <= n; i++ {\n        for j := 1; j <= m; j++ {\n            if s[i-1] == t[j-1] {\n                // Если два символа равны, сразу пропустить их\n                dp[i][j] = dp[i-1][j-1]\n            } else {\n                // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[i][j] = MinInt(MinInt(dp[i][j-1], dp[i-1][j]), dp[i-1][j-1]) + 1\n            }\n        }\n    }\n    return dp[n][m]\n}\n
        edit_distance.swift
        /* Редакционное расстояние: динамическое программирование */\nfunc editDistanceDP(s: String, t: String) -> Int {\n    let n = s.utf8CString.count\n    let m = t.utf8CString.count\n    var dp = Array(repeating: Array(repeating: 0, count: m + 1), count: n + 1)\n    // Переход состояний: первая строка и первый столбец\n    for i in 1 ... n {\n        dp[i][0] = i\n    }\n    for j in 1 ... m {\n        dp[0][j] = j\n    }\n    // Переход состояний: остальные строки и столбцы\n    for i in 1 ... n {\n        for j in 1 ... m {\n            if s.utf8CString[i - 1] == t.utf8CString[j - 1] {\n                // Если два символа равны, сразу пропустить их\n                dp[i][j] = dp[i - 1][j - 1]\n            } else {\n                // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1\n            }\n        }\n    }\n    return dp[n][m]\n}\n
        edit_distance.js
        /* Редакционное расстояние: динамическое программирование */\nfunction editDistanceDP(s, t) {\n    const n = s.length,\n        m = t.length;\n    const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));\n    // Переход состояний: первая строка и первый столбец\n    for (let i = 1; i <= n; i++) {\n        dp[i][0] = i;\n    }\n    for (let j = 1; j <= m; j++) {\n        dp[0][j] = j;\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (let i = 1; i <= n; i++) {\n        for (let j = 1; j <= m; j++) {\n            if (s.charAt(i - 1) === t.charAt(j - 1)) {\n                // Если два символа равны, сразу пропустить их\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[i][j] =\n                    Math.min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    return dp[n][m];\n}\n
        edit_distance.ts
        /* Редакционное расстояние: динамическое программирование */\nfunction editDistanceDP(s: string, t: string): number {\n    const n = s.length,\n        m = t.length;\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: m + 1 }, () => 0)\n    );\n    // Переход состояний: первая строка и первый столбец\n    for (let i = 1; i <= n; i++) {\n        dp[i][0] = i;\n    }\n    for (let j = 1; j <= m; j++) {\n        dp[0][j] = j;\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (let i = 1; i <= n; i++) {\n        for (let j = 1; j <= m; j++) {\n            if (s.charAt(i - 1) === t.charAt(j - 1)) {\n                // Если два символа равны, сразу пропустить их\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[i][j] =\n                    Math.min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    return dp[n][m];\n}\n
        edit_distance.dart
        /* Редакционное расстояние: динамическое программирование */\nint editDistanceDP(String s, String t) {\n  int n = s.length, m = t.length;\n  List<List<int>> dp = List.generate(n + 1, (_) => List.filled(m + 1, 0));\n  // Переход состояний: первая строка и первый столбец\n  for (int i = 1; i <= n; i++) {\n    dp[i][0] = i;\n  }\n  for (int j = 1; j <= m; j++) {\n    dp[0][j] = j;\n  }\n  // Переход состояний: остальные строки и столбцы\n  for (int i = 1; i <= n; i++) {\n    for (int j = 1; j <= m; j++) {\n      if (s[i - 1] == t[j - 1]) {\n        // Если два символа равны, сразу пропустить их\n        dp[i][j] = dp[i - 1][j - 1];\n      } else {\n        // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n        dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n      }\n    }\n  }\n  return dp[n][m];\n}\n
        edit_distance.rs
        /* Редакционное расстояние: динамическое программирование */\nfn edit_distance_dp(s: &str, t: &str) -> i32 {\n    let (n, m) = (s.len(), t.len());\n    let mut dp = vec![vec![0; m + 1]; n + 1];\n    // Переход состояний: первая строка и первый столбец\n    for i in 1..=n {\n        dp[i][0] = i as i32;\n    }\n    for j in 1..m {\n        dp[0][j] = j as i32;\n    }\n    // Переход состояний: остальные строки и столбцы\n    for i in 1..=n {\n        for j in 1..=m {\n            if s.chars().nth(i - 1) == t.chars().nth(j - 1) {\n                // Если два символа равны, сразу пропустить их\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[i][j] =\n                    std::cmp::min(std::cmp::min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    dp[n][m]\n}\n
        edit_distance.c
        /* Редакционное расстояние: динамическое программирование */\nint editDistanceDP(char *s, char *t, int n, int m) {\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(m + 1, sizeof(int));\n    }\n    // Переход состояний: первая строка и первый столбец\n    for (int i = 1; i <= n; i++) {\n        dp[i][0] = i;\n    }\n    for (int j = 1; j <= m; j++) {\n        dp[0][j] = j;\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (int i = 1; i <= n; i++) {\n        for (int j = 1; j <= m; j++) {\n            if (s[i - 1] == t[j - 1]) {\n                // Если два символа равны, сразу пропустить их\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[i][j] = myMin(myMin(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    int res = dp[n][m];\n    // Освободить память\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    return res;\n}\n
        edit_distance.kt
        /* Редакционное расстояние: динамическое программирование */\nfun editDistanceDP(s: String, t: String): Int {\n    val n = s.length\n    val m = t.length\n    val dp = Array(n + 1) { IntArray(m + 1) }\n    // Переход состояний: первая строка и первый столбец\n    for (i in 1..n) {\n        dp[i][0] = i\n    }\n    for (j in 1..m) {\n        dp[0][j] = j\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (i in 1..n) {\n        for (j in 1..m) {\n            if (s[i - 1] == t[j - 1]) {\n                // Если два символа равны, сразу пропустить их\n                dp[i][j] = dp[i - 1][j - 1]\n            } else {\n                // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1\n            }\n        }\n    }\n    return dp[n][m]\n}\n
        edit_distance.rb
        ### Редакционное расстояние: динамическое программирование ###\ndef edit_distance_dp(s, t)\n  n, m = s.length, t.length\n  dp = Array.new(n + 1) { Array.new(m + 1, 0) }\n  # Переход состояний: первая строка и первый столбец\n  (1...(n + 1)).each { |i| dp[i][0] = i }\n  (1...(m + 1)).each { |j| dp[0][j] = j }\n  # Переход состояний: остальные строки и столбцы\n  for i in 1...(n + 1)\n    for j in 1...(m +1)\n      if s[i - 1] == t[j - 1]\n        # Если два символа равны, сразу пропустить их\n        dp[i][j] = dp[i - 1][j - 1]\n      else\n        # Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n        dp[i][j] = [dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]].min + 1\n      end\n    end\n  end\n  dp[n][m]\nend\n
        Визуализация кода

        Во весь экран >

        Как показано на рисунке 14-30, процесс переходов состояния в задаче о расстоянии редактирования очень похож на задачи о рюкзаке: и там и здесь его можно рассматривать как заполнение двумерной сетки.

        <1><2><3><4><5><6><7><8><9><10><11><12><13><14><15>

        Рисунок 14-30   Процесс динамического программирования для расстояния редактирования

        ","path":["Глава 14. Динамическое программирование","14.6   Задача о расстоянии редактирования"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/#3","level":3,"title":"3.   Оптимизация пространства","text":"

        Поскольку \\(dp[i,j]\\) зависит от значения сверху \\(dp[i-1, j]\\) , слева \\(dp[i, j-1]\\) и слева сверху \\(dp[i-1, j-1]\\) , прямой обход после оптимизации памяти теряет значение слева сверху, а обратный обход не позволяет заранее построить значение слева \\(dp[i, j-1]\\) . Значит, оба наивных варианта обхода здесь непригодны.

        Чтобы решить эту проблему, можно использовать переменную leftup для временного сохранения значения слева сверху \\(dp[i-1, j-1]\\). После этого остается учитывать только верхнее и левое значения. Тогда ситуация становится аналогичной задаче о полном рюкзаке, и можно выполнять прямой обход. Код приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby edit_distance.py
        def edit_distance_dp_comp(s: str, t: str) -> int:\n    \"\"\"Редакционное расстояние: динамическое программирование с оптимизацией памяти\"\"\"\n    n, m = len(s), len(t)\n    dp = [0] * (m + 1)\n    # Переход состояний: первая строка\n    for j in range(1, m + 1):\n        dp[j] = j\n    # Переход состояний: остальные строки\n    for i in range(1, n + 1):\n        # Переход состояний: первый столбец\n        leftup = dp[0]  # Временно сохранить dp[i-1, j-1]\n        dp[0] += 1\n        # Переход состояний: остальные столбцы\n        for j in range(1, m + 1):\n            temp = dp[j]\n            if s[i - 1] == t[j - 1]:\n                # Если два символа равны, сразу пропустить их\n                dp[j] = leftup\n            else:\n                # Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[j] = min(dp[j - 1], dp[j], leftup) + 1\n            leftup = temp  # Обновить до значения dp[i-1, j-1] для следующей итерации\n    return dp[m]\n
        edit_distance.cpp
        /* Редакционное расстояние: динамическое программирование с оптимизацией памяти */\nint editDistanceDPComp(string s, string t) {\n    int n = s.length(), m = t.length();\n    vector<int> dp(m + 1, 0);\n    // Переход состояний: первая строка\n    for (int j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // Переход состояний: остальные строки\n    for (int i = 1; i <= n; i++) {\n        // Переход состояний: первый столбец\n        int leftup = dp[0]; // Временно сохранить dp[i-1, j-1]\n        dp[0] = i;\n        // Переход состояний: остальные столбцы\n        for (int j = 1; j <= m; j++) {\n            int temp = dp[j];\n            if (s[i - 1] == t[j - 1]) {\n                // Если два символа равны, сразу пропустить их\n                dp[j] = leftup;\n            } else {\n                // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // Обновить до значения dp[i-1, j-1] для следующей итерации\n        }\n    }\n    return dp[m];\n}\n
        edit_distance.java
        /* Редакционное расстояние: динамическое программирование с оптимизацией памяти */\nint editDistanceDPComp(String s, String t) {\n    int n = s.length(), m = t.length();\n    int[] dp = new int[m + 1];\n    // Переход состояний: первая строка\n    for (int j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // Переход состояний: остальные строки\n    for (int i = 1; i <= n; i++) {\n        // Переход состояний: первый столбец\n        int leftup = dp[0]; // Временно сохранить dp[i-1, j-1]\n        dp[0] = i;\n        // Переход состояний: остальные столбцы\n        for (int j = 1; j <= m; j++) {\n            int temp = dp[j];\n            if (s.charAt(i - 1) == t.charAt(j - 1)) {\n                // Если два символа равны, сразу пропустить их\n                dp[j] = leftup;\n            } else {\n                // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[j] = Math.min(Math.min(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // Обновить до значения dp[i-1, j-1] для следующей итерации\n        }\n    }\n    return dp[m];\n}\n
        edit_distance.cs
        /* Редакционное расстояние: динамическое программирование с оптимизацией памяти */\nint EditDistanceDPComp(string s, string t) {\n    int n = s.Length, m = t.Length;\n    int[] dp = new int[m + 1];\n    // Переход состояний: первая строка\n    for (int j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // Переход состояний: остальные строки\n    for (int i = 1; i <= n; i++) {\n        // Переход состояний: первый столбец\n        int leftup = dp[0]; // Временно сохранить dp[i-1, j-1]\n        dp[0] = i;\n        // Переход состояний: остальные столбцы\n        for (int j = 1; j <= m; j++) {\n            int temp = dp[j];\n            if (s[i - 1] == t[j - 1]) {\n                // Если два символа равны, сразу пропустить их\n                dp[j] = leftup;\n            } else {\n                // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[j] = Math.Min(Math.Min(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // Обновить до значения dp[i-1, j-1] для следующей итерации\n        }\n    }\n    return dp[m];\n}\n
        edit_distance.go
        /* Редакционное расстояние: динамическое программирование с оптимизацией памяти */\nfunc editDistanceDPComp(s string, t string) int {\n    n := len(s)\n    m := len(t)\n    dp := make([]int, m+1)\n    // Переход состояний: первая строка\n    for j := 1; j <= m; j++ {\n        dp[j] = j\n    }\n    // Переход состояний: остальные строки\n    for i := 1; i <= n; i++ {\n        // Переход состояний: первый столбец\n        leftUp := dp[0] // Временно сохранить dp[i-1, j-1]\n        dp[0] = i\n        // Переход состояний: остальные столбцы\n        for j := 1; j <= m; j++ {\n            temp := dp[j]\n            if s[i-1] == t[j-1] {\n                // Если два символа равны, сразу пропустить их\n                dp[j] = leftUp\n            } else {\n                // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[j] = MinInt(MinInt(dp[j-1], dp[j]), leftUp) + 1\n            }\n            leftUp = temp // Обновить до значения dp[i-1, j-1] для следующей итерации\n        }\n    }\n    return dp[m]\n}\n
        edit_distance.swift
        /* Редакционное расстояние: динамическое программирование с оптимизацией памяти */\nfunc editDistanceDPComp(s: String, t: String) -> Int {\n    let n = s.utf8CString.count\n    let m = t.utf8CString.count\n    var dp = Array(repeating: 0, count: m + 1)\n    // Переход состояний: первая строка\n    for j in 1 ... m {\n        dp[j] = j\n    }\n    // Переход состояний: остальные строки\n    for i in 1 ... n {\n        // Переход состояний: первый столбец\n        var leftup = dp[0] // Временно сохранить dp[i-1, j-1]\n        dp[0] = i\n        // Переход состояний: остальные столбцы\n        for j in 1 ... m {\n            let temp = dp[j]\n            if s.utf8CString[i - 1] == t.utf8CString[j - 1] {\n                // Если два символа равны, сразу пропустить их\n                dp[j] = leftup\n            } else {\n                // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1\n            }\n            leftup = temp // Обновить до значения dp[i-1, j-1] для следующей итерации\n        }\n    }\n    return dp[m]\n}\n
        edit_distance.js
        /* Редакционное расстояние: динамическое программирование с оптимизацией памяти */\nfunction editDistanceDPComp(s, t) {\n    const n = s.length,\n        m = t.length;\n    const dp = new Array(m + 1).fill(0);\n    // Переход состояний: первая строка\n    for (let j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // Переход состояний: остальные строки\n    for (let i = 1; i <= n; i++) {\n        // Переход состояний: первый столбец\n        let leftup = dp[0]; // Временно сохранить dp[i-1, j-1]\n        dp[0] = i;\n        // Переход состояний: остальные столбцы\n        for (let j = 1; j <= m; j++) {\n            const temp = dp[j];\n            if (s.charAt(i - 1) === t.charAt(j - 1)) {\n                // Если два символа равны, сразу пропустить их\n                dp[j] = leftup;\n            } else {\n                // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[j] = Math.min(dp[j - 1], dp[j], leftup) + 1;\n            }\n            leftup = temp; // Обновить до значения dp[i-1, j-1] для следующей итерации\n        }\n    }\n    return dp[m];\n}\n
        edit_distance.ts
        /* Редакционное расстояние: динамическое программирование с оптимизацией памяти */\nfunction editDistanceDPComp(s: string, t: string): number {\n    const n = s.length,\n        m = t.length;\n    const dp = new Array(m + 1).fill(0);\n    // Переход состояний: первая строка\n    for (let j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // Переход состояний: остальные строки\n    for (let i = 1; i <= n; i++) {\n        // Переход состояний: первый столбец\n        let leftup = dp[0]; // Временно сохранить dp[i-1, j-1]\n        dp[0] = i;\n        // Переход состояний: остальные столбцы\n        for (let j = 1; j <= m; j++) {\n            const temp = dp[j];\n            if (s.charAt(i - 1) === t.charAt(j - 1)) {\n                // Если два символа равны, сразу пропустить их\n                dp[j] = leftup;\n            } else {\n                // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[j] = Math.min(dp[j - 1], dp[j], leftup) + 1;\n            }\n            leftup = temp; // Обновить до значения dp[i-1, j-1] для следующей итерации\n        }\n    }\n    return dp[m];\n}\n
        edit_distance.dart
        /* Редакционное расстояние: динамическое программирование с оптимизацией памяти */\nint editDistanceDPComp(String s, String t) {\n  int n = s.length, m = t.length;\n  List<int> dp = List.filled(m + 1, 0);\n  // Переход состояний: первая строка\n  for (int j = 1; j <= m; j++) {\n    dp[j] = j;\n  }\n  // Переход состояний: остальные строки\n  for (int i = 1; i <= n; i++) {\n    // Переход состояний: первый столбец\n    int leftup = dp[0]; // Временно сохранить dp[i-1, j-1]\n    dp[0] = i;\n    // Переход состояний: остальные столбцы\n    for (int j = 1; j <= m; j++) {\n      int temp = dp[j];\n      if (s[i - 1] == t[j - 1]) {\n        // Если два символа равны, сразу пропустить их\n        dp[j] = leftup;\n      } else {\n        // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n        dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1;\n      }\n      leftup = temp; // Обновить до значения dp[i-1, j-1] для следующей итерации\n    }\n  }\n  return dp[m];\n}\n
        edit_distance.rs
        /* Редакционное расстояние: динамическое программирование с оптимизацией памяти */\nfn edit_distance_dp_comp(s: &str, t: &str) -> i32 {\n    let (n, m) = (s.len(), t.len());\n    let mut dp = vec![0; m + 1];\n    // Переход состояний: первая строка\n    for j in 1..m {\n        dp[j] = j as i32;\n    }\n    // Переход состояний: остальные строки\n    for i in 1..=n {\n        // Переход состояний: первый столбец\n        let mut leftup = dp[0]; // Временно сохранить dp[i-1, j-1]\n        dp[0] = i as i32;\n        // Переход состояний: остальные столбцы\n        for j in 1..=m {\n            let temp = dp[j];\n            if s.chars().nth(i - 1) == t.chars().nth(j - 1) {\n                // Если два символа равны, сразу пропустить их\n                dp[j] = leftup;\n            } else {\n                // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[j] = std::cmp::min(std::cmp::min(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // Обновить до значения dp[i-1, j-1] для следующей итерации\n        }\n    }\n    dp[m]\n}\n
        edit_distance.c
        /* Редакционное расстояние: динамическое программирование с оптимизацией памяти */\nint editDistanceDPComp(char *s, char *t, int n, int m) {\n    int *dp = calloc(m + 1, sizeof(int));\n    // Переход состояний: первая строка\n    for (int j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // Переход состояний: остальные строки\n    for (int i = 1; i <= n; i++) {\n        // Переход состояний: первый столбец\n        int leftup = dp[0]; // Временно сохранить dp[i-1, j-1]\n        dp[0] = i;\n        // Переход состояний: остальные столбцы\n        for (int j = 1; j <= m; j++) {\n            int temp = dp[j];\n            if (s[i - 1] == t[j - 1]) {\n                // Если два символа равны, сразу пропустить их\n                dp[j] = leftup;\n            } else {\n                // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[j] = myMin(myMin(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // Обновить до значения dp[i-1, j-1] для следующей итерации\n        }\n    }\n    int res = dp[m];\n    // Освободить память\n    free(dp);\n    return res;\n}\n
        edit_distance.kt
        /* Редакционное расстояние: динамическое программирование с оптимизацией памяти */\nfun editDistanceDPComp(s: String, t: String): Int {\n    val n = s.length\n    val m = t.length\n    val dp = IntArray(m + 1)\n    // Переход состояний: первая строка\n    for (j in 1..m) {\n        dp[j] = j\n    }\n    // Переход состояний: остальные строки\n    for (i in 1..n) {\n        // Переход состояний: первый столбец\n        var leftup = dp[0] // Временно сохранить dp[i-1, j-1]\n        dp[0] = i\n        // Переход состояний: остальные столбцы\n        for (j in 1..m) {\n            val temp = dp[j]\n            if (s[i - 1] == t[j - 1]) {\n                // Если два символа равны, сразу пропустить их\n                dp[j] = leftup\n            } else {\n                // Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n                dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1\n            }\n            leftup = temp // Обновить до значения dp[i-1, j-1] для следующей итерации\n        }\n    }\n    return dp[m]\n}\n
        edit_distance.rb
        ### Редакционное расстояние: динамическое программирование с оптимизацией памяти ###\ndef edit_distance_dp_comp(s, t)\n  n, m = s.length, t.length\n  dp = Array.new(m + 1, 0)\n  # Переход состояний: первая строка\n  (1...(m + 1)).each { |j| dp[j] = j }\n  # Переход состояний: остальные строки\n  for i in 1...(n + 1)\n    # Переход состояний: первый столбец\n    leftup = dp.first # Временно сохранить dp[i-1, j-1]\n    dp[0] += 1\n    # Переход состояний: остальные столбцы\n    for j in 1...(m + 1)\n      temp = dp[j]\n      if s[i - 1] == t[j - 1]\n        # Если два символа равны, сразу пропустить их\n        dp[j] = leftup\n      else\n        # Минимальное число шагов редактирования = минимальное число шагов для вставки, удаления и замены + 1\n        dp[j] = [dp[j - 1], dp[j], leftup].min + 1\n      end\n      leftup = temp # Обновить до значения dp[i-1, j-1] для следующей итерации\n    end\n  end\n  dp[m]\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 14. Динамическое программирование","14.6   Задача о расстоянии редактирования"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/","level":1,"title":"14.1   Первое знакомство с динамическим программированием","text":"

        Динамическое программирование (dynamic programming) - это важная алгоритмическая парадигма, которая разбивает задачу на последовательность более мелких подзадач и за счет хранения их решений избегает повторных вычислений, тем самым резко повышая эффективность по времени.

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

        Подъем по лестнице

        Дана лестница из \\(n\\) ступеней. За один шаг можно подняться на \\(1\\) или на \\(2\\) ступени. Сколькими способами можно добраться до вершины?

        Как показано на рисунке 14-1, для лестницы из \\(3\\) ступеней существует \\(3\\) способа добраться до вершины.

        Рисунок 14-1   Число способов подняться на 3-ю ступень

        Цель этой задачи - вычислить количество способов. Поэтому можно попробовать использовать для ее решения метод поиска с возвратом. Если представить подъем по лестнице как последовательность решений, то мы начинаем от земли и на каждом раунде выбираем прыжок на \\(1\\) или на \\(2\\) ступени. Всякий раз, когда достигаем вершины, увеличиваем число способов на \\(1\\) , а если перескакиваем вершину, обрезаем эту ветвь. Код выглядит так:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_backtrack.py
        def backtrack(choices: list[int], state: int, n: int, res: list[int]) -> int:\n    \"\"\"Бэктрекинг\"\"\"\n    # Когда подъем достигает n-й ступени, число вариантов увеличивается на 1\n    if state == n:\n        res[0] += 1\n    # Перебор всех вариантов выбора\n    for choice in choices:\n        # Отсечение: нельзя выходить за n-ю ступень\n        if state + choice > n:\n            continue\n        # Попытка: сделать выбор и обновить состояние\n        backtrack(choices, state + choice, n, res)\n        # Откат\n\ndef climbing_stairs_backtrack(n: int) -> int:\n    \"\"\"Подъем по лестнице: бэктрекинг\"\"\"\n    choices = [1, 2]  # Можно подняться на 1 или 2 ступени\n    state = 0  # Начать подъем с 0-й ступени\n    res = [0]  # Использовать res[0] для хранения числа решений\n    backtrack(choices, state, n, res)\n    return res[0]\n
        climbing_stairs_backtrack.cpp
        /* Бэктрекинг */\nvoid backtrack(vector<int> &choices, int state, int n, vector<int> &res) {\n    // Когда подъем достигает n-й ступени, число вариантов увеличивается на 1\n    if (state == n)\n        res[0]++;\n    // Перебор всех вариантов выбора\n    for (auto &choice : choices) {\n        // Отсечение: нельзя выходить за n-ю ступень\n        if (state + choice > n)\n            continue;\n        // Попытка: сделать выбор и обновить состояние\n        backtrack(choices, state + choice, n, res);\n        // Откат\n    }\n}\n\n/* Подъем по лестнице: бэктрекинг */\nint climbingStairsBacktrack(int n) {\n    vector<int> choices = {1, 2}; // Можно подняться на 1 или 2 ступени\n    int state = 0;                // Начать подъем с 0-й ступени\n    vector<int> res = {0};        // Использовать res[0] для хранения числа решений\n    backtrack(choices, state, n, res);\n    return res[0];\n}\n
        climbing_stairs_backtrack.java
        /* Бэктрекинг */\nvoid backtrack(List<Integer> choices, int state, int n, List<Integer> res) {\n    // Когда подъем достигает n-й ступени, число вариантов увеличивается на 1\n    if (state == n)\n        res.set(0, res.get(0) + 1);\n    // Перебор всех вариантов выбора\n    for (Integer choice : choices) {\n        // Отсечение: нельзя выходить за n-ю ступень\n        if (state + choice > n)\n            continue;\n        // Попытка: сделать выбор и обновить состояние\n        backtrack(choices, state + choice, n, res);\n        // Откат\n    }\n}\n\n/* Подъем по лестнице: бэктрекинг */\nint climbingStairsBacktrack(int n) {\n    List<Integer> choices = Arrays.asList(1, 2); // Можно подняться на 1 или 2 ступени\n    int state = 0; // Начать подъем с 0-й ступени\n    List<Integer> res = new ArrayList<>();\n    res.add(0); // Использовать res[0] для хранения числа решений\n    backtrack(choices, state, n, res);\n    return res.get(0);\n}\n
        climbing_stairs_backtrack.cs
        /* Бэктрекинг */\nvoid Backtrack(List<int> choices, int state, int n, List<int> res) {\n    // Когда подъем достигает n-й ступени, число вариантов увеличивается на 1\n    if (state == n)\n        res[0]++;\n    // Перебор всех вариантов выбора\n    foreach (int choice in choices) {\n        // Отсечение: нельзя выходить за n-ю ступень\n        if (state + choice > n)\n            continue;\n        // Попытка: сделать выбор и обновить состояние\n        Backtrack(choices, state + choice, n, res);\n        // Откат\n    }\n}\n\n/* Подъем по лестнице: бэктрекинг */\nint ClimbingStairsBacktrack(int n) {\n    List<int> choices = [1, 2]; // Можно подняться на 1 или 2 ступени\n    int state = 0; // Начать подъем с 0-й ступени\n    List<int> res = [0]; // Использовать res[0] для хранения числа решений\n    Backtrack(choices, state, n, res);\n    return res[0];\n}\n
        climbing_stairs_backtrack.go
        /* Бэктрекинг */\nfunc backtrack(choices []int, state, n int, res []int) {\n    // Когда подъем достигает n-й ступени, число вариантов увеличивается на 1\n    if state == n {\n        res[0] = res[0] + 1\n    }\n    // Перебор всех вариантов выбора\n    for _, choice := range choices {\n        // Отсечение: нельзя выходить за n-ю ступень\n        if state+choice > n {\n            continue\n        }\n        // Попытка: сделать выбор и обновить состояние\n        backtrack(choices, state+choice, n, res)\n        // Откат\n    }\n}\n\n/* Подъем по лестнице: бэктрекинг */\nfunc climbingStairsBacktrack(n int) int {\n    // Можно подняться на 1 или 2 ступени\n    choices := []int{1, 2}\n    // Начать подъем с 0-й ступени\n    state := 0\n    res := make([]int, 1)\n    // Использовать res[0] для хранения числа решений\n    res[0] = 0\n    backtrack(choices, state, n, res)\n    return res[0]\n}\n
        climbing_stairs_backtrack.swift
        /* Бэктрекинг */\nfunc backtrack(choices: [Int], state: Int, n: Int, res: inout [Int]) {\n    // Когда подъем достигает n-й ступени, число вариантов увеличивается на 1\n    if state == n {\n        res[0] += 1\n    }\n    // Перебор всех вариантов выбора\n    for choice in choices {\n        // Отсечение: нельзя выходить за n-ю ступень\n        if state + choice > n {\n            continue\n        }\n        // Попытка: сделать выбор и обновить состояние\n        backtrack(choices: choices, state: state + choice, n: n, res: &res)\n        // Откат\n    }\n}\n\n/* Подъем по лестнице: бэктрекинг */\nfunc climbingStairsBacktrack(n: Int) -> Int {\n    let choices = [1, 2] // Можно подняться на 1 или 2 ступени\n    let state = 0 // Начать подъем с 0-й ступени\n    var res: [Int] = []\n    res.append(0) // Использовать res[0] для хранения числа решений\n    backtrack(choices: choices, state: state, n: n, res: &res)\n    return res[0]\n}\n
        climbing_stairs_backtrack.js
        /* Бэктрекинг */\nfunction backtrack(choices, state, n, res) {\n    // Когда подъем достигает n-й ступени, число вариантов увеличивается на 1\n    if (state === n) res.set(0, res.get(0) + 1);\n    // Перебор всех вариантов выбора\n    for (const choice of choices) {\n        // Отсечение: нельзя выходить за n-ю ступень\n        if (state + choice > n) continue;\n        // Попытка: сделать выбор и обновить состояние\n        backtrack(choices, state + choice, n, res);\n        // Откат\n    }\n}\n\n/* Подъем по лестнице: бэктрекинг */\nfunction climbingStairsBacktrack(n) {\n    const choices = [1, 2]; // Можно подняться на 1 или 2 ступени\n    const state = 0; // Начать подъем с 0-й ступени\n    const res = new Map();\n    res.set(0, 0); // Использовать res[0] для хранения числа решений\n    backtrack(choices, state, n, res);\n    return res.get(0);\n}\n
        climbing_stairs_backtrack.ts
        /* Бэктрекинг */\nfunction backtrack(\n    choices: number[],\n    state: number,\n    n: number,\n    res: Map<0, any>\n): void {\n    // Когда подъем достигает n-й ступени, число вариантов увеличивается на 1\n    if (state === n) res.set(0, res.get(0) + 1);\n    // Перебор всех вариантов выбора\n    for (const choice of choices) {\n        // Отсечение: нельзя выходить за n-ю ступень\n        if (state + choice > n) continue;\n        // Попытка: сделать выбор и обновить состояние\n        backtrack(choices, state + choice, n, res);\n        // Откат\n    }\n}\n\n/* Подъем по лестнице: бэктрекинг */\nfunction climbingStairsBacktrack(n: number): number {\n    const choices = [1, 2]; // Можно подняться на 1 или 2 ступени\n    const state = 0; // Начать подъем с 0-й ступени\n    const res = new Map();\n    res.set(0, 0); // Использовать res[0] для хранения числа решений\n    backtrack(choices, state, n, res);\n    return res.get(0);\n}\n
        climbing_stairs_backtrack.dart
        /* Бэктрекинг */\nvoid backtrack(List<int> choices, int state, int n, List<int> res) {\n  // Когда подъем достигает n-й ступени, число вариантов увеличивается на 1\n  if (state == n) {\n    res[0]++;\n  }\n  // Перебор всех вариантов выбора\n  for (int choice in choices) {\n    // Отсечение: нельзя выходить за n-ю ступень\n    if (state + choice > n) continue;\n    // Попытка: сделать выбор и обновить состояние\n    backtrack(choices, state + choice, n, res);\n    // Откат\n  }\n}\n\n/* Подъем по лестнице: бэктрекинг */\nint climbingStairsBacktrack(int n) {\n  List<int> choices = [1, 2]; // Можно подняться на 1 или 2 ступени\n  int state = 0; // Начать подъем с 0-й ступени\n  List<int> res = [];\n  res.add(0); // Использовать res[0] для хранения числа решений\n  backtrack(choices, state, n, res);\n  return res[0];\n}\n
        climbing_stairs_backtrack.rs
        /* Бэктрекинг */\nfn backtrack(choices: &[i32], state: i32, n: i32, res: &mut [i32]) {\n    // Когда подъем достигает n-й ступени, число вариантов увеличивается на 1\n    if state == n {\n        res[0] = res[0] + 1;\n    }\n    // Перебор всех вариантов выбора\n    for &choice in choices {\n        // Отсечение: нельзя выходить за n-ю ступень\n        if state + choice > n {\n            continue;\n        }\n        // Попытка: сделать выбор и обновить состояние\n        backtrack(choices, state + choice, n, res);\n        // Откат\n    }\n}\n\n/* Подъем по лестнице: бэктрекинг */\nfn climbing_stairs_backtrack(n: usize) -> i32 {\n    let choices = vec![1, 2]; // Можно подняться на 1 или 2 ступени\n    let state = 0; // Начать подъем с 0-й ступени\n    let mut res = Vec::new();\n    res.push(0); // Использовать res[0] для хранения числа решений\n    backtrack(&choices, state, n as i32, &mut res);\n    res[0]\n}\n
        climbing_stairs_backtrack.c
        /* Бэктрекинг */\nvoid backtrack(int *choices, int state, int n, int *res, int len) {\n    // Когда подъем достигает n-й ступени, число вариантов увеличивается на 1\n    if (state == n)\n        res[0]++;\n    // Перебор всех вариантов выбора\n    for (int i = 0; i < len; i++) {\n        int choice = choices[i];\n        // Отсечение: нельзя выходить за n-ю ступень\n        if (state + choice > n)\n            continue;\n        // Попытка: сделать выбор и обновить состояние\n        backtrack(choices, state + choice, n, res, len);\n        // Откат\n    }\n}\n\n/* Подъем по лестнице: бэктрекинг */\nint climbingStairsBacktrack(int n) {\n    int choices[2] = {1, 2}; // Можно подняться на 1 или 2 ступени\n    int state = 0;           // Начать подъем с 0-й ступени\n    int *res = (int *)malloc(sizeof(int));\n    *res = 0; // Использовать res[0] для хранения числа решений\n    int len = sizeof(choices) / sizeof(int);\n    backtrack(choices, state, n, res, len);\n    int result = *res;\n    free(res);\n    return result;\n}\n
        climbing_stairs_backtrack.kt
        /* Бэктрекинг */\nfun backtrack(\n    choices: MutableList<Int>,\n    state: Int,\n    n: Int,\n    res: MutableList<Int>\n) {\n    // Когда подъем достигает n-й ступени, число вариантов увеличивается на 1\n    if (state == n)\n        res[0] = res[0] + 1\n    // Перебор всех вариантов выбора\n    for (choice in choices) {\n        // Отсечение: нельзя выходить за n-ю ступень\n        if (state + choice > n) continue\n        // Попытка: сделать выбор и обновить состояние\n        backtrack(choices, state + choice, n, res)\n        // Откат\n    }\n}\n\n/* Подъем по лестнице: бэктрекинг */\nfun climbingStairsBacktrack(n: Int): Int {\n    val choices = mutableListOf(1, 2) // Можно подняться на 1 или 2 ступени\n    val state = 0 // Начать подъем с 0-й ступени\n    val res = mutableListOf<Int>()\n    res.add(0) // Использовать res[0] для хранения числа решений\n    backtrack(choices, state, n, res)\n    return res[0]\n}\n
        climbing_stairs_backtrack.rb
        ### Бэктрекинг ###\ndef backtrack(choices, state, n, res)\n  # Когда подъем достигает n-й ступени, число вариантов увеличивается на 1\n  res[0] += 1 if state == n\n  # Перебор всех вариантов выбора\n  for choice in choices\n    # Отсечение: нельзя выходить за n-ю ступень\n    next if state + choice > n\n\n    # Попытка: сделать выбор и обновить состояние\n    backtrack(choices, state + choice, n, res)\n  end\n  # Откат\nend\n\n### Подъем по лестнице: бэктрекинг ###\ndef climbing_stairs_backtrack(n)\n  choices = [1, 2] # Можно подняться на 1 или 2 ступени\n  state = 0 # Начать подъем с 0-й ступени\n  res = [0] # Использовать res[0] для хранения числа решений\n  backtrack(choices, state, n, res)\n  res.first\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 14. Динамическое программирование","14.1   Первое знакомство с динамическим программированием"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1411-1","level":2,"title":"14.1.1   Метод 1: полный перебор","text":"

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

        Попробуем посмотреть на задачу именно как на разложение подзадач. Пусть число способов добраться до ступени \\(i\\) равно \\(dp[i]\\). Тогда \\(dp[i]\\) - это исходная задача, а ее подзадачи включают:

        \\[ dp[i-1], dp[i-2], \\dots, dp[2], dp[1] \\]

        Поскольку за один раунд можно подняться только на \\(1\\) или на \\(2\\) ступени, стоя на ступени \\(i\\) , в предыдущий раунд мы могли находиться только на ступени \\(i - 1\\) или на ступени \\(i - 2\\) . Иначе говоря, на ступень \\(i\\) можно попасть только со ступени \\(i -1\\) или со ступени \\(i - 2\\) .

        Отсюда получается важный вывод: число способов добраться до ступени \\(i - 1\\) плюс число способов добраться до ступени \\(i - 2\\) равно числу способов добраться до ступени \\(i\\). Формула имеет вид:

        \\[ dp[i] = dp[i-1] + dp[i-2] \\]

        Это означает, что в задаче о подъеме по лестнице между подзадачами существует рекуррентная зависимость, и решение исходной задачи может быть построено на основе решений подзадач. Эта связь показана на рисунке 14-2.

        Рисунок 14-2   Рекуррентная связь числа способов

        По рекуррентной формуле можно получить решение полного перебора. Начиная с \\(dp[n]\\) , мы рекурсивно разлагаем большую задачу в сумму двух меньших задач , пока не дойдем до наименьших подзадач \\(dp[1]\\) и \\(dp[2]\\) . Их решения уже известны: \\(dp[1] = 1\\) и \\(dp[2] = 2\\) , что означает \\(1\\) и \\(2\\) способа подняться соответственно на \\(1\\)-ю и \\(2\\)-ю ступени.

        Посмотрите на следующий код: как и стандартный поиск с возвратом, он относится к поиску в глубину, но выглядит более компактно:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dfs.py
        def dfs(i: int) -> int:\n    \"\"\"Поиск\"\"\"\n    # dp[1] и dp[2] уже известны, вернуть их\n    if i == 1 or i == 2:\n        return i\n    # dp[i] = dp[i-1] + dp[i-2]\n    count = dfs(i - 1) + dfs(i - 2)\n    return count\n\ndef climbing_stairs_dfs(n: int) -> int:\n    \"\"\"Подъем по лестнице: поиск\"\"\"\n    return dfs(n)\n
        climbing_stairs_dfs.cpp
        /* Поиск */\nint dfs(int i) {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if (i == 1 || i == 2)\n        return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1) + dfs(i - 2);\n    return count;\n}\n\n/* Подъем по лестнице: поиск */\nint climbingStairsDFS(int n) {\n    return dfs(n);\n}\n
        climbing_stairs_dfs.java
        /* Поиск */\nint dfs(int i) {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if (i == 1 || i == 2)\n        return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1) + dfs(i - 2);\n    return count;\n}\n\n/* Подъем по лестнице: поиск */\nint climbingStairsDFS(int n) {\n    return dfs(n);\n}\n
        climbing_stairs_dfs.cs
        /* Поиск */\nint DFS(int i) {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if (i == 1 || i == 2)\n        return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = DFS(i - 1) + DFS(i - 2);\n    return count;\n}\n\n/* Подъем по лестнице: поиск */\nint ClimbingStairsDFS(int n) {\n    return DFS(n);\n}\n
        climbing_stairs_dfs.go
        /* Поиск */\nfunc dfs(i int) int {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if i == 1 || i == 2 {\n        return i\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    count := dfs(i-1) + dfs(i-2)\n    return count\n}\n\n/* Подъем по лестнице: поиск */\nfunc climbingStairsDFS(n int) int {\n    return dfs(n)\n}\n
        climbing_stairs_dfs.swift
        /* Поиск */\nfunc dfs(i: Int) -> Int {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if i == 1 || i == 2 {\n        return i\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    let count = dfs(i: i - 1) + dfs(i: i - 2)\n    return count\n}\n\n/* Подъем по лестнице: поиск */\nfunc climbingStairsDFS(n: Int) -> Int {\n    dfs(i: n)\n}\n
        climbing_stairs_dfs.js
        /* Поиск */\nfunction dfs(i) {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if (i === 1 || i === 2) return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    const count = dfs(i - 1) + dfs(i - 2);\n    return count;\n}\n\n/* Подъем по лестнице: поиск */\nfunction climbingStairsDFS(n) {\n    return dfs(n);\n}\n
        climbing_stairs_dfs.ts
        /* Поиск */\nfunction dfs(i: number): number {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if (i === 1 || i === 2) return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    const count = dfs(i - 1) + dfs(i - 2);\n    return count;\n}\n\n/* Подъем по лестнице: поиск */\nfunction climbingStairsDFS(n: number): number {\n    return dfs(n);\n}\n
        climbing_stairs_dfs.dart
        /* Поиск */\nint dfs(int i) {\n  // dp[1] и dp[2] уже известны, вернуть их\n  if (i == 1 || i == 2) return i;\n  // dp[i] = dp[i-1] + dp[i-2]\n  int count = dfs(i - 1) + dfs(i - 2);\n  return count;\n}\n\n/* Подъем по лестнице: поиск */\nint climbingStairsDFS(int n) {\n  return dfs(n);\n}\n
        climbing_stairs_dfs.rs
        /* Поиск */\nfn dfs(i: usize) -> i32 {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if i == 1 || i == 2 {\n        return i as i32;\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    let count = dfs(i - 1) + dfs(i - 2);\n    count\n}\n\n/* Подъем по лестнице: поиск */\nfn climbing_stairs_dfs(n: usize) -> i32 {\n    dfs(n)\n}\n
        climbing_stairs_dfs.c
        /* Поиск */\nint dfs(int i) {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if (i == 1 || i == 2)\n        return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1) + dfs(i - 2);\n    return count;\n}\n\n/* Подъем по лестнице: поиск */\nint climbingStairsDFS(int n) {\n    return dfs(n);\n}\n
        climbing_stairs_dfs.kt
        /* Поиск */\nfun dfs(i: Int): Int {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if (i == 1 || i == 2) return i\n    // dp[i] = dp[i-1] + dp[i-2]\n    val count = dfs(i - 1) + dfs(i - 2)\n    return count\n}\n\n/* Подъем по лестнице: поиск */\nfun climbingStairsDFS(n: Int): Int {\n    return dfs(n)\n}\n
        climbing_stairs_dfs.rb
        ### Поиск ###\ndef dfs(i)\n  # dp[1] и dp[2] уже известны, вернуть их\n  return i if i == 1 || i == 2\n  # dp[i] = dp[i-1] + dp[i-2]\n  dfs(i - 1) + dfs(i - 2)\nend\n\n### Подъем по лестнице: поиск ###\ndef climbing_stairs_dfs(n)\n  dfs(n)\nend\n
        Визуализация кода

        Во весь экран >

        На рисунке 14-3 показано дерево рекурсии, возникающее при полном переборе. Для задачи \\(dp[n]\\) глубина дерева рекурсии равна \\(n\\) , а временная сложность равна \\(O(2^n)\\) . Экспоненциальный рост взрывообразен: если подать на вход достаточно большое значение \\(n\\) , ожидание станет очень долгим.

        Рисунок 14-3   Дерево рекурсии для подъема по лестнице

        Как видно на рисунке 14-3, экспоненциальная временная сложность порождается «перекрывающимися подзадачами». Например, \\(dp[9]\\) раскладывается в \\(dp[8]\\) и \\(dp[7]\\) , а \\(dp[8]\\) - в \\(dp[7]\\) и \\(dp[6]\\). Обе ветви содержат подзадачу \\(dp[7]\\) .

        Продолжая это рассуждение, мы видим, что подзадачи порождают все более мелкие перекрывающиеся подзадачи без конца. Подавляющая часть вычислительных ресурсов уходит именно на них.

        ","path":["Глава 14. Динамическое программирование","14.1   Первое знакомство с динамическим программированием"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1412-2","level":2,"title":"14.1.2   Метод 2: поиск с мемоизацией","text":"

        Чтобы ускорить алгоритм, мы хотим, чтобы каждая перекрывающаяся подзадача вычислялась только один раз. Для этого объявим массив mem для хранения решения каждой подзадачи и будем обрезать повторные вычисления в процессе поиска.

        1. Когда \\(dp[i]\\) вычисляется впервые, мы сохраняем его в mem[i] для последующего использования.
        2. Когда значение \\(dp[i]\\) требуется снова, мы просто берем его напрямую из mem[i] и тем самым избегаем повторного вычисления подзадачи.

        Код приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dfs_mem.py
        def dfs(i: int, mem: list[int]) -> int:\n    \"\"\"Поиск с мемоизацией\"\"\"\n    # dp[1] и dp[2] уже известны, вернуть их\n    if i == 1 or i == 2:\n        return i\n    # Если запись dp[i] существует, сразу вернуть ее\n    if mem[i] != -1:\n        return mem[i]\n    # dp[i] = dp[i-1] + dp[i-2]\n    count = dfs(i - 1, mem) + dfs(i - 2, mem)\n    # Сохранить dp[i]\n    mem[i] = count\n    return count\n\ndef climbing_stairs_dfs_mem(n: int) -> int:\n    \"\"\"Подъем по лестнице: поиск с мемоизацией\"\"\"\n    # mem[i] хранит число способов подняться на i-ю ступень, -1 означает отсутствие записи\n    mem = [-1] * (n + 1)\n    return dfs(n, mem)\n
        climbing_stairs_dfs_mem.cpp
        /* Поиск с мемоизацией */\nint dfs(int i, vector<int> &mem) {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if (i == 1 || i == 2)\n        return i;\n    // Если запись dp[i] существует, сразу вернуть ее\n    if (mem[i] != -1)\n        return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // Сохранить dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* Подъем по лестнице: поиск с мемоизацией */\nint climbingStairsDFSMem(int n) {\n    // mem[i] хранит число способов подняться на i-ю ступень, -1 означает отсутствие записи\n    vector<int> mem(n + 1, -1);\n    return dfs(n, mem);\n}\n
        climbing_stairs_dfs_mem.java
        /* Поиск с мемоизацией */\nint dfs(int i, int[] mem) {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if (i == 1 || i == 2)\n        return i;\n    // Если запись dp[i] существует, сразу вернуть ее\n    if (mem[i] != -1)\n        return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // Сохранить dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* Подъем по лестнице: поиск с мемоизацией */\nint climbingStairsDFSMem(int n) {\n    // mem[i] хранит число способов подняться на i-ю ступень, -1 означает отсутствие записи\n    int[] mem = new int[n + 1];\n    Arrays.fill(mem, -1);\n    return dfs(n, mem);\n}\n
        climbing_stairs_dfs_mem.cs
        /* Поиск с мемоизацией */\nint DFS(int i, int[] mem) {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if (i == 1 || i == 2)\n        return i;\n    // Если запись dp[i] существует, сразу вернуть ее\n    if (mem[i] != -1)\n        return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = DFS(i - 1, mem) + DFS(i - 2, mem);\n    // Сохранить dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* Подъем по лестнице: поиск с мемоизацией */\nint ClimbingStairsDFSMem(int n) {\n    // mem[i] хранит число способов подняться на i-ю ступень, -1 означает отсутствие записи\n    int[] mem = new int[n + 1];\n    Array.Fill(mem, -1);\n    return DFS(n, mem);\n}\n
        climbing_stairs_dfs_mem.go
        /* Поиск с мемоизацией */\nfunc dfsMem(i int, mem []int) int {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if i == 1 || i == 2 {\n        return i\n    }\n    // Если запись dp[i] существует, сразу вернуть ее\n    if mem[i] != -1 {\n        return mem[i]\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    count := dfsMem(i-1, mem) + dfsMem(i-2, mem)\n    // Сохранить dp[i]\n    mem[i] = count\n    return count\n}\n\n/* Подъем по лестнице: поиск с мемоизацией */\nfunc climbingStairsDFSMem(n int) int {\n    // mem[i] хранит число способов подняться на i-ю ступень, -1 означает отсутствие записи\n    mem := make([]int, n+1)\n    for i := range mem {\n        mem[i] = -1\n    }\n    return dfsMem(n, mem)\n}\n
        climbing_stairs_dfs_mem.swift
        /* Поиск с мемоизацией */\nfunc dfs(i: Int, mem: inout [Int]) -> Int {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if i == 1 || i == 2 {\n        return i\n    }\n    // Если запись dp[i] существует, сразу вернуть ее\n    if mem[i] != -1 {\n        return mem[i]\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    let count = dfs(i: i - 1, mem: &mem) + dfs(i: i - 2, mem: &mem)\n    // Сохранить dp[i]\n    mem[i] = count\n    return count\n}\n\n/* Подъем по лестнице: поиск с мемоизацией */\nfunc climbingStairsDFSMem(n: Int) -> Int {\n    // mem[i] хранит число способов подняться на i-ю ступень, -1 означает отсутствие записи\n    var mem = Array(repeating: -1, count: n + 1)\n    return dfs(i: n, mem: &mem)\n}\n
        climbing_stairs_dfs_mem.js
        /* Поиск с мемоизацией */\nfunction dfs(i, mem) {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if (i === 1 || i === 2) return i;\n    // Если запись dp[i] существует, сразу вернуть ее\n    if (mem[i] != -1) return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    const count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // Сохранить dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* Подъем по лестнице: поиск с мемоизацией */\nfunction climbingStairsDFSMem(n) {\n    // mem[i] хранит число способов подняться на i-ю ступень, -1 означает отсутствие записи\n    const mem = new Array(n + 1).fill(-1);\n    return dfs(n, mem);\n}\n
        climbing_stairs_dfs_mem.ts
        /* Поиск с мемоизацией */\nfunction dfs(i: number, mem: number[]): number {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if (i === 1 || i === 2) return i;\n    // Если запись dp[i] существует, сразу вернуть ее\n    if (mem[i] != -1) return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    const count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // Сохранить dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* Подъем по лестнице: поиск с мемоизацией */\nfunction climbingStairsDFSMem(n: number): number {\n    // mem[i] хранит число способов подняться на i-ю ступень, -1 означает отсутствие записи\n    const mem = new Array(n + 1).fill(-1);\n    return dfs(n, mem);\n}\n
        climbing_stairs_dfs_mem.dart
        /* Поиск с мемоизацией */\nint dfs(int i, List<int> mem) {\n  // dp[1] и dp[2] уже известны, вернуть их\n  if (i == 1 || i == 2) return i;\n  // Если запись dp[i] существует, сразу вернуть ее\n  if (mem[i] != -1) return mem[i];\n  // dp[i] = dp[i-1] + dp[i-2]\n  int count = dfs(i - 1, mem) + dfs(i - 2, mem);\n  // Сохранить dp[i]\n  mem[i] = count;\n  return count;\n}\n\n/* Подъем по лестнице: поиск с мемоизацией */\nint climbingStairsDFSMem(int n) {\n  // mem[i] хранит число способов подняться на i-ю ступень, -1 означает отсутствие записи\n  List<int> mem = List.filled(n + 1, -1);\n  return dfs(n, mem);\n}\n
        climbing_stairs_dfs_mem.rs
        /* Поиск с мемоизацией */\nfn dfs(i: usize, mem: &mut [i32]) -> i32 {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if i == 1 || i == 2 {\n        return i as i32;\n    }\n    // Если запись dp[i] существует, сразу вернуть ее\n    if mem[i] != -1 {\n        return mem[i];\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    let count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // Сохранить dp[i]\n    mem[i] = count;\n    count\n}\n\n/* Подъем по лестнице: поиск с мемоизацией */\nfn climbing_stairs_dfs_mem(n: usize) -> i32 {\n    // mem[i] хранит число способов подняться на i-ю ступень, -1 означает отсутствие записи\n    let mut mem = vec![-1; n + 1];\n    dfs(n, &mut mem)\n}\n
        climbing_stairs_dfs_mem.c
        /* Поиск с мемоизацией */\nint dfs(int i, int *mem) {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if (i == 1 || i == 2)\n        return i;\n    // Если запись dp[i] существует, сразу вернуть ее\n    if (mem[i] != -1)\n        return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // Сохранить dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* Подъем по лестнице: поиск с мемоизацией */\nint climbingStairsDFSMem(int n) {\n    // mem[i] хранит число способов подняться на i-ю ступень, -1 означает отсутствие записи\n    int *mem = (int *)malloc((n + 1) * sizeof(int));\n    for (int i = 0; i <= n; i++) {\n        mem[i] = -1;\n    }\n    int result = dfs(n, mem);\n    free(mem);\n    return result;\n}\n
        climbing_stairs_dfs_mem.kt
        /* Поиск с мемоизацией */\nfun dfs(i: Int, mem: IntArray): Int {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if (i == 1 || i == 2) return i\n    // Если запись dp[i] существует, сразу вернуть ее\n    if (mem[i] != -1) return mem[i]\n    // dp[i] = dp[i-1] + dp[i-2]\n    val count = dfs(i - 1, mem) + dfs(i - 2, mem)\n    // Сохранить dp[i]\n    mem[i] = count\n    return count\n}\n\n/* Подъем по лестнице: поиск с мемоизацией */\nfun climbingStairsDFSMem(n: Int): Int {\n    // mem[i] хранит число способов подняться на i-ю ступень, -1 означает отсутствие записи\n    val mem = IntArray(n + 1)\n    mem.fill(-1)\n    return dfs(n, mem)\n}\n
        climbing_stairs_dfs_mem.rb
        ### Поиск с мемоизацией ###\ndef dfs(i, mem)\n  # dp[1] и dp[2] уже известны, вернуть их\n  return i if i == 1 || i == 2\n  # Если запись dp[i] существует, сразу вернуть ее\n  return mem[i] if mem[i] != -1\n\n  # dp[i] = dp[i-1] + dp[i-2]\n  count = dfs(i - 1, mem) + dfs(i - 2, mem)\n  # Сохранить dp[i]\n  mem[i] = count\nend\n\n### Подъем по лестнице: поиск с мемоизацией ###\ndef climbing_stairs_dfs_mem(n)\n  # mem[i] хранит число способов подняться на i-ю ступень, -1 означает отсутствие записи\n  mem = Array.new(n + 1, -1)\n  dfs(n, mem)\nend\n
        Визуализация кода

        Во весь экран >

        Как показано на рисунке 14-4, после введения мемоизации каждая перекрывающаяся подзадача вычисляется только один раз, и временная сложность оптимизируется до \\(O(n)\\) . Это огромный скачок в эффективности.

        Рисунок 14-4   Дерево рекурсии для поиска с мемоизацией

        ","path":["Глава 14. Динамическое программирование","14.1   Первое знакомство с динамическим программированием"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1413-3","level":2,"title":"14.1.3   Метод 3: динамическое программирование","text":"

        Поиск с мемоизацией - это метод «сверху вниз» : мы начинаем с исходной задачи (корня), рекурсивно раскладываем более крупные подзадачи на меньшие, пока не достигнем наименьших подзадач с уже известным ответом (листьев). Затем в процессе возврата постепенно собираем решения подзадач и тем самым получаем решение исходной задачи.

        Напротив, динамическое программирование - это метод «снизу вверх» : начиная с решений наименьших подзадач, мы итеративно строим решения для более крупных подзадач, пока не получим ответ на исходную задачу.

        Поскольку в динамическом программировании нет этапа возврата, для его реализации достаточно обычных циклов, без рекурсии. В приведенном ниже коде мы инициализируем массив dp для хранения решений подзадач. Он выполняет ту же роль, что и массив mem в мемоизированном поиске:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dp.py
        def climbing_stairs_dp(n: int) -> int:\n    \"\"\"Подъем по лестнице: динамическое программирование\"\"\"\n    if n == 1 or n == 2:\n        return n\n    # Инициализация таблицы dp для хранения решений подзадач\n    dp = [0] * (n + 1)\n    # Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1], dp[2] = 1, 2\n    # Переход состояний: постепенное решение больших подзадач через меньшие\n    for i in range(3, n + 1):\n        dp[i] = dp[i - 1] + dp[i - 2]\n    return dp[n]\n
        climbing_stairs_dp.cpp
        /* Подъем по лестнице: динамическое программирование */\nint climbingStairsDP(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    // Инициализация таблицы dp для хранения решений подзадач\n    vector<int> dp(n + 1);\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1] = 1;\n    dp[2] = 2;\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (int i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    return dp[n];\n}\n
        climbing_stairs_dp.java
        /* Подъем по лестнице: динамическое программирование */\nint climbingStairsDP(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    // Инициализация таблицы dp для хранения решений подзадач\n    int[] dp = new int[n + 1];\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1] = 1;\n    dp[2] = 2;\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (int i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    return dp[n];\n}\n
        climbing_stairs_dp.cs
        /* Подъем по лестнице: динамическое программирование */\nint ClimbingStairsDP(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    // Инициализация таблицы dp для хранения решений подзадач\n    int[] dp = new int[n + 1];\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1] = 1;\n    dp[2] = 2;\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (int i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    return dp[n];\n}\n
        climbing_stairs_dp.go
        /* Подъем по лестнице: динамическое программирование */\nfunc climbingStairsDP(n int) int {\n    if n == 1 || n == 2 {\n        return n\n    }\n    // Инициализация таблицы dp для хранения решений подзадач\n    dp := make([]int, n+1)\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1] = 1\n    dp[2] = 2\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for i := 3; i <= n; i++ {\n        dp[i] = dp[i-1] + dp[i-2]\n    }\n    return dp[n]\n}\n
        climbing_stairs_dp.swift
        /* Подъем по лестнице: динамическое программирование */\nfunc climbingStairsDP(n: Int) -> Int {\n    if n == 1 || n == 2 {\n        return n\n    }\n    // Инициализация таблицы dp для хранения решений подзадач\n    var dp = Array(repeating: 0, count: n + 1)\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1] = 1\n    dp[2] = 2\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for i in 3 ... n {\n        dp[i] = dp[i - 1] + dp[i - 2]\n    }\n    return dp[n]\n}\n
        climbing_stairs_dp.js
        /* Подъем по лестнице: динамическое программирование */\nfunction climbingStairsDP(n) {\n    if (n === 1 || n === 2) return n;\n    // Инициализация таблицы dp для хранения решений подзадач\n    const dp = new Array(n + 1).fill(-1);\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1] = 1;\n    dp[2] = 2;\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (let i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    return dp[n];\n}\n
        climbing_stairs_dp.ts
        /* Подъем по лестнице: динамическое программирование */\nfunction climbingStairsDP(n: number): number {\n    if (n === 1 || n === 2) return n;\n    // Инициализация таблицы dp для хранения решений подзадач\n    const dp = new Array(n + 1).fill(-1);\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1] = 1;\n    dp[2] = 2;\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (let i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    return dp[n];\n}\n
        climbing_stairs_dp.dart
        /* Подъем по лестнице: динамическое программирование */\nint climbingStairsDP(int n) {\n  if (n == 1 || n == 2) return n;\n  // Инициализация таблицы dp для хранения решений подзадач\n  List<int> dp = List.filled(n + 1, 0);\n  // Начальное состояние: заранее задать решения наименьших подзадач\n  dp[1] = 1;\n  dp[2] = 2;\n  // Переход состояний: постепенное решение больших подзадач через меньшие\n  for (int i = 3; i <= n; i++) {\n    dp[i] = dp[i - 1] + dp[i - 2];\n  }\n  return dp[n];\n}\n
        climbing_stairs_dp.rs
        /* Подъем по лестнице: динамическое программирование */\nfn climbing_stairs_dp(n: usize) -> i32 {\n    // dp[1] и dp[2] уже известны, вернуть их\n    if n == 1 || n == 2 {\n        return n as i32;\n    }\n    // Инициализация таблицы dp для хранения решений подзадач\n    let mut dp = vec![-1; n + 1];\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1] = 1;\n    dp[2] = 2;\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for i in 3..=n {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    dp[n]\n}\n
        climbing_stairs_dp.c
        /* Подъем по лестнице: динамическое программирование */\nint climbingStairsDP(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    // Инициализация таблицы dp для хранения решений подзадач\n    int *dp = (int *)malloc((n + 1) * sizeof(int));\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1] = 1;\n    dp[2] = 2;\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (int i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    int result = dp[n];\n    free(dp);\n    return result;\n}\n
        climbing_stairs_dp.kt
        /* Подъем по лестнице: динамическое программирование */\nfun climbingStairsDP(n: Int): Int {\n    if (n == 1 || n == 2) return n\n    // Инициализация таблицы dp для хранения решений подзадач\n    val dp = IntArray(n + 1)\n    // Начальное состояние: заранее задать решения наименьших подзадач\n    dp[1] = 1\n    dp[2] = 2\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for (i in 3..n) {\n        dp[i] = dp[i - 1] + dp[i - 2]\n    }\n    return dp[n]\n}\n
        climbing_stairs_dp.rb
        ### Подъем по лестнице: динамическое программирование ###\ndef climbing_stairs_dp(n)\n  return n  if n == 1 || n == 2\n\n  # Инициализация таблицы dp для хранения решений подзадач\n  dp = Array.new(n + 1, 0)\n  # Начальное состояние: заранее задать решения наименьших подзадач\n  dp[1], dp[2] = 1, 2\n  # Переход состояний: постепенное решение больших подзадач через меньшие\n  (3...(n + 1)).each { |i| dp[i] = dp[i - 1] + dp[i - 2] }\n\n  dp[n]\nend\n
        Визуализация кода

        Во весь экран >

        На рисунке 14-5 смоделирован процесс выполнения этого кода.

        Рисунок 14-5   Процесс динамического программирования для подъема по лестнице

        Как и в поиске с возвратом, в динамическом программировании используется понятие «состояние» для обозначения некоторого этапа решения задачи. Каждое состояние соответствует одной подзадаче и ее локально оптимальному решению. Например, в задаче о лестнице состояние определяется текущим номером ступени \\(i\\) .

        На основе сказанного можно подвести несколько часто используемых терминов динамического программирования.

        • Массив dp называют таблицей dp, а \\(dp[i]\\) обозначает решение подзадачи, соответствующей состоянию \\(i\\) .
        • Состояния, соответствующие наименьшим подзадачам (первая и вторая ступени), называют начальными состояниями.
        • Рекуррентную формулу \\(dp[i] = dp[i-1] + dp[i-2]\\) называют уравнением перехода состояния.
        ","path":["Глава 14. Динамическое программирование","14.1   Первое знакомство с динамическим программированием"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1414","level":2,"title":"14.1.4   Оптимизация пространства","text":"

        Внимательный читатель мог заметить, что поскольку \\(dp[i]\\) зависит только от \\(dp[i-1]\\) и \\(dp[i-2]\\) , нам не нужен весь массив dp для хранения ответов всех подзадач. Достаточно двух переменных, которые будут «перекатываться» вперед. Код имеет вид:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dp.py
        def climbing_stairs_dp_comp(n: int) -> int:\n    \"\"\"Подъем по лестнице: динамическое программирование с оптимизацией памяти\"\"\"\n    if n == 1 or n == 2:\n        return n\n    a, b = 1, 2\n    for _ in range(3, n + 1):\n        a, b = b, a + b\n    return b\n
        climbing_stairs_dp.cpp
        /* Подъем по лестнице: динамическое программирование с оптимизацией памяти */\nint climbingStairsDPComp(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    int a = 1, b = 2;\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
        climbing_stairs_dp.java
        /* Подъем по лестнице: динамическое программирование с оптимизацией памяти */\nint climbingStairsDPComp(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    int a = 1, b = 2;\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
        climbing_stairs_dp.cs
        /* Подъем по лестнице: динамическое программирование с оптимизацией памяти */\nint ClimbingStairsDPComp(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    int a = 1, b = 2;\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
        climbing_stairs_dp.go
        /* Подъем по лестнице: динамическое программирование с оптимизацией памяти */\nfunc climbingStairsDPComp(n int) int {\n    if n == 1 || n == 2 {\n        return n\n    }\n    a, b := 1, 2\n    // Переход состояний: постепенное решение больших подзадач через меньшие\n    for i := 3; i <= n; i++ {\n        a, b = b, a+b\n    }\n    return b\n}\n
        climbing_stairs_dp.swift
        /* Подъем по лестнице: динамическое программирование с оптимизацией памяти */\nfunc climbingStairsDPComp(n: Int) -> Int {\n    if n == 1 || n == 2 {\n        return n\n    }\n    var a = 1\n    var b = 2\n    for _ in 3 ... n {\n        (a, b) = (b, a + b)\n    }\n    return b\n}\n
        climbing_stairs_dp.js
        /* Подъем по лестнице: динамическое программирование с оптимизацией памяти */\nfunction climbingStairsDPComp(n) {\n    if (n === 1 || n === 2) return n;\n    let a = 1,\n        b = 2;\n    for (let i = 3; i <= n; i++) {\n        const tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
        climbing_stairs_dp.ts
        /* Подъем по лестнице: динамическое программирование с оптимизацией памяти */\nfunction climbingStairsDPComp(n: number): number {\n    if (n === 1 || n === 2) return n;\n    let a = 1,\n        b = 2;\n    for (let i = 3; i <= n; i++) {\n        const tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
        climbing_stairs_dp.dart
        /* Подъем по лестнице: динамическое программирование с оптимизацией памяти */\nint climbingStairsDPComp(int n) {\n  if (n == 1 || n == 2) return n;\n  int a = 1, b = 2;\n  for (int i = 3; i <= n; i++) {\n    int tmp = b;\n    b = a + b;\n    a = tmp;\n  }\n  return b;\n}\n
        climbing_stairs_dp.rs
        /* Подъем по лестнице: динамическое программирование с оптимизацией памяти */\nfn climbing_stairs_dp_comp(n: usize) -> i32 {\n    if n == 1 || n == 2 {\n        return n as i32;\n    }\n    let (mut a, mut b) = (1, 2);\n    for _ in 3..=n {\n        let tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    b\n}\n
        climbing_stairs_dp.c
        /* Подъем по лестнице: динамическое программирование с оптимизацией памяти */\nint climbingStairsDPComp(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    int a = 1, b = 2;\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
        climbing_stairs_dp.kt
        /* Подъем по лестнице: динамическое программирование с оптимизацией памяти */\nfun climbingStairsDPComp(n: Int): Int {\n    if (n == 1 || n == 2) return n\n    var a = 1\n    var b = 2\n    for (i in 3..n) {\n        val temp = b\n        b += a\n        a = temp\n    }\n    return b\n}\n
        climbing_stairs_dp.rb
        ### Подъем по лестнице: динамическое программирование с оптимизацией памяти ###\ndef climbing_stairs_dp_comp(n)\n  return n if n == 1 || n == 2\n\n  a, b = 1, 2\n  (3...(n + 1)).each { a, b = b, a + b }\n\n  b\nend\n
        Визуализация кода

        Во весь экран >

        Из кода видно, что после отказа от массива dp пространственная сложность уменьшается с \\(O(n)\\) до \\(O(1)\\) .

        Во многих задачах динамического программирования текущее состояние зависит лишь от ограниченного числа предыдущих состояний. Тогда можно сохранять только действительно нужные состояния и за счет «уменьшения размерности» экономить память. Этот прием оптимизации памяти называют «скользящими переменными» или «скользящим массивом».

        ","path":["Глава 14. Динамическое программирование","14.1   Первое знакомство с динамическим программированием"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/","level":1,"title":"14.4   Задача о рюкзаке 0-1","text":"

        Задача о рюкзаке является отличным примером для начала изучения динамического программирования и представляет собой одну из наиболее распространенных форм этой задачи. У нее существует множество вариантов, например задача о рюкзаке 0-1, задача о полном рюкзаке, задача о многократном рюкзаке и т.д.

        В этом разделе сначала разберем самый распространенный вариант - задачу о рюкзаке 0-1.

        Question

        Даны \\(n\\) предметов. Вес \\(i\\)-го предмета равен \\(wgt[i-1]\\) , стоимость равна \\(val[i-1]\\) . Также дан рюкзак вместимости \\(cap\\) . Каждый предмет можно выбрать только один раз. Найдите максимальную суммарную стоимость, которую можно поместить в рюкзак при заданной вместимости.

        Как видно на рисунке 14-17, поскольку номер предмета \\(i\\) начинается с \\(1\\) , а индексы массива начинаются с \\(0\\) , предмету \\(i\\) соответствуют вес \\(wgt[i-1]\\) и стоимость \\(val[i-1]\\) .

        Рисунок 14-17   Пример данных для задачи о рюкзаке 0-1

        Задачу о рюкзаке 0-1 можно рассматривать как процесс из \\(n\\) раундов принятия решений: для каждого предмета есть два решения - не класть его в рюкзак или положить в рюкзак. Поэтому задача удовлетворяет модели дерева решений.

        Цель задачи - найти «максимальную суммарную стоимость при ограниченной вместимости рюкзака», а это с большой вероятностью указывает на задачу динамического программирования.

        Шаг 1: продумать решения на каждом раунде, определить состояние и тем самым получить таблицу \\(dp\\)

        Для каждого предмета возможны два случая: не класть его в рюкзак, тогда вместимость не меняется. Или положить его в рюкзак, тогда оставшаяся вместимость уменьшается. Отсюда получается определение состояния: текущий номер предмета \\(i\\) и текущая вместимость рюкзака \\(c\\) , то есть состояние обозначается как \\([i, c]\\) .

        Подзадача, соответствующая состоянию \\([i, c]\\) , такова: максимальная стоимость, которую можно получить, используя первые \\(i\\) предметов и рюкзак вместимости \\(c\\). Ее решение обозначается через \\(dp[i, c]\\) .

        Искомым значением является \\(dp[n, cap]\\) , значит, нам нужна двумерная таблица \\(dp\\) размера \\((n+1) \\times (cap+1)\\) .

        Шаг 2: найти оптимальную подструктуру и на ее основе вывести уравнение перехода состояния

        После того как мы принимаем решение по предмету \\(i\\) , остается подзадача, связанная с первыми \\(i-1\\) предметами. Здесь возможны два случая.

        • Не класть предмет \\(i\\) : вместимость рюкзака не меняется, и состояние переходит в \\([i-1, c]\\) .
        • Положить предмет \\(i\\) : вместимость рюкзака уменьшается на \\(wgt[i-1]\\) , а стоимость увеличивается на \\(val[i-1]\\) , и состояние переходит в \\([i-1, c-wgt[i-1]]\\) .

        Этот анализ и раскрывает оптимальную подструктуру задачи: максимальная стоимость \\(dp[i, c]\\) равна лучшему из двух вариантов - не брать предмет \\(i\\) или взять предмет \\(i\\). Отсюда получается уравнение перехода состояния:

        \\[ dp[i, c] = \\max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1]) \\]

        Нужно учитывать, что если вес текущего предмета \\(wgt[i - 1]\\) превышает оставшуюся вместимость \\(c\\) , то предмет можно только не брать.

        Шаг 3: определить граничные условия и порядок переходов

        Когда предметов нет или вместимость рюкзака равна \\(0\\) , максимальная стоимость равна \\(0\\). То есть весь первый столбец \\(dp[i, 0]\\) и вся первая строка \\(dp[0, c]\\) заполняются нулями.

        Текущее состояние \\([i, c]\\) зависит от состояния сверху \\([i-1, c]\\) и состояния слева сверху \\([i-1, c-wgt[i-1]]\\) , поэтому достаточно двумя вложенными циклами пройти по всей таблице \\(dp\\) в прямом порядке.

        После этого анализа реализуем по порядку: полный перебор, поиск с мемоизацией и динамическое программирование.

        ","path":["Глава 14. Динамическое программирование","14.4   Задача о рюкзаке 0-1"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#1-1","level":3,"title":"1.   Метод 1: полный перебор","text":"

        Код поиска содержит следующие элементы.

        • Параметры рекурсии: состояние \\([i, c]\\) .
        • Возвращаемое значение: решение подзадачи \\(dp[i, c]\\) .
        • Условие завершения: когда номер предмета выходит за границу, то есть \\(i = 0\\) , или оставшаяся вместимость равна \\(0\\) , рекурсия завершается и возвращается стоимость \\(0\\) .
        • Обрезка: если вес текущего предмета превышает оставшуюся вместимость рюкзака, то можно только не класть этот предмет.
        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py
        def knapsack_dfs(wgt: list[int], val: list[int], i: int, c: int) -> int:\n    \"\"\"Рюкзак 0-1: полный перебор\"\"\"\n    # Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if i == 0 or c == 0:\n        return 0\n    # Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if wgt[i - 1] > c:\n        return knapsack_dfs(wgt, val, i - 1, c)\n    # Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    no = knapsack_dfs(wgt, val, i - 1, c)\n    yes = knapsack_dfs(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]\n    # Вернуть вариант с большей стоимостью из двух возможных\n    return max(no, yes)\n
        knapsack.cpp
        /* Рюкзак 0-1: полный перебор */\nint knapsackDFS(vector<int> &wgt, vector<int> &val, int i, int c) {\n    // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    int no = knapsackDFS(wgt, val, i - 1, c);\n    int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // Вернуть вариант с большей стоимостью из двух возможных\n    return max(no, yes);\n}\n
        knapsack.java
        /* Рюкзак 0-1: полный перебор */\nint knapsackDFS(int[] wgt, int[] val, int i, int c) {\n    // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    int no = knapsackDFS(wgt, val, i - 1, c);\n    int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // Вернуть вариант с большей стоимостью из двух возможных\n    return Math.max(no, yes);\n}\n
        knapsack.cs
        /* Рюкзак 0-1: полный перебор */\nint KnapsackDFS(int[] weight, int[] val, int i, int c) {\n    // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if (weight[i - 1] > c) {\n        return KnapsackDFS(weight, val, i - 1, c);\n    }\n    // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    int no = KnapsackDFS(weight, val, i - 1, c);\n    int yes = KnapsackDFS(weight, val, i - 1, c - weight[i - 1]) + val[i - 1];\n    // Вернуть вариант с большей стоимостью из двух возможных\n    return Math.Max(no, yes);\n}\n
        knapsack.go
        /* Рюкзак 0-1: полный перебор */\nfunc knapsackDFS(wgt, val []int, i, c int) int {\n    // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if i == 0 || c == 0 {\n        return 0\n    }\n    // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if wgt[i-1] > c {\n        return knapsackDFS(wgt, val, i-1, c)\n    }\n    // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    no := knapsackDFS(wgt, val, i-1, c)\n    yes := knapsackDFS(wgt, val, i-1, c-wgt[i-1]) + val[i-1]\n    // Вернуть вариант с большей стоимостью из двух возможных\n    return int(math.Max(float64(no), float64(yes)))\n}\n
        knapsack.swift
        /* Рюкзак 0-1: полный перебор */\nfunc knapsackDFS(wgt: [Int], val: [Int], i: Int, c: Int) -> Int {\n    // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if i == 0 || c == 0 {\n        return 0\n    }\n    // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if wgt[i - 1] > c {\n        return knapsackDFS(wgt: wgt, val: val, i: i - 1, c: c)\n    }\n    // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    let no = knapsackDFS(wgt: wgt, val: val, i: i - 1, c: c)\n    let yes = knapsackDFS(wgt: wgt, val: val, i: i - 1, c: c - wgt[i - 1]) + val[i - 1]\n    // Вернуть вариант с большей стоимостью из двух возможных\n    return max(no, yes)\n}\n
        knapsack.js
        /* Рюкзак 0-1: полный перебор */\nfunction knapsackDFS(wgt, val, i, c) {\n    // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if (i === 0 || c === 0) {\n        return 0;\n    }\n    // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    const no = knapsackDFS(wgt, val, i - 1, c);\n    const yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // Вернуть вариант с большей стоимостью из двух возможных\n    return Math.max(no, yes);\n}\n
        knapsack.ts
        /* Рюкзак 0-1: полный перебор */\nfunction knapsackDFS(\n    wgt: Array<number>,\n    val: Array<number>,\n    i: number,\n    c: number\n): number {\n    // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if (i === 0 || c === 0) {\n        return 0;\n    }\n    // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    const no = knapsackDFS(wgt, val, i - 1, c);\n    const yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // Вернуть вариант с большей стоимостью из двух возможных\n    return Math.max(no, yes);\n}\n
        knapsack.dart
        /* Рюкзак 0-1: полный перебор */\nint knapsackDFS(List<int> wgt, List<int> val, int i, int c) {\n  // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n  if (i == 0 || c == 0) {\n    return 0;\n  }\n  // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n  if (wgt[i - 1] > c) {\n    return knapsackDFS(wgt, val, i - 1, c);\n  }\n  // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n  int no = knapsackDFS(wgt, val, i - 1, c);\n  int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n  // Вернуть вариант с большей стоимостью из двух возможных\n  return max(no, yes);\n}\n
        knapsack.rs
        /* Рюкзак 0-1: полный перебор */\nfn knapsack_dfs(wgt: &[i32], val: &[i32], i: usize, c: usize) -> i32 {\n    // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if i == 0 || c == 0 {\n        return 0;\n    }\n    // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if wgt[i - 1] > c as i32 {\n        return knapsack_dfs(wgt, val, i - 1, c);\n    }\n    // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    let no = knapsack_dfs(wgt, val, i - 1, c);\n    let yes = knapsack_dfs(wgt, val, i - 1, c - wgt[i - 1] as usize) + val[i - 1];\n    // Вернуть вариант с большей стоимостью из двух возможных\n    std::cmp::max(no, yes)\n}\n
        knapsack.c
        /* Рюкзак 0-1: полный перебор */\nint knapsackDFS(int wgt[], int val[], int i, int c) {\n    // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    int no = knapsackDFS(wgt, val, i - 1, c);\n    int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // Вернуть вариант с большей стоимостью из двух возможных\n    return myMax(no, yes);\n}\n
        knapsack.kt
        /* Рюкзак 0-1: полный перебор */\nfun knapsackDFS(\n    wgt: IntArray,\n    _val: IntArray,\n    i: Int,\n    c: Int\n): Int {\n    // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if (i == 0 || c == 0) {\n        return 0\n    }\n    // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, _val, i - 1, c)\n    }\n    // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    val no = knapsackDFS(wgt, _val, i - 1, c)\n    val yes = knapsackDFS(wgt, _val, i - 1, c - wgt[i - 1]) + _val[i - 1]\n    // Вернуть вариант с большей стоимостью из двух возможных\n    return max(no, yes)\n}\n
        knapsack.rb
        ### Рюкзак 0-1: полный перебор ###\ndef knapsack_dfs(wgt, val, i, c)\n  # Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n  return 0 if i == 0 || c == 0\n  # Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n  return knapsack_dfs(wgt, val, i - 1, c) if wgt[i - 1] > c\n  # Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n  no = knapsack_dfs(wgt, val, i - 1, c)\n  yes = knapsack_dfs(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]\n  # Вернуть вариант с большей стоимостью из двух возможных\n  [no, yes].max\nend\n
        Визуализация кода

        Во весь экран >

        Как показано на рисунке 14-18, поскольку каждый предмет создает две ветви поиска - «не брать» и «брать», временная сложность равна \\(O(2^n)\\) .

        Посмотрев на дерево рекурсии, легко заметить наличие перекрывающихся подзадач, например \\(dp[1, 10]\\) и подобных. Когда число предметов растет, вместимость рюкзака велика, а особенно когда много предметов с одинаковым весом, количество перекрывающихся подзадач быстро увеличивается.

        Рисунок 14-18   Дерево полного перебора для задачи о рюкзаке 0-1

        ","path":["Глава 14. Динамическое программирование","14.4   Задача о рюкзаке 0-1"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#2-2","level":3,"title":"2.   Метод 2: мемоизация","text":"

        Чтобы каждая перекрывающаяся подзадача вычислялась только один раз, используем таблицу памяти mem для хранения решений подзадач, где mem[i][c] соответствует \\(dp[i, c]\\) .

        После введения мемоизации временная сложность определяется числом подзадач , то есть равна \\(O(n \\times cap)\\) . Код выглядит так:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py
        def knapsack_dfs_mem(\n    wgt: list[int], val: list[int], mem: list[list[int]], i: int, c: int\n) -> int:\n    \"\"\"Рюкзак 0-1: поиск с мемоизацией\"\"\"\n    # Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if i == 0 or c == 0:\n        return 0\n    # Если запись уже есть, вернуть сразу\n    if mem[i][c] != -1:\n        return mem[i][c]\n    # Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if wgt[i - 1] > c:\n        return knapsack_dfs_mem(wgt, val, mem, i - 1, c)\n    # Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    no = knapsack_dfs_mem(wgt, val, mem, i - 1, c)\n    yes = knapsack_dfs_mem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1]\n    # Сохранить и вернуть вариант с большей стоимостью из двух решений\n    mem[i][c] = max(no, yes)\n    return mem[i][c]\n
        knapsack.cpp
        /* Рюкзак 0-1: поиск с мемоизацией */\nint knapsackDFSMem(vector<int> &wgt, vector<int> &val, vector<vector<int>> &mem, int i, int c) {\n    // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // Если запись уже есть, вернуть сразу\n    if (mem[i][c] != -1) {\n        return mem[i][c];\n    }\n    // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, mem, i - 1, c);\n    }\n    // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    int no = knapsackDFSMem(wgt, val, mem, i - 1, c);\n    int yes = knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // Сохранить и вернуть вариант с большей стоимостью из двух решений\n    mem[i][c] = max(no, yes);\n    return mem[i][c];\n}\n
        knapsack.java
        /* Рюкзак 0-1: поиск с мемоизацией */\nint knapsackDFSMem(int[] wgt, int[] val, int[][] mem, int i, int c) {\n    // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // Если запись уже есть, вернуть сразу\n    if (mem[i][c] != -1) {\n        return mem[i][c];\n    }\n    // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, mem, i - 1, c);\n    }\n    // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    int no = knapsackDFSMem(wgt, val, mem, i - 1, c);\n    int yes = knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // Сохранить и вернуть вариант с большей стоимостью из двух решений\n    mem[i][c] = Math.max(no, yes);\n    return mem[i][c];\n}\n
        knapsack.cs
        /* Рюкзак 0-1: поиск с мемоизацией */\nint KnapsackDFSMem(int[] weight, int[] val, int[][] mem, int i, int c) {\n    // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // Если запись уже есть, вернуть сразу\n    if (mem[i][c] != -1) {\n        return mem[i][c];\n    }\n    // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if (weight[i - 1] > c) {\n        return KnapsackDFSMem(weight, val, mem, i - 1, c);\n    }\n    // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    int no = KnapsackDFSMem(weight, val, mem, i - 1, c);\n    int yes = KnapsackDFSMem(weight, val, mem, i - 1, c - weight[i - 1]) + val[i - 1];\n    // Сохранить и вернуть вариант с большей стоимостью из двух решений\n    mem[i][c] = Math.Max(no, yes);\n    return mem[i][c];\n}\n
        knapsack.go
        /* Рюкзак 0-1: поиск с мемоизацией */\nfunc knapsackDFSMem(wgt, val []int, mem [][]int, i, c int) int {\n    // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if i == 0 || c == 0 {\n        return 0\n    }\n    // Если запись уже есть, вернуть сразу\n    if mem[i][c] != -1 {\n        return mem[i][c]\n    }\n    // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if wgt[i-1] > c {\n        return knapsackDFSMem(wgt, val, mem, i-1, c)\n    }\n    // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    no := knapsackDFSMem(wgt, val, mem, i-1, c)\n    yes := knapsackDFSMem(wgt, val, mem, i-1, c-wgt[i-1]) + val[i-1]\n    // Вернуть вариант с большей стоимостью из двух возможных\n    mem[i][c] = int(math.Max(float64(no), float64(yes)))\n    return mem[i][c]\n}\n
        knapsack.swift
        /* Рюкзак 0-1: поиск с мемоизацией */\nfunc knapsackDFSMem(wgt: [Int], val: [Int], mem: inout [[Int]], i: Int, c: Int) -> Int {\n    // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if i == 0 || c == 0 {\n        return 0\n    }\n    // Если запись уже есть, вернуть сразу\n    if mem[i][c] != -1 {\n        return mem[i][c]\n    }\n    // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if wgt[i - 1] > c {\n        return knapsackDFSMem(wgt: wgt, val: val, mem: &mem, i: i - 1, c: c)\n    }\n    // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    let no = knapsackDFSMem(wgt: wgt, val: val, mem: &mem, i: i - 1, c: c)\n    let yes = knapsackDFSMem(wgt: wgt, val: val, mem: &mem, i: i - 1, c: c - wgt[i - 1]) + val[i - 1]\n    // Сохранить и вернуть вариант с большей стоимостью из двух решений\n    mem[i][c] = max(no, yes)\n    return mem[i][c]\n}\n
        knapsack.js
        /* Рюкзак 0-1: поиск с мемоизацией */\nfunction knapsackDFSMem(wgt, val, mem, i, c) {\n    // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if (i === 0 || c === 0) {\n        return 0;\n    }\n    // Если запись уже есть, вернуть сразу\n    if (mem[i][c] !== -1) {\n        return mem[i][c];\n    }\n    // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, mem, i - 1, c);\n    }\n    // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    const no = knapsackDFSMem(wgt, val, mem, i - 1, c);\n    const yes =\n        knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // Сохранить и вернуть вариант с большей стоимостью из двух решений\n    mem[i][c] = Math.max(no, yes);\n    return mem[i][c];\n}\n
        knapsack.ts
        /* Рюкзак 0-1: поиск с мемоизацией */\nfunction knapsackDFSMem(\n    wgt: Array<number>,\n    val: Array<number>,\n    mem: Array<Array<number>>,\n    i: number,\n    c: number\n): number {\n    // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if (i === 0 || c === 0) {\n        return 0;\n    }\n    // Если запись уже есть, вернуть сразу\n    if (mem[i][c] !== -1) {\n        return mem[i][c];\n    }\n    // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, mem, i - 1, c);\n    }\n    // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    const no = knapsackDFSMem(wgt, val, mem, i - 1, c);\n    const yes =\n        knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // Сохранить и вернуть вариант с большей стоимостью из двух решений\n    mem[i][c] = Math.max(no, yes);\n    return mem[i][c];\n}\n
        knapsack.dart
        /* Рюкзак 0-1: поиск с мемоизацией */\nint knapsackDFSMem(\n  List<int> wgt,\n  List<int> val,\n  List<List<int>> mem,\n  int i,\n  int c,\n) {\n  // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n  if (i == 0 || c == 0) {\n    return 0;\n  }\n  // Если запись уже есть, вернуть сразу\n  if (mem[i][c] != -1) {\n    return mem[i][c];\n  }\n  // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n  if (wgt[i - 1] > c) {\n    return knapsackDFSMem(wgt, val, mem, i - 1, c);\n  }\n  // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n  int no = knapsackDFSMem(wgt, val, mem, i - 1, c);\n  int yes = knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n  // Сохранить и вернуть вариант с большей стоимостью из двух решений\n  mem[i][c] = max(no, yes);\n  return mem[i][c];\n}\n
        knapsack.rs
        /* Рюкзак 0-1: поиск с мемоизацией */\nfn knapsack_dfs_mem(wgt: &[i32], val: &[i32], mem: &mut Vec<Vec<i32>>, i: usize, c: usize) -> i32 {\n    // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if i == 0 || c == 0 {\n        return 0;\n    }\n    // Если запись уже есть, вернуть сразу\n    if mem[i][c] != -1 {\n        return mem[i][c];\n    }\n    // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if wgt[i - 1] > c as i32 {\n        return knapsack_dfs_mem(wgt, val, mem, i - 1, c);\n    }\n    // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    let no = knapsack_dfs_mem(wgt, val, mem, i - 1, c);\n    let yes = knapsack_dfs_mem(wgt, val, mem, i - 1, c - wgt[i - 1] as usize) + val[i - 1];\n    // Сохранить и вернуть вариант с большей стоимостью из двух решений\n    mem[i][c] = std::cmp::max(no, yes);\n    mem[i][c]\n}\n
        knapsack.c
        /* Рюкзак 0-1: поиск с мемоизацией */\nint knapsackDFSMem(int wgt[], int val[], int memCols, int **mem, int i, int c) {\n    // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // Если запись уже есть, вернуть сразу\n    if (mem[i][c] != -1) {\n        return mem[i][c];\n    }\n    // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, memCols, mem, i - 1, c);\n    }\n    // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    int no = knapsackDFSMem(wgt, val, memCols, mem, i - 1, c);\n    int yes = knapsackDFSMem(wgt, val, memCols, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // Сохранить и вернуть вариант с большей стоимостью из двух решений\n    mem[i][c] = myMax(no, yes);\n    return mem[i][c];\n}\n
        knapsack.kt
        /* Рюкзак 0-1: поиск с мемоизацией */\nfun knapsackDFSMem(\n    wgt: IntArray,\n    _val: IntArray,\n    mem: Array<IntArray>,\n    i: Int,\n    c: Int\n): Int {\n    // Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n    if (i == 0 || c == 0) {\n        return 0\n    }\n    // Если запись уже есть, вернуть сразу\n    if (mem[i][c] != -1) {\n        return mem[i][c]\n    }\n    // Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, _val, mem, i - 1, c)\n    }\n    // Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n    val no = knapsackDFSMem(wgt, _val, mem, i - 1, c)\n    val yes = knapsackDFSMem(wgt, _val, mem, i - 1, c - wgt[i - 1]) + _val[i - 1]\n    // Сохранить и вернуть вариант с большей стоимостью из двух решений\n    mem[i][c] = max(no, yes)\n    return mem[i][c]\n}\n
        knapsack.rb
        ### Рюкзак 0-1: поиск с мемоизацией ###\ndef knapsack_dfs_mem(wgt, val, mem, i, c)\n  # Если все предметы уже рассмотрены или в рюкзаке не осталось места, вернуть стоимость 0\n  return 0 if i == 0 || c == 0\n  # Если запись уже есть, вернуть сразу\n  return mem[i][c] if mem[i][c] != -1\n  # Если вместимость рюкзака превышена, можно только не класть предмет в рюкзак\n  return knapsack_dfs_mem(wgt, val, mem, i - 1, c) if wgt[i - 1] > c\n  # Вычислить максимальную стоимость для случаев, когда предмет i не кладут и кладут\n  no = knapsack_dfs_mem(wgt, val, mem, i - 1, c)\n  yes = knapsack_dfs_mem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1]\n  # Сохранить и вернуть вариант с большей стоимостью из двух решений\n  mem[i][c] = [no, yes].max\nend\n
        Визуализация кода

        Во весь экран >

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

        Рисунок 14-19   Дерево поиска с мемоизацией для задачи о рюкзаке 0-1

        ","path":["Глава 14. Динамическое программирование","14.4   Задача о рюкзаке 0-1"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#3-3","level":3,"title":"3.   Метод 3: динамическое программирование","text":"

        По своей сути динамическое программирование здесь - это процесс последовательного заполнения таблицы \\(dp\\) в соответствии с переходами состояний. Код приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py
        def knapsack_dp(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"Рюкзак 0-1: динамическое программирование\"\"\"\n    n = len(wgt)\n    # Инициализация таблицы dp\n    dp = [[0] * (cap + 1) for _ in range(n + 1)]\n    # Переход состояний\n    for i in range(1, n + 1):\n        for c in range(1, cap + 1):\n            if wgt[i - 1] > c:\n                # Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i][c] = dp[i - 1][c]\n            else:\n                # Большее из двух решений: не брать или взять предмет i\n                dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1])\n    return dp[n][cap]\n
        knapsack.cpp
        /* Рюкзак 0-1: динамическое программирование */\nint knapsackDP(vector<int> &wgt, vector<int> &val, int cap) {\n    int n = wgt.size();\n    // Инициализация таблицы dp\n    vector<vector<int>> dp(n + 1, vector<int>(cap + 1, 0));\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
        knapsack.java
        /* Рюкзак 0-1: динамическое программирование */\nint knapsackDP(int[] wgt, int[] val, int cap) {\n    int n = wgt.length;\n    // Инициализация таблицы dp\n    int[][] dp = new int[n + 1][cap + 1];\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[i][c] = Math.max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
        knapsack.cs
        /* Рюкзак 0-1: динамическое программирование */\nint KnapsackDP(int[] weight, int[] val, int cap) {\n    int n = weight.Length;\n    // Инициализация таблицы dp\n    int[,] dp = new int[n + 1, cap + 1];\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (weight[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i, c] = dp[i - 1, c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[i, c] = Math.Max(dp[i - 1, c - weight[i - 1]] + val[i - 1], dp[i - 1, c]);\n            }\n        }\n    }\n    return dp[n, cap];\n}\n
        knapsack.go
        /* Рюкзак 0-1: динамическое программирование */\nfunc knapsackDP(wgt, val []int, cap int) int {\n    n := len(wgt)\n    // Инициализация таблицы dp\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, cap+1)\n    }\n    // Переход состояний\n    for i := 1; i <= n; i++ {\n        for c := 1; c <= cap; c++ {\n            if wgt[i-1] > c {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i][c] = dp[i-1][c]\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[i][c] = int(math.Max(float64(dp[i-1][c]), float64(dp[i-1][c-wgt[i-1]]+val[i-1])))\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
        knapsack.swift
        /* Рюкзак 0-1: динамическое программирование */\nfunc knapsackDP(wgt: [Int], val: [Int], cap: Int) -> Int {\n    let n = wgt.count\n    // Инициализация таблицы dp\n    var dp = Array(repeating: Array(repeating: 0, count: cap + 1), count: n + 1)\n    // Переход состояний\n    for i in 1 ... n {\n        for c in 1 ... cap {\n            if wgt[i - 1] > c {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i][c] = dp[i - 1][c]\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1])\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
        knapsack.js
        /* Рюкзак 0-1: динамическое программирование */\nfunction knapsackDP(wgt, val, cap) {\n    const n = wgt.length;\n    // Инициализация таблицы dp\n    const dp = Array(n + 1)\n        .fill(0)\n        .map(() => Array(cap + 1).fill(0));\n    // Переход состояний\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[i][c] = Math.max(\n                    dp[i - 1][c],\n                    dp[i - 1][c - wgt[i - 1]] + val[i - 1]\n                );\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
        knapsack.ts
        /* Рюкзак 0-1: динамическое программирование */\nfunction knapsackDP(\n    wgt: Array<number>,\n    val: Array<number>,\n    cap: number\n): number {\n    const n = wgt.length;\n    // Инициализация таблицы dp\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: cap + 1 }, () => 0)\n    );\n    // Переход состояний\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[i][c] = Math.max(\n                    dp[i - 1][c],\n                    dp[i - 1][c - wgt[i - 1]] + val[i - 1]\n                );\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
        knapsack.dart
        /* Рюкзак 0-1: динамическое программирование */\nint knapsackDP(List<int> wgt, List<int> val, int cap) {\n  int n = wgt.length;\n  // Инициализация таблицы dp\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(cap + 1, 0));\n  // Переход состояний\n  for (int i = 1; i <= n; i++) {\n    for (int c = 1; c <= cap; c++) {\n      if (wgt[i - 1] > c) {\n        // Если вместимость рюкзака превышена, предмет i не выбирать\n        dp[i][c] = dp[i - 1][c];\n      } else {\n        // Большее из двух решений: не брать или взять предмет i\n        dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);\n      }\n    }\n  }\n  return dp[n][cap];\n}\n
        knapsack.rs
        /* Рюкзак 0-1: динамическое программирование */\nfn knapsack_dp(wgt: &[i32], val: &[i32], cap: usize) -> i32 {\n    let n = wgt.len();\n    // Инициализация таблицы dp\n    let mut dp = vec![vec![0; cap + 1]; n + 1];\n    // Переход состояний\n    for i in 1..=n {\n        for c in 1..=cap {\n            if wgt[i - 1] > c as i32 {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[i][c] = std::cmp::max(\n                    dp[i - 1][c],\n                    dp[i - 1][c - wgt[i - 1] as usize] + val[i - 1],\n                );\n            }\n        }\n    }\n    dp[n][cap]\n}\n
        knapsack.c
        /* Рюкзак 0-1: динамическое программирование */\nint knapsackDP(int wgt[], int val[], int cap, int wgtSize) {\n    int n = wgtSize;\n    // Инициализация таблицы dp\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(cap + 1, sizeof(int));\n    }\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[i][c] = myMax(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    int res = dp[n][cap];\n    // Освободить память\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    return res;\n}\n
        knapsack.kt
        /* Рюкзак 0-1: динамическое программирование */\nfun knapsackDP(wgt: IntArray, _val: IntArray, cap: Int): Int {\n    val n = wgt.size\n    // Инициализация таблицы dp\n    val dp = Array(n + 1) { IntArray(cap + 1) }\n    // Переход состояний\n    for (i in 1..n) {\n        for (c in 1..cap) {\n            if (wgt[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i][c] = dp[i - 1][c]\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + _val[i - 1])\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
        knapsack.rb
        ### Рюкзак 0-1: динамическое программирование ###\ndef knapsack_dp(wgt, val, cap)\n  n = wgt.length\n  # Инициализация таблицы dp\n  dp = Array.new(n + 1) { Array.new(cap + 1, 0) }\n  # Переход состояний\n  for i in 1...(n + 1)\n    for c in 1...(cap + 1)\n      if wgt[i - 1] > c\n        # Если вместимость рюкзака превышена, предмет i не выбирать\n        dp[i][c] = dp[i - 1][c]\n      else\n        # Большее из двух решений: не брать или взять предмет i\n        dp[i][c] = [dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]].max\n      end\n    end\n  end\n  dp[n][cap]\nend\n
        Визуализация кода

        Во весь экран >

        Как показано на рисунке 14-20, и временная сложность, и пространственная сложность определяются размером массива dp , то есть равны \\(O(n \\times cap)\\) .

        <1><2><3><4><5><6><7><8><9><10><11><12><13><14>

        Рисунок 14-20   Процесс динамического программирования для задачи о рюкзаке 0-1

        ","path":["Глава 14. Динамическое программирование","14.4   Задача о рюкзаке 0-1"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#4","level":3,"title":"4.   Оптимизация пространства","text":"

        Поскольку каждое состояние зависит только от состояния в предыдущей строке, можно использовать два массива, которые будут продвигаться вперед по очереди, и тем самым уменьшить пространственную сложность с \\(O(n^2)\\) до \\(O(n)\\) .

        Если пойти дальше, можно спросить: можно ли оптимизировать память так, чтобы использовать только один массив? Наблюдение показывает, что каждое состояние зависит от клетки прямо сверху и клетки слева сверху. Предположим, что у нас есть только один массив, и в момент начала обхода строки \\(i\\) он еще хранит состояния строки \\(i-1\\) .

        • Если обходить массив слева направо, то к моменту вычисления \\(dp[i, j]\\) значения слева сверху \\(dp[i-1, 1]\\) ~ \\(dp[i-1, j-1]\\) могут уже быть перезаписаны, и правильный результат перехода состояния получить не удастся.
        • Если же обходить массив справа налево, проблема перезаписи не возникает, и переход состояния вычисляется корректно.

        На рисунке 14-21 показан процесс перехода от строки \\(i = 1\\) к строке \\(i = 2\\) при использовании одного массива. С его помощью удобно понять различие между прямым и обратным обходом.

        <1><2><3><4><5><6>

        Рисунок 14-21   Процесс динамического программирования после оптимизации памяти для рюкзака 0-1

        В коде для этого достаточно удалить первое измерение массива dp , а внутренний цикл заменить на обратный обход:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py
        def knapsack_dp_comp(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"Рюкзак 0-1: динамическое программирование с оптимизацией памяти\"\"\"\n    n = len(wgt)\n    # Инициализация таблицы dp\n    dp = [0] * (cap + 1)\n    # Переход состояний\n    for i in range(1, n + 1):\n        # Обход в обратном порядке\n        for c in range(cap, 0, -1):\n            if wgt[i - 1] > c:\n                # Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[c] = dp[c]\n            else:\n                # Большее из двух решений: не брать или взять предмет i\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])\n    return dp[cap]\n
        knapsack.cpp
        /* Рюкзак 0-1: динамическое программирование с оптимизацией памяти */\nint knapsackDPComp(vector<int> &wgt, vector<int> &val, int cap) {\n    int n = wgt.size();\n    // Инициализация таблицы dp\n    vector<int> dp(cap + 1, 0);\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        // Обход в обратном порядке\n        for (int c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
        knapsack.java
        /* Рюкзак 0-1: динамическое программирование с оптимизацией памяти */\nint knapsackDPComp(int[] wgt, int[] val, int cap) {\n    int n = wgt.length;\n    // Инициализация таблицы dp\n    int[] dp = new int[cap + 1];\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        // Обход в обратном порядке\n        for (int c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
        knapsack.cs
        /* Рюкзак 0-1: динамическое программирование с оптимизацией памяти */\nint KnapsackDPComp(int[] weight, int[] val, int cap) {\n    int n = weight.Length;\n    // Инициализация таблицы dp\n    int[] dp = new int[cap + 1];\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        // Обход в обратном порядке\n        for (int c = cap; c > 0; c--) {\n            if (weight[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[c] = dp[c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[c] = Math.Max(dp[c], dp[c - weight[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
        knapsack.go
        /* Рюкзак 0-1: динамическое программирование с оптимизацией памяти */\nfunc knapsackDPComp(wgt, val []int, cap int) int {\n    n := len(wgt)\n    // Инициализация таблицы dp\n    dp := make([]int, cap+1)\n    // Переход состояний\n    for i := 1; i <= n; i++ {\n        // Обход в обратном порядке\n        for c := cap; c >= 1; c-- {\n            if wgt[i-1] <= c {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[c] = int(math.Max(float64(dp[c]), float64(dp[c-wgt[i-1]]+val[i-1])))\n            }\n        }\n    }\n    return dp[cap]\n}\n
        knapsack.swift
        /* Рюкзак 0-1: динамическое программирование с оптимизацией памяти */\nfunc knapsackDPComp(wgt: [Int], val: [Int], cap: Int) -> Int {\n    let n = wgt.count\n    // Инициализация таблицы dp\n    var dp = Array(repeating: 0, count: cap + 1)\n    // Переход состояний\n    for i in 1 ... n {\n        // Обход в обратном порядке\n        for c in (1 ... cap).reversed() {\n            if wgt[i - 1] <= c {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])\n            }\n        }\n    }\n    return dp[cap]\n}\n
        knapsack.js
        /* Рюкзак 0-1: динамическое программирование с оптимизацией памяти */\nfunction knapsackDPComp(wgt, val, cap) {\n    const n = wgt.length;\n    // Инициализация таблицы dp\n    const dp = Array(cap + 1).fill(0);\n    // Переход состояний\n    for (let i = 1; i <= n; i++) {\n        // Обход в обратном порядке\n        for (let c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
        knapsack.ts
        /* Рюкзак 0-1: динамическое программирование с оптимизацией памяти */\nfunction knapsackDPComp(\n    wgt: Array<number>,\n    val: Array<number>,\n    cap: number\n): number {\n    const n = wgt.length;\n    // Инициализация таблицы dp\n    const dp = Array(cap + 1).fill(0);\n    // Переход состояний\n    for (let i = 1; i <= n; i++) {\n        // Обход в обратном порядке\n        for (let c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
        knapsack.dart
        /* Рюкзак 0-1: динамическое программирование с оптимизацией памяти */\nint knapsackDPComp(List<int> wgt, List<int> val, int cap) {\n  int n = wgt.length;\n  // Инициализация таблицы dp\n  List<int> dp = List.filled(cap + 1, 0);\n  // Переход состояний\n  for (int i = 1; i <= n; i++) {\n    // Обход в обратном порядке\n    for (int c = cap; c >= 1; c--) {\n      if (wgt[i - 1] <= c) {\n        // Большее из двух решений: не брать или взять предмет i\n        dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n      }\n    }\n  }\n  return dp[cap];\n}\n
        knapsack.rs
        /* Рюкзак 0-1: динамическое программирование с оптимизацией памяти */\nfn knapsack_dp_comp(wgt: &[i32], val: &[i32], cap: usize) -> i32 {\n    let n = wgt.len();\n    // Инициализация таблицы dp\n    let mut dp = vec![0; cap + 1];\n    // Переход состояний\n    for i in 1..=n {\n        // Обход в обратном порядке\n        for c in (1..=cap).rev() {\n            if wgt[i - 1] <= c as i32 {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[c] = std::cmp::max(dp[c], dp[c - wgt[i - 1] as usize] + val[i - 1]);\n            }\n        }\n    }\n    dp[cap]\n}\n
        knapsack.c
        /* Рюкзак 0-1: динамическое программирование с оптимизацией памяти */\nint knapsackDPComp(int wgt[], int val[], int cap, int wgtSize) {\n    int n = wgtSize;\n    // Инициализация таблицы dp\n    int *dp = calloc(cap + 1, sizeof(int));\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        // Обход в обратном порядке\n        for (int c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[c] = myMax(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    int res = dp[cap];\n    // Освободить память\n    free(dp);\n    return res;\n}\n
        knapsack.kt
        /* Рюкзак 0-1: динамическое программирование с оптимизацией памяти */\nfun knapsackDPComp(wgt: IntArray, _val: IntArray, cap: Int): Int {\n    val n = wgt.size\n    // Инициализация таблицы dp\n    val dp = IntArray(cap + 1)\n    // Переход состояний\n    for (i in 1..n) {\n        // Обход в обратном порядке\n        for (c in cap downTo 1) {\n            if (wgt[i - 1] <= c) {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + _val[i - 1])\n            }\n        }\n    }\n    return dp[cap]\n}\n
        knapsack.rb
        ### Рюкзак 0-1: динамическое программирование с оптимизацией памяти ###\ndef knapsack_dp_comp(wgt, val, cap)\n  n = wgt.length\n  # Инициализация таблицы dp\n  dp = Array.new(cap + 1, 0)\n  # Переход состояний\n  for i in 1...(n + 1)\n    # Обход в обратном порядке\n    for c in cap.downto(1)\n      if wgt[i - 1] > c\n        # Если вместимость рюкзака превышена, предмет i не выбирать\n        dp[c] = dp[c]\n      else\n        # Большее из двух решений: не брать или взять предмет i\n        dp[c] = [dp[c], dp[c - wgt[i - 1]] + val[i - 1]].max\n      end\n    end\n  end\n  dp[cap]\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 14. Динамическое программирование","14.4   Задача о рюкзаке 0-1"],"tags":[]},{"location":"chapter_dynamic_programming/summary/","level":1,"title":"14.7   Резюме","text":"","path":["Глава 14. Динамическое программирование","14.7   Резюме"],"tags":[]},{"location":"chapter_dynamic_programming/summary/#1","level":3,"title":"1.   Ключевые выводы","text":"
        • Динамическое программирование раскладывает задачу на подзадачи и повышает вычислительную эффективность за счет хранения решений этих подзадач и устранения повторных вычислений.
        • Если не учитывать затраты времени, то любую задачу динамического программирования можно решить с помощью поиска с возвратом (полного перебора), однако в дереве рекурсии возникает множество перекрывающихся подзадач, из-за чего эффективность крайне низка. После введения таблицы памяти можно хранить решения всех уже вычисленных подзадач и гарантировать, что каждая перекрывающаяся подзадача будет вычисляться только один раз.
        • Поиск с мемоизацией - это рекурсивный метод «сверху вниз», а соответствующее ему динамическое программирование - это итеративный метод «снизу вверх», похожий на заполнение таблицы. Поскольку текущее состояние обычно зависит только от части локальных состояний, можно убрать одно измерение таблицы \\(dp\\) и тем самым снизить пространственную сложность.
        • Разложение на подзадачи - это общий алгоритмический подход, но в методе «разделяй и властвуй», динамическом программировании и поиске с возвратом он имеет разные свойства.
        • Для задач динамического программирования характерны три главных свойства: перекрывающиеся подзадачи, оптимальная подструктура и отсутствие последствий.
        • Если оптимальное решение исходной задачи можно построить из оптимальных решений подзадач, то задача обладает оптимальной подструктурой.
        • Отсутствие последствий означает, что для данного состояния его дальнейшее развитие определяется только этим состоянием и не зависит от всех прошлых состояний. Многие задачи комбинаторной оптимизации этим свойством не обладают и потому не могут эффективно решаться с помощью динамического программирования.

        Задачи о рюкзаке

        • Задача о рюкзаке - один из самых типичных классов задач динамического программирования. Она включает варианты 0-1 рюкзака, полного рюкзака, многократного рюкзака и другие.
        • В задаче о рюкзаке 0-1 состояние определяется как максимальная стоимость первых \\(i\\) предметов в рюкзаке вместимости \\(c\\) . Рассматривая два решения - не брать предмет и брать предмет, - можно получить оптимальную подструктуру и вывести уравнение перехода состояния. При оптимизации памяти, поскольку каждое состояние зависит от значения сверху и слева сверху, внутренний цикл нужно выполнять в обратном порядке, чтобы не перезаписать нужное значение.
        • В задаче о полном рюкзаке число экземпляров каждого предмета не ограничено, поэтому при выборе предмета переход состояния отличается от варианта 0-1. Поскольку состояние зависит от значения сверху и слева, после оптимизации памяти внутренний цикл следует выполнять в прямом порядке.
        • Задача о размене монет - это вариант задачи о полном рюкзаке. Здесь вместо «максимальной стоимости» ищется «минимальное число монет», поэтому в уравнении перехода \\(\\max()\\) заменяется на \\(\\min()\\) . Кроме того, вместо условия «не превышать вместимость рюкзака» нужно ровно набрать целевую сумму, поэтому значение \\(amt + 1\\) используется как обозначение недопустимого решения «сумму набрать нельзя».
        • В задаче о размене монет II вместо «минимального числа монет» требуется найти «число комбинаций монет», поэтому в уравнении перехода оператор \\(\\min()\\) заменяется на суммирование.

        Задача о расстоянии редактирования

        • Расстояние редактирования (расстояние Левенштейна) используется для измерения сходства двух строк и определяется как минимальное число операций редактирования, необходимых для преобразования одной строки в другую. Допустимые операции - вставка, удаление и замена.
        • В задаче о расстоянии редактирования состояние определяется как минимальное число шагов редактирования, необходимых для преобразования первых \\(i\\) символов строки \\(s\\) в первые \\(j\\) символов строки \\(t\\) . Если \\(s[i] \\ne t[j]\\) , то существуют три решения: вставка, удаление и замена, и каждому из них соответствует своя остаточная подзадача. На этой основе выводятся оптимальная подструктура и уравнение перехода состояния. Если же \\(s[i] = t[j]\\) , то редактировать текущий символ не нужно.
        • В задаче о расстоянии редактирования состояние зависит от значений сверху, слева и слева сверху. Поэтому после оптимизации памяти ни прямой, ни обратный обход сам по себе не дает корректного перехода состояния. Для решения этой проблемы значение слева сверху временно сохраняется в отдельной переменной, что делает ситуацию эквивалентной задаче о полном рюкзаке и позволяет использовать прямой обход.
        ","path":["Глава 14. Динамическое программирование","14.7   Резюме"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/","level":1,"title":"14.5   Задача о полном рюкзаке","text":"

        В этом разделе сначала решим еще одну распространенную задачу о рюкзаке - задачу о полном рюкзаке, а затем рассмотрим один из ее типичных частных случаев: задачу о размене монет.

        ","path":["Глава 14. Динамическое программирование","14.5   Задача о полном рюкзаке"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1451","level":2,"title":"14.5.1   Задача о полном рюкзаке","text":"

        Question

        Даны \\(n\\) предметов. Вес \\(i\\)-го предмета равен \\(wgt[i-1]\\) , стоимость равна \\(val[i-1]\\) . Также дан рюкзак вместимости \\(cap\\) . Каждый предмет можно выбирать многократно. Найдите максимальную суммарную стоимость, которую можно поместить в рюкзак при заданной вместимости. Пример показан на рисунке 14-22.

        Рисунок 14-22   Пример данных для задачи о полном рюкзаке

        ","path":["Глава 14. Динамическое программирование","14.5   Задача о полном рюкзаке"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1","level":3,"title":"1.   Идея динамического программирования","text":"

        Задача о полном рюкзаке очень похожа на задачу о рюкзаке 0-1. Разница состоит только в том, что количество выборов каждого предмета не ограничено.

        • В задаче о рюкзаке 0-1 каждого предмета существует только один экземпляр, поэтому после того как предмет \\(i\\) помещен в рюкзак, выбирать можно только из первых \\(i-1\\) предметов.
        • В задаче о полном рюкзаке количество предметов не ограничено, поэтому после того как предмет \\(i\\) помещен в рюкзак, можно продолжать выбирать из первых \\(i\\) предметов.

        При этом состояние \\([i, c]\\) в задаче о полном рюкзаке может изменяться двумя способами.

        • Не брать предмет \\(i\\) : как и в задаче о рюкзаке 0-1, переход осуществляется в \\([i-1, c]\\) .
        • Взять предмет \\(i\\) : в отличие от рюкзака 0-1 переход происходит в \\([i, c-wgt[i-1]]\\) .

        Следовательно, уравнение перехода состояния принимает вид:

        \\[ dp[i, c] = \\max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1]) \\]","path":["Глава 14. Динамическое программирование","14.5   Задача о полном рюкзаке"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#2","level":3,"title":"2.   Реализация кода","text":"

        Если сравнить код этой задачи с кодом задачи о рюкзаке 0-1, то окажется, что в переходе состояний меняется только одна деталь: вместо \\(i-1\\) появляется \\(i\\). Все остальное остается таким же:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby unbounded_knapsack.py
        def unbounded_knapsack_dp(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"Полный рюкзак: динамическое программирование\"\"\"\n    n = len(wgt)\n    # Инициализация таблицы dp\n    dp = [[0] * (cap + 1) for _ in range(n + 1)]\n    # Переход состояний\n    for i in range(1, n + 1):\n        for c in range(1, cap + 1):\n            if wgt[i - 1] > c:\n                # Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i][c] = dp[i - 1][c]\n            else:\n                # Большее из двух решений: не брать или взять предмет i\n                dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1])\n    return dp[n][cap]\n
        unbounded_knapsack.cpp
        /* Полный рюкзак: динамическое программирование */\nint unboundedKnapsackDP(vector<int> &wgt, vector<int> &val, int cap) {\n    int n = wgt.size();\n    // Инициализация таблицы dp\n    vector<vector<int>> dp(n + 1, vector<int>(cap + 1, 0));\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
        unbounded_knapsack.java
        /* Полный рюкзак: динамическое программирование */\nint unboundedKnapsackDP(int[] wgt, int[] val, int cap) {\n    int n = wgt.length;\n    // Инициализация таблицы dp\n    int[][] dp = new int[n + 1][cap + 1];\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[i][c] = Math.max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
        unbounded_knapsack.cs
        /* Полный рюкзак: динамическое программирование */\nint UnboundedKnapsackDP(int[] wgt, int[] val, int cap) {\n    int n = wgt.Length;\n    // Инициализация таблицы dp\n    int[,] dp = new int[n + 1, cap + 1];\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i, c] = dp[i - 1, c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[i, c] = Math.Max(dp[i - 1, c], dp[i, c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n, cap];\n}\n
        unbounded_knapsack.go
        /* Полный рюкзак: динамическое программирование */\nfunc unboundedKnapsackDP(wgt, val []int, cap int) int {\n    n := len(wgt)\n    // Инициализация таблицы dp\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, cap+1)\n    }\n    // Переход состояний\n    for i := 1; i <= n; i++ {\n        for c := 1; c <= cap; c++ {\n            if wgt[i-1] > c {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i][c] = dp[i-1][c]\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[i][c] = int(math.Max(float64(dp[i-1][c]), float64(dp[i][c-wgt[i-1]]+val[i-1])))\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
        unbounded_knapsack.swift
        /* Полный рюкзак: динамическое программирование */\nfunc unboundedKnapsackDP(wgt: [Int], val: [Int], cap: Int) -> Int {\n    let n = wgt.count\n    // Инициализация таблицы dp\n    var dp = Array(repeating: Array(repeating: 0, count: cap + 1), count: n + 1)\n    // Переход состояний\n    for i in 1 ... n {\n        for c in 1 ... cap {\n            if wgt[i - 1] > c {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i][c] = dp[i - 1][c]\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1])\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
        unbounded_knapsack.js
        /* Полный рюкзак: динамическое программирование */\nfunction unboundedKnapsackDP(wgt, val, cap) {\n    const n = wgt.length;\n    // Инициализация таблицы dp\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: cap + 1 }, () => 0)\n    );\n    // Переход состояний\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[i][c] = Math.max(\n                    dp[i - 1][c],\n                    dp[i][c - wgt[i - 1]] + val[i - 1]\n                );\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
        unbounded_knapsack.ts
        /* Полный рюкзак: динамическое программирование */\nfunction unboundedKnapsackDP(\n    wgt: Array<number>,\n    val: Array<number>,\n    cap: number\n): number {\n    const n = wgt.length;\n    // Инициализация таблицы dp\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: cap + 1 }, () => 0)\n    );\n    // Переход состояний\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[i][c] = Math.max(\n                    dp[i - 1][c],\n                    dp[i][c - wgt[i - 1]] + val[i - 1]\n                );\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
        unbounded_knapsack.dart
        /* Полный рюкзак: динамическое программирование */\nint unboundedKnapsackDP(List<int> wgt, List<int> val, int cap) {\n  int n = wgt.length;\n  // Инициализация таблицы dp\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(cap + 1, 0));\n  // Переход состояний\n  for (int i = 1; i <= n; i++) {\n    for (int c = 1; c <= cap; c++) {\n      if (wgt[i - 1] > c) {\n        // Если вместимость рюкзака превышена, предмет i не выбирать\n        dp[i][c] = dp[i - 1][c];\n      } else {\n        // Большее из двух решений: не брать или взять предмет i\n        dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]);\n      }\n    }\n  }\n  return dp[n][cap];\n}\n
        unbounded_knapsack.rs
        /* Полный рюкзак: динамическое программирование */\nfn unbounded_knapsack_dp(wgt: &[i32], val: &[i32], cap: usize) -> i32 {\n    let n = wgt.len();\n    // Инициализация таблицы dp\n    let mut dp = vec![vec![0; cap + 1]; n + 1];\n    // Переход состояний\n    for i in 1..=n {\n        for c in 1..=cap {\n            if wgt[i - 1] > c as i32 {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[i][c] = std::cmp::max(dp[i - 1][c], dp[i][c - wgt[i - 1] as usize] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
        unbounded_knapsack.c
        /* Полный рюкзак: динамическое программирование */\nint unboundedKnapsackDP(int wgt[], int val[], int cap, int wgtSize) {\n    int n = wgtSize;\n    // Инициализация таблицы dp\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(cap + 1, sizeof(int));\n    }\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[i][c] = myMax(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    int res = dp[n][cap];\n    // Освободить память\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    return res;\n}\n
        unbounded_knapsack.kt
        /* Полный рюкзак: динамическое программирование */\nfun unboundedKnapsackDP(wgt: IntArray, _val: IntArray, cap: Int): Int {\n    val n = wgt.size\n    // Инициализация таблицы dp\n    val dp = Array(n + 1) { IntArray(cap + 1) }\n    // Переход состояний\n    for (i in 1..n) {\n        for (c in 1..cap) {\n            if (wgt[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[i][c] = dp[i - 1][c]\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + _val[i - 1])\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
        unbounded_knapsack.rb
        ### Полный рюкзак: динамическое программирование ###\ndef unbounded_knapsack_dp(wgt, val, cap)\n  n = wgt.length\n  # Инициализация таблицы dp\n  dp = Array.new(n + 1) { Array.new(cap + 1, 0) }\n  # Переход состояний\n  for i in 1...(n + 1)\n    for c in 1...(cap + 1)\n      if wgt[i - 1] > c\n        # Если вместимость рюкзака превышена, предмет i не выбирать\n        dp[i][c] = dp[i - 1][c]\n      else\n        # Большее из двух решений: не брать или взять предмет i\n        dp[i][c] = [dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]].max\n      end\n    end\n  end\n  dp[n][cap]\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 14. Динамическое программирование","14.5   Задача о полном рюкзаке"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#3","level":3,"title":"3.   Оптимизация пространства","text":"

        Поскольку текущее состояние переходит из состояния слева и состояния сверху, после оптимизации памяти каждую строку таблицы \\(dp\\) нужно обходить слева направо.

        Этот порядок обхода как раз противоположен задаче о рюкзаке 0-1. Эту разницу удобно понять, рассмотрев то, что показано на рисунке 14-23.

        <1><2><3><4><5><6>

        Рисунок 14-23   Процесс динамического программирования после оптимизации памяти для полного рюкзака

        Код реализации здесь довольно прост: достаточно просто убрать первое измерение массива dp :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby unbounded_knapsack.py
        def unbounded_knapsack_dp_comp(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"Полный рюкзак: динамическое программирование с оптимизацией памяти\"\"\"\n    n = len(wgt)\n    # Инициализация таблицы dp\n    dp = [0] * (cap + 1)\n    # Переход состояний\n    for i in range(1, n + 1):\n        # Прямой обход\n        for c in range(1, cap + 1):\n            if wgt[i - 1] > c:\n                # Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[c] = dp[c]\n            else:\n                # Большее из двух решений: не брать или взять предмет i\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])\n    return dp[cap]\n
        unbounded_knapsack.cpp
        /* Полный рюкзак: динамическое программирование с оптимизацией памяти */\nint unboundedKnapsackDPComp(vector<int> &wgt, vector<int> &val, int cap) {\n    int n = wgt.size();\n    // Инициализация таблицы dp\n    vector<int> dp(cap + 1, 0);\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[c] = dp[c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
        unbounded_knapsack.java
        /* Полный рюкзак: динамическое программирование с оптимизацией памяти */\nint unboundedKnapsackDPComp(int[] wgt, int[] val, int cap) {\n    int n = wgt.length;\n    // Инициализация таблицы dp\n    int[] dp = new int[cap + 1];\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[c] = dp[c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
        unbounded_knapsack.cs
        /* Полный рюкзак: динамическое программирование с оптимизацией памяти */\nint UnboundedKnapsackDPComp(int[] wgt, int[] val, int cap) {\n    int n = wgt.Length;\n    // Инициализация таблицы dp\n    int[] dp = new int[cap + 1];\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[c] = dp[c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[c] = Math.Max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
        unbounded_knapsack.go
        /* Полный рюкзак: динамическое программирование с оптимизацией памяти */\nfunc unboundedKnapsackDPComp(wgt, val []int, cap int) int {\n    n := len(wgt)\n    // Инициализация таблицы dp\n    dp := make([]int, cap+1)\n    // Переход состояний\n    for i := 1; i <= n; i++ {\n        for c := 1; c <= cap; c++ {\n            if wgt[i-1] > c {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[c] = dp[c]\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[c] = int(math.Max(float64(dp[c]), float64(dp[c-wgt[i-1]]+val[i-1])))\n            }\n        }\n    }\n    return dp[cap]\n}\n
        unbounded_knapsack.swift
        /* Полный рюкзак: динамическое программирование с оптимизацией памяти */\nfunc unboundedKnapsackDPComp(wgt: [Int], val: [Int], cap: Int) -> Int {\n    let n = wgt.count\n    // Инициализация таблицы dp\n    var dp = Array(repeating: 0, count: cap + 1)\n    // Переход состояний\n    for i in 1 ... n {\n        for c in 1 ... cap {\n            if wgt[i - 1] > c {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[c] = dp[c]\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])\n            }\n        }\n    }\n    return dp[cap]\n}\n
        unbounded_knapsack.js
        /* Полный рюкзак: динамическое программирование с оптимизацией памяти */\nfunction unboundedKnapsackDPComp(wgt, val, cap) {\n    const n = wgt.length;\n    // Инициализация таблицы dp\n    const dp = Array.from({ length: cap + 1 }, () => 0);\n    // Переход состояний\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[c] = dp[c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
        unbounded_knapsack.ts
        /* Полный рюкзак: динамическое программирование с оптимизацией памяти */\nfunction unboundedKnapsackDPComp(\n    wgt: Array<number>,\n    val: Array<number>,\n    cap: number\n): number {\n    const n = wgt.length;\n    // Инициализация таблицы dp\n    const dp = Array.from({ length: cap + 1 }, () => 0);\n    // Переход состояний\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[c] = dp[c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
        unbounded_knapsack.dart
        /* Полный рюкзак: динамическое программирование с оптимизацией памяти */\nint unboundedKnapsackDPComp(List<int> wgt, List<int> val, int cap) {\n  int n = wgt.length;\n  // Инициализация таблицы dp\n  List<int> dp = List.filled(cap + 1, 0);\n  // Переход состояний\n  for (int i = 1; i <= n; i++) {\n    for (int c = 1; c <= cap; c++) {\n      if (wgt[i - 1] > c) {\n        // Если вместимость рюкзака превышена, предмет i не выбирать\n        dp[c] = dp[c];\n      } else {\n        // Большее из двух решений: не брать или взять предмет i\n        dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n      }\n    }\n  }\n  return dp[cap];\n}\n
        unbounded_knapsack.rs
        /* Полный рюкзак: динамическое программирование с оптимизацией памяти */\nfn unbounded_knapsack_dp_comp(wgt: &[i32], val: &[i32], cap: usize) -> i32 {\n    let n = wgt.len();\n    // Инициализация таблицы dp\n    let mut dp = vec![0; cap + 1];\n    // Переход состояний\n    for i in 1..=n {\n        for c in 1..=cap {\n            if wgt[i - 1] > c as i32 {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[c] = dp[c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[c] = std::cmp::max(dp[c], dp[c - wgt[i - 1] as usize] + val[i - 1]);\n            }\n        }\n    }\n    dp[cap]\n}\n
        unbounded_knapsack.c
        /* Полный рюкзак: динамическое программирование с оптимизацией памяти */\nint unboundedKnapsackDPComp(int wgt[], int val[], int cap, int wgtSize) {\n    int n = wgtSize;\n    // Инициализация таблицы dp\n    int *dp = calloc(cap + 1, sizeof(int));\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[c] = dp[c];\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[c] = myMax(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    int res = dp[cap];\n    // Освободить память\n    free(dp);\n    return res;\n}\n
        unbounded_knapsack.kt
        /* Полный рюкзак: динамическое программирование с оптимизацией памяти */\nfun unboundedKnapsackDPComp(\n    wgt: IntArray,\n    _val: IntArray,\n    cap: Int\n): Int {\n    val n = wgt.size\n    // Инициализация таблицы dp\n    val dp = IntArray(cap + 1)\n    // Переход состояний\n    for (i in 1..n) {\n        for (c in 1..cap) {\n            if (wgt[i - 1] > c) {\n                // Если вместимость рюкзака превышена, предмет i не выбирать\n                dp[c] = dp[c]\n            } else {\n                // Большее из двух решений: не брать или взять предмет i\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + _val[i - 1])\n            }\n        }\n    }\n    return dp[cap]\n}\n
        unbounded_knapsack.rb
        ### Полный рюкзак: динамическое программирование ###\ndef unbounded_knapsack_dp(wgt, val, cap)\n  n = wgt.length\n  # Инициализация таблицы dp\n  dp = Array.new(n + 1) { Array.new(cap + 1, 0) }\n  # Переход состояний\n  for i in 1...(n + 1)\n    for c in 1...(cap + 1)\n      if wgt[i - 1] > c\n        # Если вместимость рюкзака превышена, предмет i не выбирать\n        dp[i][c] = dp[i - 1][c]\n      else\n        # Большее из двух решений: не брать или взять предмет i\n        dp[i][c] = [dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]].max\n      end\n    end\n  end\n  dp[n][cap]\nend\n\n# ## Полный рюкзак: динамическое программирование с оптимизацией памяти ##3\ndef unbounded_knapsack_dp_comp(wgt, val, cap)\n  n = wgt.length\n  # Инициализация таблицы dp\n  dp = Array.new(cap + 1, 0)\n  # Переход состояний\n  for i in 1...(n + 1)\n    # Прямой обход\n    for c in 1...(cap + 1)\n      if wgt[i -1] > c\n        # Если вместимость рюкзака превышена, предмет i не выбирать\n        dp[c] = dp[c]\n      else\n        # Большее из двух решений: не брать или взять предмет i\n        dp[c] = [dp[c], dp[c - wgt[i - 1]] + val[i - 1]].max\n      end\n    end\n  end\n  dp[cap]\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 14. Динамическое программирование","14.5   Задача о полном рюкзаке"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1452","level":2,"title":"14.5.2   Задача о размене монет","text":"

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

        Question

        Даны \\(n\\) видов монет, номинал монеты \\(i\\) равен \\(coins[i - 1]\\) , а целевая сумма равна \\(amt\\) . Монеты каждого вида можно брать многократно. Требуется найти минимальное число монет, которыми можно набрать целевую сумму. Если набрать сумму невозможно, верните \\(-1\\) . Пример показан на рисунке 14-24.

        Рисунок 14-24   Пример данных для задачи о размене монет

        ","path":["Глава 14. Динамическое программирование","14.5   Задача о полном рюкзаке"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1_1","level":3,"title":"1.   Идея динамического программирования","text":"

        Задачу о размене монет можно рассматривать как частный случай задачи о полном рюкзаке. Между ними существуют следующие соответствия и различия.

        • Эти две задачи можно взаимно преобразовать: «предмет» соответствует «монете», «вес предмета» соответствует «номиналу монеты», а «вместимость рюкзака» соответствует «целевой сумме».
        • Цель оптимизации противоположна: в задаче о полном рюкзаке нужно максимизировать стоимость предметов, а в задаче о размене монет - минимизировать число монет.
        • В задаче о полном рюкзаке ищется решение, не превышающее вместимость, а в задаче о размене монет требуется ровно набрать целевую сумму.

        Шаг 1: продумать решения на каждом раунде, определить состояние и тем самым получить таблицу \\(dp\\)

        Подзадача, соответствующая состоянию \\([i, a]\\) , выглядит так: минимальное число монет из первых \\(i\\) видов, которыми можно набрать сумму \\(a\\). Решение этой подзадачи обозначается как \\(dp[i, a]\\) .

        Размер двумерной таблицы \\(dp\\) равен \\((n+1) \\times (amt+1)\\) .

        Шаг 2: найти оптимальную подструктуру и на ее основе вывести уравнение перехода состояния

        По сравнению с задачей о полном рюкзаке здесь есть два отличия в уравнении перехода состояния.

        • Нужно искать минимум, а не максимум, поэтому оператор \\(\\max()\\) заменяется на \\(\\min()\\) .
        • Оптимизируемое значение - это число монет, а не суммарная стоимость, поэтому при выборе монеты нужно просто прибавить \\(1\\) .
        \\[ dp[i, a] = \\min(dp[i-1, a], dp[i, a - coins[i-1]] + 1) \\]

        Шаг 3: определить граничные условия и порядок переходов

        Когда целевая сумма равна \\(0\\) , минимальное число монет для ее набора равно \\(0\\) , то есть весь первый столбец \\(dp[i, 0]\\) заполняется нулями.

        Когда монет нет, невозможно набрать никакую целевую сумму \\(> 0\\). Это и есть недопустимое решение. Чтобы функция \\(\\min()\\) в уравнении перехода состояния могла распознавать и отбрасывать такие недопустимые решения, удобно использовать значение \\(+ \\infty\\). То есть всю первую строку \\(dp[0, a]\\) нужно инициализировать значением \\(+ \\infty\\) .

        ","path":["Глава 14. Динамическое программирование","14.5   Задача о полном рюкзаке"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#2_1","level":3,"title":"2.   Реализация кода","text":"

        Большинство языков программирования не предоставляет представление для \\(+ \\infty\\) в целочисленном виде, поэтому обычно приходится заменять его на максимальное значение типа int . Но тогда возникает риск переполнения: операция \\(+ 1\\) в уравнении перехода может переполнить большое число.

        Поэтому здесь мы используем число \\(amt + 1\\) как обозначение недопустимого решения, потому что для набора суммы \\(amt\\) максимум нужно не больше чем \\(amt\\) монет. Перед возвратом результата проверяем, равно ли \\(dp[n, amt]\\) значению \\(amt + 1\\). Если да, то возвращаем \\(-1\\) , что означает невозможность набрать целевую сумму. Код приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change.py
        def coin_change_dp(coins: list[int], amt: int) -> int:\n    \"\"\"Размен монет: динамическое программирование\"\"\"\n    n = len(coins)\n    MAX = amt + 1\n    # Инициализация таблицы dp\n    dp = [[0] * (amt + 1) for _ in range(n + 1)]\n    # Переход состояний: первая строка и первый столбец\n    for a in range(1, amt + 1):\n        dp[0][a] = MAX\n    # Переход состояний: остальные строки и столбцы\n    for i in range(1, n + 1):\n        for a in range(1, amt + 1):\n            if coins[i - 1] > a:\n                # Если целевая сумма превышена, монету i не выбирать\n                dp[i][a] = dp[i - 1][a]\n            else:\n                # Меньшее из двух решений: не брать или взять монету i\n                dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1)\n    return dp[n][amt] if dp[n][amt] != MAX else -1\n
        coin_change.cpp
        /* Размен монет: динамическое программирование */\nint coinChangeDP(vector<int> &coins, int amt) {\n    int n = coins.size();\n    int MAX = amt + 1;\n    // Инициализация таблицы dp\n    vector<vector<int>> dp(n + 1, vector<int>(amt + 1, 0));\n    // Переход состояний: первая строка и первый столбец\n    for (int a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Меньшее из двух решений: не брать или взять монету i\n                dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[n][amt] != MAX ? dp[n][amt] : -1;\n}\n
        coin_change.java
        /* Размен монет: динамическое программирование */\nint coinChangeDP(int[] coins, int amt) {\n    int n = coins.length;\n    int MAX = amt + 1;\n    // Инициализация таблицы dp\n    int[][] dp = new int[n + 1][amt + 1];\n    // Переход состояний: первая строка и первый столбец\n    for (int a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Меньшее из двух решений: не брать или взять монету i\n                dp[i][a] = Math.min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[n][amt] != MAX ? dp[n][amt] : -1;\n}\n
        coin_change.cs
        /* Размен монет: динамическое программирование */\nint CoinChangeDP(int[] coins, int amt) {\n    int n = coins.Length;\n    int MAX = amt + 1;\n    // Инициализация таблицы dp\n    int[,] dp = new int[n + 1, amt + 1];\n    // Переход состояний: первая строка и первый столбец\n    for (int a = 1; a <= amt; a++) {\n        dp[0, a] = MAX;\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[i, a] = dp[i - 1, a];\n            } else {\n                // Меньшее из двух решений: не брать или взять монету i\n                dp[i, a] = Math.Min(dp[i - 1, a], dp[i, a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[n, amt] != MAX ? dp[n, amt] : -1;\n}\n
        coin_change.go
        /* Размен монет: динамическое программирование */\nfunc coinChangeDP(coins []int, amt int) int {\n    n := len(coins)\n    max := amt + 1\n    // Инициализация таблицы dp\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, amt+1)\n    }\n    // Переход состояний: первая строка и первый столбец\n    for a := 1; a <= amt; a++ {\n        dp[0][a] = max\n    }\n    // Переход состояний: остальные строки и столбцы\n    for i := 1; i <= n; i++ {\n        for a := 1; a <= amt; a++ {\n            if coins[i-1] > a {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[i][a] = dp[i-1][a]\n            } else {\n                // Меньшее из двух решений: не брать или взять монету i\n                dp[i][a] = int(math.Min(float64(dp[i-1][a]), float64(dp[i][a-coins[i-1]]+1)))\n            }\n        }\n    }\n    if dp[n][amt] != max {\n        return dp[n][amt]\n    }\n    return -1\n}\n
        coin_change.swift
        /* Размен монет: динамическое программирование */\nfunc coinChangeDP(coins: [Int], amt: Int) -> Int {\n    let n = coins.count\n    let MAX = amt + 1\n    // Инициализация таблицы dp\n    var dp = Array(repeating: Array(repeating: 0, count: amt + 1), count: n + 1)\n    // Переход состояний: первая строка и первый столбец\n    for a in 1 ... amt {\n        dp[0][a] = MAX\n    }\n    // Переход состояний: остальные строки и столбцы\n    for i in 1 ... n {\n        for a in 1 ... amt {\n            if coins[i - 1] > a {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[i][a] = dp[i - 1][a]\n            } else {\n                // Меньшее из двух решений: не брать или взять монету i\n                dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1)\n            }\n        }\n    }\n    return dp[n][amt] != MAX ? dp[n][amt] : -1\n}\n
        coin_change.js
        /* Размен монет: динамическое программирование */\nfunction coinChangeDP(coins, amt) {\n    const n = coins.length;\n    const MAX = amt + 1;\n    // Инициализация таблицы dp\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: amt + 1 }, () => 0)\n    );\n    // Переход состояний: первая строка и первый столбец\n    for (let a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Меньшее из двух решений: не брать или взять монету i\n                dp[i][a] = Math.min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[n][amt] !== MAX ? dp[n][amt] : -1;\n}\n
        coin_change.ts
        /* Размен монет: динамическое программирование */\nfunction coinChangeDP(coins: Array<number>, amt: number): number {\n    const n = coins.length;\n    const MAX = amt + 1;\n    // Инициализация таблицы dp\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: amt + 1 }, () => 0)\n    );\n    // Переход состояний: первая строка и первый столбец\n    for (let a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Меньшее из двух решений: не брать или взять монету i\n                dp[i][a] = Math.min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[n][amt] !== MAX ? dp[n][amt] : -1;\n}\n
        coin_change.dart
        /* Размен монет: динамическое программирование */\nint coinChangeDP(List<int> coins, int amt) {\n  int n = coins.length;\n  int MAX = amt + 1;\n  // Инициализация таблицы dp\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(amt + 1, 0));\n  // Переход состояний: первая строка и первый столбец\n  for (int a = 1; a <= amt; a++) {\n    dp[0][a] = MAX;\n  }\n  // Переход состояний: остальные строки и столбцы\n  for (int i = 1; i <= n; i++) {\n    for (int a = 1; a <= amt; a++) {\n      if (coins[i - 1] > a) {\n        // Если целевая сумма превышена, монету i не выбирать\n        dp[i][a] = dp[i - 1][a];\n      } else {\n        // Меньшее из двух решений: не брать или взять монету i\n        dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n      }\n    }\n  }\n  return dp[n][amt] != MAX ? dp[n][amt] : -1;\n}\n
        coin_change.rs
        /* Размен монет: динамическое программирование */\nfn coin_change_dp(coins: &[i32], amt: usize) -> i32 {\n    let n = coins.len();\n    let max = amt + 1;\n    // Инициализация таблицы dp\n    let mut dp = vec![vec![0; amt + 1]; n + 1];\n    // Переход состояний: первая строка и первый столбец\n    for a in 1..=amt {\n        dp[0][a] = max;\n    }\n    // Переход состояний: остальные строки и столбцы\n    for i in 1..=n {\n        for a in 1..=amt {\n            if coins[i - 1] > a as i32 {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Меньшее из двух решений: не брать или взять монету i\n                dp[i][a] = std::cmp::min(dp[i - 1][a], dp[i][a - coins[i - 1] as usize] + 1);\n            }\n        }\n    }\n    if dp[n][amt] != max {\n        return dp[n][amt] as i32;\n    } else {\n        -1\n    }\n}\n
        coin_change.c
        /* Размен монет: динамическое программирование */\nint coinChangeDP(int coins[], int amt, int coinsSize) {\n    int n = coinsSize;\n    int MAX = amt + 1;\n    // Инициализация таблицы dp\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(amt + 1, sizeof(int));\n    }\n    // Переход состояний: первая строка и первый столбец\n    for (int a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Меньшее из двух решений: не брать или взять монету i\n                dp[i][a] = myMin(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    int res = dp[n][amt] != MAX ? dp[n][amt] : -1;\n    // Освободить память\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    free(dp);\n    return res;\n}\n
        coin_change.kt
        /* Размен монет: динамическое программирование */\nfun coinChangeDP(coins: IntArray, amt: Int): Int {\n    val n = coins.size\n    val MAX = amt + 1\n    // Инициализация таблицы dp\n    val dp = Array(n + 1) { IntArray(amt + 1) }\n    // Переход состояний: первая строка и первый столбец\n    for (a in 1..amt) {\n        dp[0][a] = MAX\n    }\n    // Переход состояний: остальные строки и столбцы\n    for (i in 1..n) {\n        for (a in 1..amt) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[i][a] = dp[i - 1][a]\n            } else {\n                // Меньшее из двух решений: не брать или взять монету i\n                dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1)\n            }\n        }\n    }\n    return if (dp[n][amt] != MAX) dp[n][amt] else -1\n}\n
        coin_change.rb
        ### Размен монет: динамическое программирование ###\ndef coin_change_dp(coins, amt)\n  n = coins.length\n  _MAX = amt + 1\n  # Инициализация таблицы dp\n  dp = Array.new(n + 1) { Array.new(amt + 1, 0) }\n  # Переход состояний: первая строка и первый столбец\n  (1...(amt + 1)).each { |a| dp[0][a] = _MAX }\n  # Переход состояний: остальные строки и столбцы\n  for i in 1...(n + 1)\n    for a in 1...(amt + 1)\n      if coins[i - 1] > a\n        # Если целевая сумма превышена, монету i не выбирать\n        dp[i][a] = dp[i - 1][a]\n      else\n        # Меньшее из двух решений: не брать или взять монету i\n        dp[i][a] = [dp[i - 1][a], dp[i][a - coins[i - 1]] + 1].min\n      end\n    end\n  end\n  dp[n][amt] != _MAX ? dp[n][amt] : -1\nend\n
        Визуализация кода

        Во весь экран >

        Как показано на рисунке 14-25, процесс динамического программирования для задачи о размене монет очень похож на задачу о полном рюкзаке.

        <1><2><3><4><5><6><7><8><9><10><11><12><13><14><15>

        Рисунок 14-25   Процесс динамического программирования для задачи о размене монет

        ","path":["Глава 14. Динамическое программирование","14.5   Задача о полном рюкзаке"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#3_1","level":3,"title":"3.   Оптимизация пространства","text":"

        Оптимизация пространства для задачи о размене монет выполняется так же, как и для полного рюкзака:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change.py
        def coin_change_dp_comp(coins: list[int], amt: int) -> int:\n    \"\"\"Размен монет: динамическое программирование с оптимизацией памяти\"\"\"\n    n = len(coins)\n    MAX = amt + 1\n    # Инициализация таблицы dp\n    dp = [MAX] * (amt + 1)\n    dp[0] = 0\n    # Переход состояний\n    for i in range(1, n + 1):\n        # Прямой обход\n        for a in range(1, amt + 1):\n            if coins[i - 1] > a:\n                # Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a]\n            else:\n                # Меньшее из двух решений: не брать или взять монету i\n                dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1)\n    return dp[amt] if dp[amt] != MAX else -1\n
        coin_change.cpp
        /* Размен монет: динамическое программирование с оптимизацией памяти */\nint coinChangeDPComp(vector<int> &coins, int amt) {\n    int n = coins.size();\n    int MAX = amt + 1;\n    // Инициализация таблицы dp\n    vector<int> dp(amt + 1, MAX);\n    dp[0] = 0;\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a];\n            } else {\n                // Меньшее из двух решений: не брать или взять монету i\n                dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[amt] != MAX ? dp[amt] : -1;\n}\n
        coin_change.java
        /* Размен монет: динамическое программирование с оптимизацией памяти */\nint coinChangeDPComp(int[] coins, int amt) {\n    int n = coins.length;\n    int MAX = amt + 1;\n    // Инициализация таблицы dp\n    int[] dp = new int[amt + 1];\n    Arrays.fill(dp, MAX);\n    dp[0] = 0;\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a];\n            } else {\n                // Меньшее из двух решений: не брать или взять монету i\n                dp[a] = Math.min(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[amt] != MAX ? dp[amt] : -1;\n}\n
        coin_change.cs
        /* Размен монет: динамическое программирование с оптимизацией памяти */\nint CoinChangeDPComp(int[] coins, int amt) {\n    int n = coins.Length;\n    int MAX = amt + 1;\n    // Инициализация таблицы dp\n    int[] dp = new int[amt + 1];\n    Array.Fill(dp, MAX);\n    dp[0] = 0;\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a];\n            } else {\n                // Меньшее из двух решений: не брать или взять монету i\n                dp[a] = Math.Min(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[amt] != MAX ? dp[amt] : -1;\n}\n
        coin_change.go
        /* Размен монет: динамическое программирование */\nfunc coinChangeDPComp(coins []int, amt int) int {\n    n := len(coins)\n    max := amt + 1\n    // Инициализация таблицы dp\n    dp := make([]int, amt+1)\n    for i := 1; i <= amt; i++ {\n        dp[i] = max\n    }\n    // Переход состояний\n    for i := 1; i <= n; i++ {\n        // Прямой обход\n        for a := 1; a <= amt; a++ {\n            if coins[i-1] > a {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a]\n            } else {\n                // Меньшее из двух решений: не брать или взять монету i\n                dp[a] = int(math.Min(float64(dp[a]), float64(dp[a-coins[i-1]]+1)))\n            }\n        }\n    }\n    if dp[amt] != max {\n        return dp[amt]\n    }\n    return -1\n}\n
        coin_change.swift
        /* Размен монет: динамическое программирование с оптимизацией памяти */\nfunc coinChangeDPComp(coins: [Int], amt: Int) -> Int {\n    let n = coins.count\n    let MAX = amt + 1\n    // Инициализация таблицы dp\n    var dp = Array(repeating: MAX, count: amt + 1)\n    dp[0] = 0\n    // Переход состояний\n    for i in 1 ... n {\n        for a in 1 ... amt {\n            if coins[i - 1] > a {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a]\n            } else {\n                // Меньшее из двух решений: не брать или взять монету i\n                dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1)\n            }\n        }\n    }\n    return dp[amt] != MAX ? dp[amt] : -1\n}\n
        coin_change.js
        /* Размен монет: динамическое программирование с оптимизацией памяти */\nfunction coinChangeDPComp(coins, amt) {\n    const n = coins.length;\n    const MAX = amt + 1;\n    // Инициализация таблицы dp\n    const dp = Array.from({ length: amt + 1 }, () => MAX);\n    dp[0] = 0;\n    // Переход состояний\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a];\n            } else {\n                // Меньшее из двух решений: не брать или взять монету i\n                dp[a] = Math.min(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[amt] !== MAX ? dp[amt] : -1;\n}\n
        coin_change.ts
        /* Размен монет: динамическое программирование с оптимизацией памяти */\nfunction coinChangeDPComp(coins: Array<number>, amt: number): number {\n    const n = coins.length;\n    const MAX = amt + 1;\n    // Инициализация таблицы dp\n    const dp = Array.from({ length: amt + 1 }, () => MAX);\n    dp[0] = 0;\n    // Переход состояний\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a];\n            } else {\n                // Меньшее из двух решений: не брать или взять монету i\n                dp[a] = Math.min(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[amt] !== MAX ? dp[amt] : -1;\n}\n
        coin_change.dart
        /* Размен монет: динамическое программирование с оптимизацией памяти */\nint coinChangeDPComp(List<int> coins, int amt) {\n  int n = coins.length;\n  int MAX = amt + 1;\n  // Инициализация таблицы dp\n  List<int> dp = List.filled(amt + 1, MAX);\n  dp[0] = 0;\n  // Переход состояний\n  for (int i = 1; i <= n; i++) {\n    for (int a = 1; a <= amt; a++) {\n      if (coins[i - 1] > a) {\n        // Если целевая сумма превышена, монету i не выбирать\n        dp[a] = dp[a];\n      } else {\n        // Меньшее из двух решений: не брать или взять монету i\n        dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1);\n      }\n    }\n  }\n  return dp[amt] != MAX ? dp[amt] : -1;\n}\n
        coin_change.rs
        /* Размен монет: динамическое программирование с оптимизацией памяти */\nfn coin_change_dp_comp(coins: &[i32], amt: usize) -> i32 {\n    let n = coins.len();\n    let max = amt + 1;\n    // Инициализация таблицы dp\n    let mut dp = vec![0; amt + 1];\n    dp.fill(max);\n    dp[0] = 0;\n    // Переход состояний\n    for i in 1..=n {\n        for a in 1..=amt {\n            if coins[i - 1] > a as i32 {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a];\n            } else {\n                // Меньшее из двух решений: не брать или взять монету i\n                dp[a] = std::cmp::min(dp[a], dp[a - coins[i - 1] as usize] + 1);\n            }\n        }\n    }\n    if dp[amt] != max {\n        return dp[amt] as i32;\n    } else {\n        -1\n    }\n}\n
        coin_change.c
        /* Размен монет: динамическое программирование с оптимизацией памяти */\nint coinChangeDPComp(int coins[], int amt, int coinsSize) {\n    int n = coinsSize;\n    int MAX = amt + 1;\n    // Инициализация таблицы dp\n    int *dp = malloc((amt + 1) * sizeof(int));\n    for (int j = 1; j <= amt; j++) {\n        dp[j] = MAX;\n    } \n    dp[0] = 0;\n\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a];\n            } else {\n                // Меньшее из двух решений: не брать или взять монету i\n                dp[a] = myMin(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    int res = dp[amt] != MAX ? dp[amt] : -1;\n    // Освободить память\n    free(dp);\n    return res;\n}\n
        coin_change.kt
        /* Размен монет: динамическое программирование с оптимизацией памяти */\nfun coinChangeDPComp(coins: IntArray, amt: Int): Int {\n    val n = coins.size\n    val MAX = amt + 1\n    // Инициализация таблицы dp\n    val dp = IntArray(amt + 1)\n    dp.fill(MAX)\n    dp[0] = 0\n    // Переход состояний\n    for (i in 1..n) {\n        for (a in 1..amt) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a]\n            } else {\n                // Меньшее из двух решений: не брать или взять монету i\n                dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1)\n            }\n        }\n    }\n    return if (dp[amt] != MAX) dp[amt] else -1\n}\n
        coin_change.rb
        ### Размен монет: динамическое программирование с оптимизацией памяти ###\ndef coin_change_dp_comp(coins, amt)\n  n = coins.length\n  _MAX = amt + 1\n  # Инициализация таблицы dp\n  dp = Array.new(amt + 1, _MAX)\n  dp[0] = 0\n  # Переход состояний\n  for i in 1...(n + 1)\n    # Прямой обход\n    for a in 1...(amt + 1)\n      if coins[i - 1] > a\n        # Если целевая сумма превышена, монету i не выбирать\n        dp[a] = dp[a]\n      else\n        # Меньшее из двух решений: не брать или взять монету i\n        dp[a] = [dp[a], dp[a - coins[i - 1]] + 1].min\n      end\n    end\n  end\n  dp[amt] != _MAX ? dp[amt] : -1\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 14. Динамическое программирование","14.5   Задача о полном рюкзаке"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1453-ii","level":2,"title":"14.5.3   Задача о размене монет II","text":"

        Question

        Даны \\(n\\) видов монет, номинал монеты \\(i\\) равен \\(coins[i - 1]\\) , а целевая сумма равна \\(amt\\) . Монеты каждого вида можно брать многократно. Найдите число различных комбинаций монет, которыми можно набрать целевую сумму. Пример показан на рисунке 14-26.

        Рисунок 14-26   Пример данных для задачи о размене монет II

        ","path":["Глава 14. Динамическое программирование","14.5   Задача о полном рюкзаке"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1_2","level":3,"title":"1.   Идея динамического программирования","text":"

        По сравнению с предыдущей задачей здесь целью является число комбинаций. Поэтому подзадача меняется на следующую: число комбинаций из первых \\(i\\) видов монет, которыми можно набрать сумму \\(a\\). При этом таблица \\(dp\\) по-прежнему остается двумерной матрицей размера \\((n+1) \\times (amt + 1)\\) .

        Число комбинаций для текущего состояния равно сумме числа комбинаций для двух решений: не брать текущую монету и брать текущую монету. Поэтому уравнение перехода состояния принимает вид:

        \\[ dp[i, a] = dp[i-1, a] + dp[i, a - coins[i-1]] \\]

        Когда целевая сумма равна \\(0\\) , ее можно набрать, не выбирая ни одной монеты, поэтому весь первый столбец \\(dp[i, 0]\\) нужно инициализировать единицами. Когда монет нет, невозможно набрать никакую сумму \\(>0\\) , поэтому вся первая строка \\(dp[0, a]\\) должна быть заполнена нулями.

        ","path":["Глава 14. Динамическое программирование","14.5   Задача о полном рюкзаке"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#2_2","level":3,"title":"2.   Реализация кода","text":"PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change_ii.py
        def coin_change_ii_dp(coins: list[int], amt: int) -> int:\n    \"\"\"Размен монет II: динамическое программирование\"\"\"\n    n = len(coins)\n    # Инициализация таблицы dp\n    dp = [[0] * (amt + 1) for _ in range(n + 1)]\n    # Инициализация первого столбца\n    for i in range(n + 1):\n        dp[i][0] = 1\n    # Переход состояний\n    for i in range(1, n + 1):\n        for a in range(1, amt + 1):\n            if coins[i - 1] > a:\n                # Если целевая сумма превышена, монету i не выбирать\n                dp[i][a] = dp[i - 1][a]\n            else:\n                # Сумма двух решений: не брать или взять монету i\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]\n    return dp[n][amt]\n
        coin_change_ii.cpp
        /* Размен монет II: динамическое программирование */\nint coinChangeIIDP(vector<int> &coins, int amt) {\n    int n = coins.size();\n    // Инициализация таблицы dp\n    vector<vector<int>> dp(n + 1, vector<int>(amt + 1, 0));\n    // Инициализация первого столбца\n    for (int i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Сумма двух решений: не брать или взять монету i\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[n][amt];\n}\n
        coin_change_ii.java
        /* Размен монет II: динамическое программирование */\nint coinChangeIIDP(int[] coins, int amt) {\n    int n = coins.length;\n    // Инициализация таблицы dp\n    int[][] dp = new int[n + 1][amt + 1];\n    // Инициализация первого столбца\n    for (int i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Сумма двух решений: не брать или взять монету i\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[n][amt];\n}\n
        coin_change_ii.cs
        /* Размен монет II: динамическое программирование */\nint CoinChangeIIDP(int[] coins, int amt) {\n    int n = coins.Length;\n    // Инициализация таблицы dp\n    int[,] dp = new int[n + 1, amt + 1];\n    // Инициализация первого столбца\n    for (int i = 0; i <= n; i++) {\n        dp[i, 0] = 1;\n    }\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[i, a] = dp[i - 1, a];\n            } else {\n                // Сумма двух решений: не брать или взять монету i\n                dp[i, a] = dp[i - 1, a] + dp[i, a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[n, amt];\n}\n
        coin_change_ii.go
        /* Размен монет II: динамическое программирование */\nfunc coinChangeIIDP(coins []int, amt int) int {\n    n := len(coins)\n    // Инициализация таблицы dp\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, amt+1)\n    }\n    // Инициализация первого столбца\n    for i := 0; i <= n; i++ {\n        dp[i][0] = 1\n    }\n    // Переход состояний: остальные строки и столбцы\n    for i := 1; i <= n; i++ {\n        for a := 1; a <= amt; a++ {\n            if coins[i-1] > a {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[i][a] = dp[i-1][a]\n            } else {\n                // Сумма двух решений: не брать или взять монету i\n                dp[i][a] = dp[i-1][a] + dp[i][a-coins[i-1]]\n            }\n        }\n    }\n    return dp[n][amt]\n}\n
        coin_change_ii.swift
        /* Размен монет II: динамическое программирование */\nfunc coinChangeIIDP(coins: [Int], amt: Int) -> Int {\n    let n = coins.count\n    // Инициализация таблицы dp\n    var dp = Array(repeating: Array(repeating: 0, count: amt + 1), count: n + 1)\n    // Инициализация первого столбца\n    for i in 0 ... n {\n        dp[i][0] = 1\n    }\n    // Переход состояний\n    for i in 1 ... n {\n        for a in 1 ... amt {\n            if coins[i - 1] > a {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[i][a] = dp[i - 1][a]\n            } else {\n                // Сумма двух решений: не брать или взять монету i\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]\n            }\n        }\n    }\n    return dp[n][amt]\n}\n
        coin_change_ii.js
        /* Размен монет II: динамическое программирование */\nfunction coinChangeIIDP(coins, amt) {\n    const n = coins.length;\n    // Инициализация таблицы dp\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: amt + 1 }, () => 0)\n    );\n    // Инициализация первого столбца\n    for (let i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // Переход состояний\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Сумма двух решений: не брать или взять монету i\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[n][amt];\n}\n
        coin_change_ii.ts
        /* Размен монет II: динамическое программирование */\nfunction coinChangeIIDP(coins: Array<number>, amt: number): number {\n    const n = coins.length;\n    // Инициализация таблицы dp\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: amt + 1 }, () => 0)\n    );\n    // Инициализация первого столбца\n    for (let i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // Переход состояний\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Сумма двух решений: не брать или взять монету i\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[n][amt];\n}\n
        coin_change_ii.dart
        /* Размен монет II: динамическое программирование */\nint coinChangeIIDP(List<int> coins, int amt) {\n  int n = coins.length;\n  // Инициализация таблицы dp\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(amt + 1, 0));\n  // Инициализация первого столбца\n  for (int i = 0; i <= n; i++) {\n    dp[i][0] = 1;\n  }\n  // Переход состояний\n  for (int i = 1; i <= n; i++) {\n    for (int a = 1; a <= amt; a++) {\n      if (coins[i - 1] > a) {\n        // Если целевая сумма превышена, монету i не выбирать\n        dp[i][a] = dp[i - 1][a];\n      } else {\n        // Сумма двух решений: не брать или взять монету i\n        dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n      }\n    }\n  }\n  return dp[n][amt];\n}\n
        coin_change_ii.rs
        /* Размен монет II: динамическое программирование */\nfn coin_change_ii_dp(coins: &[i32], amt: usize) -> i32 {\n    let n = coins.len();\n    // Инициализация таблицы dp\n    let mut dp = vec![vec![0; amt + 1]; n + 1];\n    // Инициализация первого столбца\n    for i in 0..=n {\n        dp[i][0] = 1;\n    }\n    // Переход состояний\n    for i in 1..=n {\n        for a in 1..=amt {\n            if coins[i - 1] > a as i32 {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Сумма двух решений: не брать или взять монету i\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1] as usize];\n            }\n        }\n    }\n    dp[n][amt]\n}\n
        coin_change_ii.c
        /* Размен монет II: динамическое программирование */\nint coinChangeIIDP(int coins[], int amt, int coinsSize) {\n    int n = coinsSize;\n    // Инициализация таблицы dp\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(amt + 1, sizeof(int));\n    }\n    // Инициализация первого столбца\n    for (int i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Сумма двух решений: не брать или взять монету i\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n            }\n        }\n    }\n    int res = dp[n][amt];\n    // Освободить память\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    free(dp);\n    return res;\n}\n
        coin_change_ii.kt
        /* Размен монет II: динамическое программирование */\nfun coinChangeIIDP(coins: IntArray, amt: Int): Int {\n    val n = coins.size\n    // Инициализация таблицы dp\n    val dp = Array(n + 1) { IntArray(amt + 1) }\n    // Инициализация первого столбца\n    for (i in 0..n) {\n        dp[i][0] = 1\n    }\n    // Переход состояний\n    for (i in 1..n) {\n        for (a in 1..amt) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[i][a] = dp[i - 1][a]\n            } else {\n                // Сумма двух решений: не брать или взять монету i\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]\n            }\n        }\n    }\n    return dp[n][amt]\n}\n
        coin_change_ii.rb
        ### Размен монет II: динамическое программирование ###\ndef coin_change_ii_dp(coins, amt)\n  n = coins.length\n  # Инициализация таблицы dp\n  dp = Array.new(n + 1) { Array.new(amt + 1, 0) }\n  # Инициализация первого столбца\n  (0...(n + 1)).each { |i| dp[i][0] = 1 }\n  # Переход состояний\n  for i in 1...(n + 1)\n    for a in 1...(amt + 1)\n      if coins[i - 1] > a\n        # Если целевая сумма превышена, монету i не выбирать\n        dp[i][a] = dp[i - 1][a]\n      else\n        # Сумма двух решений: не брать или взять монету i\n        dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]\n      end\n    end\n  end\n  dp[n][amt]\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 14. Динамическое программирование","14.5   Задача о полном рюкзаке"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#3_2","level":3,"title":"3.   Оптимизация пространства","text":"

        При оптимизации памяти способ остается тем же самым: достаточно убрать измерение, отвечающее за виды монет:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change_ii.py
        def coin_change_ii_dp_comp(coins: list[int], amt: int) -> int:\n    \"\"\"Размен монет II: динамическое программирование с оптимизацией памяти\"\"\"\n    n = len(coins)\n    # Инициализация таблицы dp\n    dp = [0] * (amt + 1)\n    dp[0] = 1\n    # Переход состояний\n    for i in range(1, n + 1):\n        # Прямой обход\n        for a in range(1, amt + 1):\n            if coins[i - 1] > a:\n                # Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a]\n            else:\n                # Сумма двух решений: не брать или взять монету i\n                dp[a] = dp[a] + dp[a - coins[i - 1]]\n    return dp[amt]\n
        coin_change_ii.cpp
        /* Размен монет II: динамическое программирование с оптимизацией памяти */\nint coinChangeIIDPComp(vector<int> &coins, int amt) {\n    int n = coins.size();\n    // Инициализация таблицы dp\n    vector<int> dp(amt + 1, 0);\n    dp[0] = 1;\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a];\n            } else {\n                // Сумма двух решений: не брать или взять монету i\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
        coin_change_ii.java
        /* Размен монет II: динамическое программирование с оптимизацией памяти */\nint coinChangeIIDPComp(int[] coins, int amt) {\n    int n = coins.length;\n    // Инициализация таблицы dp\n    int[] dp = new int[amt + 1];\n    dp[0] = 1;\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a];\n            } else {\n                // Сумма двух решений: не брать или взять монету i\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
        coin_change_ii.cs
        /* Размен монет II: динамическое программирование с оптимизацией памяти */\nint CoinChangeIIDPComp(int[] coins, int amt) {\n    int n = coins.Length;\n    // Инициализация таблицы dp\n    int[] dp = new int[amt + 1];\n    dp[0] = 1;\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a];\n            } else {\n                // Сумма двух решений: не брать или взять монету i\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
        coin_change_ii.go
        /* Размен монет II: динамическое программирование с оптимизацией памяти */\nfunc coinChangeIIDPComp(coins []int, amt int) int {\n    n := len(coins)\n    // Инициализация таблицы dp\n    dp := make([]int, amt+1)\n    dp[0] = 1\n    // Переход состояний\n    for i := 1; i <= n; i++ {\n        // Прямой обход\n        for a := 1; a <= amt; a++ {\n            if coins[i-1] > a {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a]\n            } else {\n                // Сумма двух решений: не брать или взять монету i\n                dp[a] = dp[a] + dp[a-coins[i-1]]\n            }\n        }\n    }\n    return dp[amt]\n}\n
        coin_change_ii.swift
        /* Размен монет II: динамическое программирование с оптимизацией памяти */\nfunc coinChangeIIDPComp(coins: [Int], amt: Int) -> Int {\n    let n = coins.count\n    // Инициализация таблицы dp\n    var dp = Array(repeating: 0, count: amt + 1)\n    dp[0] = 1\n    // Переход состояний\n    for i in 1 ... n {\n        for a in 1 ... amt {\n            if coins[i - 1] > a {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a]\n            } else {\n                // Сумма двух решений: не брать или взять монету i\n                dp[a] = dp[a] + dp[a - coins[i - 1]]\n            }\n        }\n    }\n    return dp[amt]\n}\n
        coin_change_ii.js
        /* Размен монет II: динамическое программирование с оптимизацией памяти */\nfunction coinChangeIIDPComp(coins, amt) {\n    const n = coins.length;\n    // Инициализация таблицы dp\n    const dp = Array.from({ length: amt + 1 }, () => 0);\n    dp[0] = 1;\n    // Переход состояний\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a];\n            } else {\n                // Сумма двух решений: не брать или взять монету i\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
        coin_change_ii.ts
        /* Размен монет II: динамическое программирование с оптимизацией памяти */\nfunction coinChangeIIDPComp(coins: Array<number>, amt: number): number {\n    const n = coins.length;\n    // Инициализация таблицы dp\n    const dp = Array.from({ length: amt + 1 }, () => 0);\n    dp[0] = 1;\n    // Переход состояний\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a];\n            } else {\n                // Сумма двух решений: не брать или взять монету i\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
        coin_change_ii.dart
        /* Размен монет II: динамическое программирование с оптимизацией памяти */\nint coinChangeIIDPComp(List<int> coins, int amt) {\n  int n = coins.length;\n  // Инициализация таблицы dp\n  List<int> dp = List.filled(amt + 1, 0);\n  dp[0] = 1;\n  // Переход состояний\n  for (int i = 1; i <= n; i++) {\n    for (int a = 1; a <= amt; a++) {\n      if (coins[i - 1] > a) {\n        // Если целевая сумма превышена, монету i не выбирать\n        dp[a] = dp[a];\n      } else {\n        // Сумма двух решений: не брать или взять монету i\n        dp[a] = dp[a] + dp[a - coins[i - 1]];\n      }\n    }\n  }\n  return dp[amt];\n}\n
        coin_change_ii.rs
        /* Размен монет II: динамическое программирование с оптимизацией памяти */\nfn coin_change_ii_dp_comp(coins: &[i32], amt: usize) -> i32 {\n    let n = coins.len();\n    // Инициализация таблицы dp\n    let mut dp = vec![0; amt + 1];\n    dp[0] = 1;\n    // Переход состояний\n    for i in 1..=n {\n        for a in 1..=amt {\n            if coins[i - 1] > a as i32 {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a];\n            } else {\n                // Сумма двух решений: не брать или взять монету i\n                dp[a] = dp[a] + dp[a - coins[i - 1] as usize];\n            }\n        }\n    }\n    dp[amt]\n}\n
        coin_change_ii.c
        /* Размен монет II: динамическое программирование с оптимизацией памяти */\nint coinChangeIIDPComp(int coins[], int amt, int coinsSize) {\n    int n = coinsSize;\n    // Инициализация таблицы dp\n    int *dp = calloc(amt + 1, sizeof(int));\n    dp[0] = 1;\n    // Переход состояний\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a];\n            } else {\n                // Сумма двух решений: не брать или взять монету i\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    int res = dp[amt];\n    // Освободить память\n    free(dp);\n    return res;\n}\n
        coin_change_ii.kt
        /* Размен монет II: динамическое программирование с оптимизацией памяти */\nfun coinChangeIIDPComp(coins: IntArray, amt: Int): Int {\n    val n = coins.size\n    // Инициализация таблицы dp\n    val dp = IntArray(amt + 1)\n    dp[0] = 1\n    // Переход состояний\n    for (i in 1..n) {\n        for (a in 1..amt) {\n            if (coins[i - 1] > a) {\n                // Если целевая сумма превышена, монету i не выбирать\n                dp[a] = dp[a]\n            } else {\n                // Сумма двух решений: не брать или взять монету i\n                dp[a] = dp[a] + dp[a - coins[i - 1]]\n            }\n        }\n    }\n    return dp[amt]\n}\n
        coin_change_ii.rb
        ### Размен монет II: динамическое программирование с оптимизацией памяти ###\ndef coin_change_ii_dp_comp(coins, amt)\n  n = coins.length\n  # Инициализация таблицы dp\n  dp = Array.new(amt + 1, 0)\n  dp[0] = 1\n  # Переход состояний\n  for i in 1...(n + 1)\n    # Прямой обход\n    for a in 1...(amt + 1)\n      if coins[i - 1] > a\n        # Если целевая сумма превышена, монету i не выбирать\n        dp[a] = dp[a]\n      else\n        # Сумма двух решений: не брать или взять монету i\n        dp[a] = dp[a] + dp[a - coins[i - 1]]\n      end\n    end\n  end\n  dp[amt]\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 14. Динамическое программирование","14.5   Задача о полном рюкзаке"],"tags":[]},{"location":"chapter_graph/","level":1,"title":"Глава 9.   Графы","text":"

        Abstract

        В жизни мы похожи на вершины, соединенные множеством невидимых ребер.

        Каждая встреча и каждое расставание оставляют в этой огромной сети свой след.

        ","path":["Глава 9. Графы","Глава 9.   Графы"],"tags":[]},{"location":"chapter_graph/#_1","level":2,"title":"Содержание главы","text":"
        • 9.1   Граф
        • 9.2   Базовые операции графа
        • 9.3   Обход графа
        • 9.4   Краткие итоги
        ","path":["Глава 9. Графы","Глава 9.   Графы"],"tags":[]},{"location":"chapter_graph/graph/","level":1,"title":"9.1   Граф","text":"

        Граф (graph) - это нелинейная структура данных, состоящая из вершин (vertex) и ребер (edge). Граф \\(G\\) можно абстрактно представить как множество вершин \\(V\\) и множество ребер \\(E\\) . Ниже приведен пример графа, содержащего 5 вершин и 7 ребер.

        \\[ \\begin{aligned} V & = \\{ 1, 2, 3, 4, 5 \\} \\newline E & = \\{ (1,2), (1,3), (1,5), (2,3), (2,4), (2,5), (4,5) \\} \\newline G & = \\{ V, E \\} \\newline \\end{aligned} \\]

        Если рассматривать вершины как узлы, а ребра как ссылки, соединяющие узлы, граф можно считать структурой данных, расширяющей связный список. Как показано на рисунке 9-1, по сравнению с линейными отношениями (связный список) и отношениями разделения (дерево), сетевые отношения (граф) обладают большей свободой и потому являются более сложными.

        Рисунок 9-1   Связь между связным списком, деревом и графом

        ","path":["Глава 9. Графы","9.1   Граф"],"tags":[]},{"location":"chapter_graph/graph/#911","level":2,"title":"9.1.1   Распространенные типы и термины графов","text":"

        В зависимости от наличия направления у ребер графы делятся на неориентированные графы (undirected graph) и ориентированные графы (directed graph) , как показано на рисунке 9-2.

        • В неориентированном графе ребро представляет двустороннюю связь между двумя вершинами, например дружеские отношения в социальных сетях.
        • В ориентированном графе ребро имеет направление, то есть ребра \\(A \\rightarrow B\\) и \\(A \\leftarrow B\\) независимы друг от друга, например отношения подписки и подписчиков.

        Рисунок 9-2   Ориентированный и неориентированный графы

        В зависимости от того, связаны ли все вершины между собой, граф делится на связный граф (connected graph) и несвязный граф (disconnected graph) , как показано на рисунке 9-3.

        • В связном графе из любой вершины можно достичь любой другой вершины.
        • В несвязном графе существует по крайней мере одна вершина, недостижимая из текущей.

        Рисунок 9-3   Связный и несвязный графы

        Мы также можем добавить к ребрам переменную «вес» и получить взвешенный граф (weighted graph), как показано на рисунке 9-4. Например, в мобильных играх вроде Honor of Kings система рассчитывает «близость» между игроками по времени совместной игры, и такую сеть близости можно представить взвешенным графом.

        Рисунок 9-4   Взвешенный и невзвешенный графы

        Со структурой данных «граф» связаны следующие основные термины.

        • Смежность (adjacency): если между двумя вершинами существует ребро, то такие вершины называются смежными. На рисунке 9-4 с вершиной 1 смежны вершины 2, 3 и 5.
        • Путь (path): последовательность ребер от вершины A до вершины B называется путем из A в B. На рисунке 9-4 последовательность ребер 1-5-2-4 является одним из путей от вершины 1 к вершине 4.
        • Степень (degree): количество ребер, принадлежащих вершине. Для ориентированного графа входящая степень (in-degree) показывает, сколько ребер входит в вершину, а исходящая степень (out-degree) показывает, сколько ребер из нее выходит.
        ","path":["Глава 9. Графы","9.1   Граф"],"tags":[]},{"location":"chapter_graph/graph/#912","level":2,"title":"9.1.2   Представление графа","text":"

        Распространенные способы представления графа включают «матрицу смежности» и «список смежности». Ниже для примера рассматривается неориентированный граф.

        ","path":["Глава 9. Графы","9.1   Граф"],"tags":[]},{"location":"chapter_graph/graph/#1","level":3,"title":"1.   Матрица смежности","text":"

        Пусть число вершин графа равно \\(n\\). Тогда матрица смежности (adjacency matrix) использует матрицу размера \\(n \\times n\\) для представления графа, где каждая строка и каждый столбец соответствуют вершине, а элементы матрицы показывают наличие или отсутствие ребра.

        Как показано на рисунке 9-5, обозначим матрицу смежности через \\(M\\) , а список вершин через \\(V\\). Тогда элемент матрицы \\(M[i, j] = 1\\) означает наличие ребра между вершинами \\(V[i]\\) и \\(V[j]\\) , а элемент \\(M[i, j] = 0\\) означает отсутствие ребра.

        Рисунок 9-5   Представление графа матрицей смежности

        Матрица смежности обладает следующими особенностями.

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

        При представлении графа матрицей смежности можно напрямую обращаться к элементам матрицы и получать сведения о ребрах, поэтому операции добавления, удаления, поиска и изменения обладают высокой эффективностью и выполняются за \\(O(1)\\) . Однако пространственная сложность матрицы составляет \\(O(n^2)\\) , поэтому она требует значительных затрат памяти.

        ","path":["Глава 9. Графы","9.1   Граф"],"tags":[]},{"location":"chapter_graph/graph/#2","level":3,"title":"2.   Список смежности","text":"

        Список смежности (adjacency list) использует \\(n\\) списков для представления графа, где узлы списка обозначают вершины. \\(i\\)-й список соответствует вершине \\(i\\) и хранит все смежные с ней вершины, то есть все вершины, соединенные с данной вершиной. На рисунке 9-6 показан пример графа, представленного списком смежности.

        Рисунок 9-6   Представление графа списком смежности

        Список смежности хранит только реально существующие ребра, а общее число ребер обычно значительно меньше \\(n^2\\) , поэтому он лучше экономит память. Однако для поиска ребра в списке смежности требуется обходить список, поэтому по времени он уступает матрице смежности.

        Как видно на рисунке 9-6, структура списка смежности очень похожа на цепную адресацию в хеш-таблицах, поэтому здесь можно использовать похожие методы оптимизации эффективности. Например, если список слишком длинный, его можно преобразовать в AVL-дерево или красно-черное дерево, чтобы снизить временную сложность с \\(O(n)\\) до \\(O(\\log n)\\). Также список можно преобразовать в хеш-таблицу, чтобы довести временную сложность до \\(O(1)\\) .

        ","path":["Глава 9. Графы","9.1   Граф"],"tags":[]},{"location":"chapter_graph/graph/#913","level":2,"title":"9.1.3   Типичные применения графов","text":"

        Как показано в таблице 9-1, многие реальные системы можно моделировать с помощью графов, а соответствующие задачи затем сводить к задачам вычислений на графах.

        Таблица 9-1   Распространенные графы в реальной жизни

        Вершина Ребро Задача вычислений на графе Социальные сети Пользователь Дружеская связь Рекомендация потенциальных друзей Линии метро Станция Связь между станциями Рекомендация кратчайшего маршрута Солнечная система Небесное тело Гравитационное взаимодействие между телами Вычисление орбит планет","path":["Глава 9. Графы","9.1   Граф"],"tags":[]},{"location":"chapter_graph/graph_operations/","level":1,"title":"9.2   Базовые операции графа","text":"

        Базовые операции графа можно разделить на операции над «ребрами» и операции над «вершинами». При двух способах представления, «матрице смежности» и «списке смежности», реализация этих операций различается.

        ","path":["Глава 9. Графы","9.2   Базовые операции графа"],"tags":[]},{"location":"chapter_graph/graph_operations/#921","level":2,"title":"9.2.1   Реализация на основе матрицы смежности","text":"

        Пусть дан неориентированный граф с числом вершин \\(n\\) . Тогда способы реализации различных операций показаны на рисунке 9-7.

        • Добавление или удаление ребра: достаточно изменить соответствующее ребро в матрице смежности, что требует \\(O(1)\\) времени. Поскольку граф неориентированный, необходимо одновременно обновить ребра в обоих направлениях.
        • Добавление вершины: в конец матрицы смежности добавляется строка и столбец, полностью заполненные нулями. Это требует \\(O(n)\\) времени.
        • Удаление вершины: из матрицы смежности удаляется строка и столбец. При удалении первой строки и первого столбца достигается худший случай, когда требуется «сдвинуть влево вверх» \\((n-1)^2\\) элементов, поэтому используется \\(O(n^2)\\) времени.
        • Инициализация: передаются \\(n\\) вершин, затем инициализируется список вершин vertices длины \\(n\\) , что требует \\(O(n)\\) времени. После этого инициализируется матрица смежности adjMat размера \\(n \\times n\\) , что требует \\(O(n^2)\\) времени.
        <1><2><3><4><5>

        Рисунок 9-7   Инициализация матрицы смежности, добавление и удаление ребер и вершин

        Ниже приведен код реализации графа на основе матрицы смежности:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_adjacency_matrix.py
        class GraphAdjMat:\n    \"\"\"Класс неориентированного графа на основе матрицы смежности\"\"\"\n\n    def __init__(self, vertices: list[int], edges: list[list[int]]):\n        \"\"\"Конструктор\"\"\"\n        # Список вершин: элементы представляют «значения вершин», а индексы — «индексы вершин»\n        self.vertices: list[int] = []\n        # Матрица смежности, где индексы строк и столбцов соответствуют «индексам вершин»\n        self.adj_mat: list[list[int]] = []\n        # Добавление вершины\n        for val in vertices:\n            self.add_vertex(val)\n        # Добавить ребра\n        # Обратите внимание: элементы edges представляют собой индексы вершин, то есть соответствуют индексам элементов vertices\n        for e in edges:\n            self.add_edge(e[0], e[1])\n\n    def size(self) -> int:\n        \"\"\"Получить число вершин\"\"\"\n        return len(self.vertices)\n\n    def add_vertex(self, val: int):\n        \"\"\"Добавление вершины\"\"\"\n        n = self.size()\n        # Добавить значение новой вершины в список вершин\n        self.vertices.append(val)\n        # Добавить строку в матрицу смежности\n        new_row = [0] * n\n        self.adj_mat.append(new_row)\n        # Добавить столбец в матрицу смежности\n        for row in self.adj_mat:\n            row.append(0)\n\n    def remove_vertex(self, index: int):\n        \"\"\"Удаление вершины\"\"\"\n        if index >= self.size():\n            raise IndexError()\n        # Удалить вершину с индексом index из списка вершин\n        self.vertices.pop(index)\n        # Удалить строку с индексом index из матрицы смежности\n        self.adj_mat.pop(index)\n        # Удалить столбец с индексом index из матрицы смежности\n        for row in self.adj_mat:\n            row.pop(index)\n\n    def add_edge(self, i: int, j: int):\n        \"\"\"Добавление ребра\"\"\"\n        # Параметры i и j соответствуют индексам элементов vertices\n        # Обработка выхода индекса за границы и случая равенства\n        if i < 0 or j < 0 or i >= self.size() or j >= self.size() or i == j:\n            raise IndexError()\n        # В неориентированном графе матрица смежности симметрична относительно главной диагонали, то есть выполняется (i, j) == (j, i)\n        self.adj_mat[i][j] = 1\n        self.adj_mat[j][i] = 1\n\n    def remove_edge(self, i: int, j: int):\n        \"\"\"Удаление ребра\"\"\"\n        # Параметры i и j соответствуют индексам элементов vertices\n        # Обработка выхода индекса за границы и случая равенства\n        if i < 0 or j < 0 or i >= self.size() or j >= self.size() or i == j:\n            raise IndexError()\n        self.adj_mat[i][j] = 0\n        self.adj_mat[j][i] = 0\n\n    def print(self):\n        \"\"\"Вывести матрицу смежности\"\"\"\n        print(\"Список вершин =\", self.vertices)\n        print(\"Матрица смежности =\")\n        print_matrix(self.adj_mat)\n
        graph_adjacency_matrix.cpp
        /* Класс неориентированного графа на основе матрицы смежности */\nclass GraphAdjMat {\n    vector<int> vertices;       // Список вершин: элементы представляют «значения вершин», а индексы — «индексы вершин»\n    vector<vector<int>> adjMat; // Матрица смежности, где индексы строк и столбцов соответствуют «индексам вершин»\n\n  public:\n    /* Конструктор */\n    GraphAdjMat(const vector<int> &vertices, const vector<vector<int>> &edges) {\n        // Добавление вершины\n        for (int val : vertices) {\n            addVertex(val);\n        }\n        // Добавить ребра\n        // Обратите внимание: элементы edges представляют собой индексы вершин, то есть соответствуют индексам элементов vertices\n        for (const vector<int> &edge : edges) {\n            addEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* Получить число вершин */\n    int size() const {\n        return vertices.size();\n    }\n\n    /* Добавление вершины */\n    void addVertex(int val) {\n        int n = size();\n        // Добавить значение новой вершины в список вершин\n        vertices.push_back(val);\n        // Добавить строку в матрицу смежности\n        adjMat.emplace_back(vector<int>(n, 0));\n        // Добавить столбец в матрицу смежности\n        for (vector<int> &row : adjMat) {\n            row.push_back(0);\n        }\n    }\n\n    /* Удаление вершины */\n    void removeVertex(int index) {\n        if (index >= size()) {\n            throw out_of_range(\"вершина не существует\");\n        }\n        // Удалить вершину с индексом index из списка вершин\n        vertices.erase(vertices.begin() + index);\n        // Удалить строку с индексом index из матрицы смежности\n        adjMat.erase(adjMat.begin() + index);\n        // Удалить столбец с индексом index из матрицы смежности\n        for (vector<int> &row : adjMat) {\n            row.erase(row.begin() + index);\n        }\n    }\n\n    /* Добавление ребра */\n    // Параметры i и j соответствуют индексам элементов vertices\n    void addEdge(int i, int j) {\n        // Обработка выхода индекса за границы и случая равенства\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {\n            throw out_of_range(\"вершина не существует\");\n        }\n        // В неориентированном графе матрица смежности симметрична относительно главной диагонали, то есть выполняется (i, j) == (j, i)\n        adjMat[i][j] = 1;\n        adjMat[j][i] = 1;\n    }\n\n    /* Удаление ребра */\n    // Параметры i и j соответствуют индексам элементов vertices\n    void removeEdge(int i, int j) {\n        // Обработка выхода индекса за границы и случая равенства\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {\n            throw out_of_range(\"вершина не существует\");\n        }\n        adjMat[i][j] = 0;\n        adjMat[j][i] = 0;\n    }\n\n    /* Вывести матрицу смежности */\n    void print() {\n        cout << \"Список вершин = \";\n        printVector(vertices);\n        cout << \"Матрица смежности =\" << endl;\n        printVectorMatrix(adjMat);\n    }\n};\n
        graph_adjacency_matrix.java
        /* Класс неориентированного графа на основе матрицы смежности */\nclass GraphAdjMat {\n    List<Integer> vertices; // Список вершин: элементы представляют «значения вершин», а индексы — «индексы вершин»\n    List<List<Integer>> adjMat; // Матрица смежности, где индексы строк и столбцов соответствуют «индексам вершин»\n\n    /* Конструктор */\n    public GraphAdjMat(int[] vertices, int[][] edges) {\n        this.vertices = new ArrayList<>();\n        this.adjMat = new ArrayList<>();\n        // Добавление вершины\n        for (int val : vertices) {\n            addVertex(val);\n        }\n        // Добавить ребра\n        // Обратите внимание: элементы edges представляют собой индексы вершин, то есть соответствуют индексам элементов vertices\n        for (int[] e : edges) {\n            addEdge(e[0], e[1]);\n        }\n    }\n\n    /* Получить число вершин */\n    public int size() {\n        return vertices.size();\n    }\n\n    /* Добавление вершины */\n    public void addVertex(int val) {\n        int n = size();\n        // Добавить значение новой вершины в список вершин\n        vertices.add(val);\n        // Добавить строку в матрицу смежности\n        List<Integer> newRow = new ArrayList<>(n);\n        for (int j = 0; j < n; j++) {\n            newRow.add(0);\n        }\n        adjMat.add(newRow);\n        // Добавить столбец в матрицу смежности\n        for (List<Integer> row : adjMat) {\n            row.add(0);\n        }\n    }\n\n    /* Удаление вершины */\n    public void removeVertex(int index) {\n        if (index >= size())\n            throw new IndexOutOfBoundsException();\n        // Удалить вершину с индексом index из списка вершин\n        vertices.remove(index);\n        // Удалить строку с индексом index из матрицы смежности\n        adjMat.remove(index);\n        // Удалить столбец с индексом index из матрицы смежности\n        for (List<Integer> row : adjMat) {\n            row.remove(index);\n        }\n    }\n\n    /* Добавление ребра */\n    // Параметры i и j соответствуют индексам элементов vertices\n    public void addEdge(int i, int j) {\n        // Обработка выхода индекса за границы и случая равенства\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j)\n            throw new IndexOutOfBoundsException();\n        // В неориентированном графе матрица смежности симметрична относительно главной диагонали, то есть выполняется (i, j) == (j, i)\n        adjMat.get(i).set(j, 1);\n        adjMat.get(j).set(i, 1);\n    }\n\n    /* Удаление ребра */\n    // Параметры i и j соответствуют индексам элементов vertices\n    public void removeEdge(int i, int j) {\n        // Обработка выхода индекса за границы и случая равенства\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j)\n            throw new IndexOutOfBoundsException();\n        adjMat.get(i).set(j, 0);\n        adjMat.get(j).set(i, 0);\n    }\n\n    /* Вывести матрицу смежности */\n    public void print() {\n        System.out.print(\"Список вершин = \");\n        System.out.println(vertices);\n        System.out.println(\"Матрица смежности =\");\n        PrintUtil.printMatrix(adjMat);\n    }\n}\n
        graph_adjacency_matrix.cs
        /* Класс неориентированного графа на основе матрицы смежности */\nclass GraphAdjMat {\n    List<int> vertices;     // Список вершин: элементы представляют «значения вершин», а индексы — «индексы вершин»\n    List<List<int>> adjMat; // Матрица смежности, где индексы строк и столбцов соответствуют «индексам вершин»\n\n    /* Конструктор */\n    public GraphAdjMat(int[] vertices, int[][] edges) {\n        this.vertices = [];\n        this.adjMat = [];\n        // Добавление вершины\n        foreach (int val in vertices) {\n            AddVertex(val);\n        }\n        // Добавить ребра\n        // Обратите внимание: элементы edges представляют собой индексы вершин, то есть соответствуют индексам элементов vertices\n        foreach (int[] e in edges) {\n            AddEdge(e[0], e[1]);\n        }\n    }\n\n    /* Получить число вершин */\n    int Size() {\n        return vertices.Count;\n    }\n\n    /* Добавление вершины */\n    public void AddVertex(int val) {\n        int n = Size();\n        // Добавить значение новой вершины в список вершин\n        vertices.Add(val);\n        // Добавить строку в матрицу смежности\n        List<int> newRow = new(n);\n        for (int j = 0; j < n; j++) {\n            newRow.Add(0);\n        }\n        adjMat.Add(newRow);\n        // Добавить столбец в матрицу смежности\n        foreach (List<int> row in adjMat) {\n            row.Add(0);\n        }\n    }\n\n    /* Удаление вершины */\n    public void RemoveVertex(int index) {\n        if (index >= Size())\n            throw new IndexOutOfRangeException();\n        // Удалить вершину с индексом index из списка вершин\n        vertices.RemoveAt(index);\n        // Удалить строку с индексом index из матрицы смежности\n        adjMat.RemoveAt(index);\n        // Удалить столбец с индексом index из матрицы смежности\n        foreach (List<int> row in adjMat) {\n            row.RemoveAt(index);\n        }\n    }\n\n    /* Добавление ребра */\n    // Параметры i и j соответствуют индексам элементов vertices\n    public void AddEdge(int i, int j) {\n        // Обработка выхода индекса за границы и случая равенства\n        if (i < 0 || j < 0 || i >= Size() || j >= Size() || i == j)\n            throw new IndexOutOfRangeException();\n        // В неориентированном графе матрица смежности симметрична относительно главной диагонали, то есть выполняется (i, j) == (j, i)\n        adjMat[i][j] = 1;\n        adjMat[j][i] = 1;\n    }\n\n    /* Удаление ребра */\n    // Параметры i и j соответствуют индексам элементов vertices\n    public void RemoveEdge(int i, int j) {\n        // Обработка выхода индекса за границы и случая равенства\n        if (i < 0 || j < 0 || i >= Size() || j >= Size() || i == j)\n            throw new IndexOutOfRangeException();\n        adjMat[i][j] = 0;\n        adjMat[j][i] = 0;\n    }\n\n    /* Вывести матрицу смежности */\n    public void Print() {\n        Console.Write(\"Список вершин = \");\n        PrintUtil.PrintList(vertices);\n        Console.WriteLine(\"Матрица смежности =\");\n        PrintUtil.PrintMatrix(adjMat);\n    }\n}\n
        graph_adjacency_matrix.go
        /* Класс неориентированного графа на основе матрицы смежности */\ntype graphAdjMat struct {\n    // Список вершин: элементы представляют «значения вершин», а индексы — «индексы вершин»\n    vertices []int\n    // Матрица смежности, где индексы строк и столбцов соответствуют «индексам вершин»\n    adjMat [][]int\n}\n\n/* Конструктор */\nfunc newGraphAdjMat(vertices []int, edges [][]int) *graphAdjMat {\n    // Добавление вершины\n    n := len(vertices)\n    adjMat := make([][]int, n)\n    for i := range adjMat {\n        adjMat[i] = make([]int, n)\n    }\n    // Инициализировать граф\n    g := &graphAdjMat{\n        vertices: vertices,\n        adjMat:   adjMat,\n    }\n    // Добавить ребра\n    // Обратите внимание: элементы edges представляют собой индексы вершин, то есть соответствуют индексам элементов vertices\n    for i := range edges {\n        g.addEdge(edges[i][0], edges[i][1])\n    }\n    return g\n}\n\n/* Получить число вершин */\nfunc (g *graphAdjMat) size() int {\n    return len(g.vertices)\n}\n\n/* Добавление вершины */\nfunc (g *graphAdjMat) addVertex(val int) {\n    n := g.size()\n    // Добавить значение новой вершины в список вершин\n    g.vertices = append(g.vertices, val)\n    // Добавить строку в матрицу смежности\n    newRow := make([]int, n)\n    g.adjMat = append(g.adjMat, newRow)\n    // Добавить столбец в матрицу смежности\n    for i := range g.adjMat {\n        g.adjMat[i] = append(g.adjMat[i], 0)\n    }\n}\n\n/* Удаление вершины */\nfunc (g *graphAdjMat) removeVertex(index int) {\n    if index >= g.size() {\n        return\n    }\n    // Удалить вершину с индексом index из списка вершин\n    g.vertices = append(g.vertices[:index], g.vertices[index+1:]...)\n    // Удалить строку с индексом index из матрицы смежности\n    g.adjMat = append(g.adjMat[:index], g.adjMat[index+1:]...)\n    // Удалить столбец с индексом index из матрицы смежности\n    for i := range g.adjMat {\n        g.adjMat[i] = append(g.adjMat[i][:index], g.adjMat[i][index+1:]...)\n    }\n}\n\n/* Добавление ребра */\n// Параметры i и j соответствуют индексам элементов vertices\nfunc (g *graphAdjMat) addEdge(i, j int) {\n    // Обработка выхода индекса за границы и случая равенства\n    if i < 0 || j < 0 || i >= g.size() || j >= g.size() || i == j {\n        fmt.Errorf(\"%s\", \"Index Out Of Bounds Exception\")\n    }\n    // В неориентированном графе матрица смежности симметрична относительно главной диагонали, то есть выполняется (i, j) == (j, i)\n    g.adjMat[i][j] = 1\n    g.adjMat[j][i] = 1\n}\n\n/* Удаление ребра */\n// Параметры i и j соответствуют индексам элементов vertices\nfunc (g *graphAdjMat) removeEdge(i, j int) {\n    // Обработка выхода индекса за границы и случая равенства\n    if i < 0 || j < 0 || i >= g.size() || j >= g.size() || i == j {\n        fmt.Errorf(\"%s\", \"Index Out Of Bounds Exception\")\n    }\n    g.adjMat[i][j] = 0\n    g.adjMat[j][i] = 0\n}\n\n/* Вывести матрицу смежности */\nfunc (g *graphAdjMat) print() {\n    fmt.Printf(\"\\tСписок вершин = %v\\n\", g.vertices)\n    fmt.Printf(\"\\tМатрица смежности = \\n\")\n    for i := range g.adjMat {\n        fmt.Printf(\"\\t\\t\\t%v\\n\", g.adjMat[i])\n    }\n}\n
        graph_adjacency_matrix.swift
        /* Класс неориентированного графа на основе матрицы смежности */\nclass GraphAdjMat {\n    private var vertices: [Int] // Список вершин: элементы представляют «значения вершин», а индексы — «индексы вершин»\n    private var adjMat: [[Int]] // Матрица смежности, где индексы строк и столбцов соответствуют «индексам вершин»\n\n    /* Конструктор */\n    init(vertices: [Int], edges: [[Int]]) {\n        self.vertices = []\n        adjMat = []\n        // Добавление вершины\n        for val in vertices {\n            addVertex(val: val)\n        }\n        // Добавить ребра\n        // Обратите внимание: элементы edges представляют собой индексы вершин, то есть соответствуют индексам элементов vertices\n        for e in edges {\n            addEdge(i: e[0], j: e[1])\n        }\n    }\n\n    /* Получить число вершин */\n    func size() -> Int {\n        vertices.count\n    }\n\n    /* Добавление вершины */\n    func addVertex(val: Int) {\n        let n = size()\n        // Добавить значение новой вершины в список вершин\n        vertices.append(val)\n        // Добавить строку в матрицу смежности\n        let newRow = Array(repeating: 0, count: n)\n        adjMat.append(newRow)\n        // Добавить столбец в матрицу смежности\n        for i in adjMat.indices {\n            adjMat[i].append(0)\n        }\n    }\n\n    /* Удаление вершины */\n    func removeVertex(index: Int) {\n        if index >= size() {\n            fatalError(\"Выход за границы диапазона\")\n        }\n        // Удалить вершину с индексом index из списка вершин\n        vertices.remove(at: index)\n        // Удалить строку с индексом index из матрицы смежности\n        adjMat.remove(at: index)\n        // Удалить столбец с индексом index из матрицы смежности\n        for i in adjMat.indices {\n            adjMat[i].remove(at: index)\n        }\n    }\n\n    /* Добавление ребра */\n    // Параметры i и j соответствуют индексам элементов vertices\n    func addEdge(i: Int, j: Int) {\n        // Обработка выхода индекса за границы и случая равенства\n        if i < 0 || j < 0 || i >= size() || j >= size() || i == j {\n            fatalError(\"Выход за границы диапазона\")\n        }\n        // В неориентированном графе матрица смежности симметрична относительно главной диагонали, то есть выполняется (i, j) == (j, i)\n        adjMat[i][j] = 1\n        adjMat[j][i] = 1\n    }\n\n    /* Удаление ребра */\n    // Параметры i и j соответствуют индексам элементов vertices\n    func removeEdge(i: Int, j: Int) {\n        // Обработка выхода индекса за границы и случая равенства\n        if i < 0 || j < 0 || i >= size() || j >= size() || i == j {\n            fatalError(\"Выход за границы диапазона\")\n        }\n        adjMat[i][j] = 0\n        adjMat[j][i] = 0\n    }\n\n    /* Вывести матрицу смежности */\n    func print() {\n        Swift.print(\"Список вершин = \", terminator: \"\")\n        Swift.print(vertices)\n        Swift.print(\"Матрица смежности =\")\n        PrintUtil.printMatrix(matrix: adjMat)\n    }\n}\n
        graph_adjacency_matrix.js
        /* Класс неориентированного графа на основе матрицы смежности */\nclass GraphAdjMat {\n    vertices; // Список вершин: элементы представляют «значения вершин», а индексы — «индексы вершин»\n    adjMat; // Матрица смежности, где индексы строк и столбцов соответствуют «индексам вершин»\n\n    /* Конструктор */\n    constructor(vertices, edges) {\n        this.vertices = [];\n        this.adjMat = [];\n        // Добавление вершины\n        for (const val of vertices) {\n            this.addVertex(val);\n        }\n        // Добавить ребра\n        // Обратите внимание: элементы edges представляют собой индексы вершин, то есть соответствуют индексам элементов vertices\n        for (const e of edges) {\n            this.addEdge(e[0], e[1]);\n        }\n    }\n\n    /* Получить число вершин */\n    size() {\n        return this.vertices.length;\n    }\n\n    /* Добавление вершины */\n    addVertex(val) {\n        const n = this.size();\n        // Добавить значение новой вершины в список вершин\n        this.vertices.push(val);\n        // Добавить строку в матрицу смежности\n        const newRow = [];\n        for (let j = 0; j < n; j++) {\n            newRow.push(0);\n        }\n        this.adjMat.push(newRow);\n        // Добавить столбец в матрицу смежности\n        for (const row of this.adjMat) {\n            row.push(0);\n        }\n    }\n\n    /* Удаление вершины */\n    removeVertex(index) {\n        if (index >= this.size()) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        // Удалить вершину с индексом index из списка вершин\n        this.vertices.splice(index, 1);\n\n        // Удалить строку с индексом index из матрицы смежности\n        this.adjMat.splice(index, 1);\n        // Удалить столбец с индексом index из матрицы смежности\n        for (const row of this.adjMat) {\n            row.splice(index, 1);\n        }\n    }\n\n    /* Добавление ребра */\n    // Параметры i и j соответствуют индексам элементов vertices\n    addEdge(i, j) {\n        // Обработка выхода индекса за границы и случая равенства\n        if (i < 0 || j < 0 || i >= this.size() || j >= this.size() || i === j) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        // В неориентированном графе матрица смежности симметрична относительно главной диагонали, то есть выполняется (i, j) === (j, i)\n        this.adjMat[i][j] = 1;\n        this.adjMat[j][i] = 1;\n    }\n\n    /* Удаление ребра */\n    // Параметры i и j соответствуют индексам элементов vertices\n    removeEdge(i, j) {\n        // Обработка выхода индекса за границы и случая равенства\n        if (i < 0 || j < 0 || i >= this.size() || j >= this.size() || i === j) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        this.adjMat[i][j] = 0;\n        this.adjMat[j][i] = 0;\n    }\n\n    /* Вывести матрицу смежности */\n    print() {\n        console.log('Список вершин = ', this.vertices);\n        console.log('Матрица смежности =', this.adjMat);\n    }\n}\n
        graph_adjacency_matrix.ts
        /* Класс неориентированного графа на основе матрицы смежности */\nclass GraphAdjMat {\n    vertices: number[]; // Список вершин: элементы представляют «значения вершин», а индексы — «индексы вершин»\n    adjMat: number[][]; // Матрица смежности, где индексы строк и столбцов соответствуют «индексам вершин»\n\n    /* Конструктор */\n    constructor(vertices: number[], edges: number[][]) {\n        this.vertices = [];\n        this.adjMat = [];\n        // Добавление вершины\n        for (const val of vertices) {\n            this.addVertex(val);\n        }\n        // Добавить ребра\n        // Обратите внимание: элементы edges представляют собой индексы вершин, то есть соответствуют индексам элементов vertices\n        for (const e of edges) {\n            this.addEdge(e[0], e[1]);\n        }\n    }\n\n    /* Получить число вершин */\n    size(): number {\n        return this.vertices.length;\n    }\n\n    /* Добавление вершины */\n    addVertex(val: number): void {\n        const n: number = this.size();\n        // Добавить значение новой вершины в список вершин\n        this.vertices.push(val);\n        // Добавить строку в матрицу смежности\n        const newRow: number[] = [];\n        for (let j: number = 0; j < n; j++) {\n            newRow.push(0);\n        }\n        this.adjMat.push(newRow);\n        // Добавить столбец в матрицу смежности\n        for (const row of this.adjMat) {\n            row.push(0);\n        }\n    }\n\n    /* Удаление вершины */\n    removeVertex(index: number): void {\n        if (index >= this.size()) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        // Удалить вершину с индексом index из списка вершин\n        this.vertices.splice(index, 1);\n\n        // Удалить строку с индексом index из матрицы смежности\n        this.adjMat.splice(index, 1);\n        // Удалить столбец с индексом index из матрицы смежности\n        for (const row of this.adjMat) {\n            row.splice(index, 1);\n        }\n    }\n\n    /* Добавление ребра */\n    // Параметры i и j соответствуют индексам элементов vertices\n    addEdge(i: number, j: number): void {\n        // Обработка выхода индекса за границы и случая равенства\n        if (i < 0 || j < 0 || i >= this.size() || j >= this.size() || i === j) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        // В неориентированном графе матрица смежности симметрична относительно главной диагонали, то есть выполняется (i, j) === (j, i)\n        this.adjMat[i][j] = 1;\n        this.adjMat[j][i] = 1;\n    }\n\n    /* Удаление ребра */\n    // Параметры i и j соответствуют индексам элементов vertices\n    removeEdge(i: number, j: number): void {\n        // Обработка выхода индекса за границы и случая равенства\n        if (i < 0 || j < 0 || i >= this.size() || j >= this.size() || i === j) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        this.adjMat[i][j] = 0;\n        this.adjMat[j][i] = 0;\n    }\n\n    /* Вывести матрицу смежности */\n    print(): void {\n        console.log('Список вершин = ', this.vertices);\n        console.log('Матрица смежности =', this.adjMat);\n    }\n}\n
        graph_adjacency_matrix.dart
        /* Класс неориентированного графа на основе матрицы смежности */\nclass GraphAdjMat {\n  List<int> vertices = []; // Элемент вершины: элемент представляет «значение вершины», индекс представляет «индекс вершины»\n  List<List<int>> adjMat = []; // Матрица смежности, где индексы строк и столбцов соответствуют «индексам вершин»\n\n  /* Конструктор */\n  GraphAdjMat(List<int> vertices, List<List<int>> edges) {\n    this.vertices = [];\n    this.adjMat = [];\n    // Добавление вершины\n    for (int val in vertices) {\n      addVertex(val);\n    }\n    // Добавить ребра\n    // Обратите внимание: элементы edges представляют собой индексы вершин, то есть соответствуют индексам элементов vertices\n    for (List<int> e in edges) {\n      addEdge(e[0], e[1]);\n    }\n  }\n\n  /* Получить число вершин */\n  int size() {\n    return vertices.length;\n  }\n\n  /* Добавление вершины */\n  void addVertex(int val) {\n    int n = size();\n    // Добавить значение новой вершины в список вершин\n    vertices.add(val);\n    // Добавить строку в матрицу смежности\n    List<int> newRow = List.filled(n, 0, growable: true);\n    adjMat.add(newRow);\n    // Добавить столбец в матрицу смежности\n    for (List<int> row in adjMat) {\n      row.add(0);\n    }\n  }\n\n  /* Удаление вершины */\n  void removeVertex(int index) {\n    if (index >= size()) {\n      throw IndexError;\n    }\n    // Удалить вершину с индексом index из списка вершин\n    vertices.removeAt(index);\n    // Удалить строку с индексом index из матрицы смежности\n    adjMat.removeAt(index);\n    // Удалить столбец с индексом index из матрицы смежности\n    for (List<int> row in adjMat) {\n      row.removeAt(index);\n    }\n  }\n\n  /* Добавление ребра */\n  // Параметры i и j соответствуют индексам элементов vertices\n  void addEdge(int i, int j) {\n    // Обработка выхода индекса за границы и случая равенства\n    if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {\n      throw IndexError;\n    }\n    // В неориентированном графе матрица смежности симметрична относительно главной диагонали, то есть выполняется (i, j) == (j, i)\n    adjMat[i][j] = 1;\n    adjMat[j][i] = 1;\n  }\n\n  /* Удаление ребра */\n  // Параметры i и j соответствуют индексам элементов vertices\n  void removeEdge(int i, int j) {\n    // Обработка выхода индекса за границы и случая равенства\n    if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {\n      throw IndexError;\n    }\n    adjMat[i][j] = 0;\n    adjMat[j][i] = 0;\n  }\n\n  /* Вывести матрицу смежности */\n  void printAdjMat() {\n    print(\"Список вершин = $vertices\");\n    print(\"Матрица смежности = \");\n    printMatrix(adjMat);\n  }\n}\n
        graph_adjacency_matrix.rs
        /* Тип неориентированного графа на основе матрицы смежности */\npub struct GraphAdjMat {\n    // Список вершин: элементы представляют «значения вершин», а индексы — «индексы вершин»\n    pub vertices: Vec<i32>,\n    // Матрица смежности, где индексы строк и столбцов соответствуют «индексам вершин»\n    pub adj_mat: Vec<Vec<i32>>,\n}\n\nimpl GraphAdjMat {\n    /* Конструктор */\n    pub fn new(vertices: Vec<i32>, edges: Vec<[usize; 2]>) -> Self {\n        let mut graph = GraphAdjMat {\n            vertices: vec![],\n            adj_mat: vec![],\n        };\n        // Добавление вершины\n        for val in vertices {\n            graph.add_vertex(val);\n        }\n        // Добавить ребра\n        // Обратите внимание: элементы edges представляют собой индексы вершин, то есть соответствуют индексам элементов vertices\n        for edge in edges {\n            graph.add_edge(edge[0], edge[1])\n        }\n\n        graph\n    }\n\n    /* Получить число вершин */\n    pub fn size(&self) -> usize {\n        self.vertices.len()\n    }\n\n    /* Добавление вершины */\n    pub fn add_vertex(&mut self, val: i32) {\n        let n = self.size();\n        // Добавить значение новой вершины в список вершин\n        self.vertices.push(val);\n        // Добавить строку в матрицу смежности\n        self.adj_mat.push(vec![0; n]);\n        // Добавить столбец в матрицу смежности\n        for row in self.adj_mat.iter_mut() {\n            row.push(0);\n        }\n    }\n\n    /* Удаление вершины */\n    pub fn remove_vertex(&mut self, index: usize) {\n        if index >= self.size() {\n            panic!(\"index error\")\n        }\n        // Удалить вершину с индексом index из списка вершин\n        self.vertices.remove(index);\n        // Удалить строку с индексом index из матрицы смежности\n        self.adj_mat.remove(index);\n        // Удалить столбец с индексом index из матрицы смежности\n        for row in self.adj_mat.iter_mut() {\n            row.remove(index);\n        }\n    }\n\n    /* Добавление ребра */\n    pub fn add_edge(&mut self, i: usize, j: usize) {\n        // Параметры i и j соответствуют индексам элементов vertices\n        // Обработка выхода индекса за границы и случая равенства\n        if i >= self.size() || j >= self.size() || i == j {\n            panic!(\"index error\")\n        }\n        // В неориентированном графе матрица смежности симметрична относительно главной диагонали, то есть выполняется (i, j) == (j, i)\n        self.adj_mat[i][j] = 1;\n        self.adj_mat[j][i] = 1;\n    }\n\n    /* Удаление ребра */\n    // Параметры i и j соответствуют индексам элементов vertices\n    pub fn remove_edge(&mut self, i: usize, j: usize) {\n        // Параметры i и j соответствуют индексам элементов vertices\n        // Обработка выхода индекса за границы и случая равенства\n        if i >= self.size() || j >= self.size() || i == j {\n            panic!(\"index error\")\n        }\n        self.adj_mat[i][j] = 0;\n        self.adj_mat[j][i] = 0;\n    }\n\n    /* Вывести матрицу смежности */\n    pub fn print(&self) {\n        println!(\"Список вершин = {:?}\", self.vertices);\n        println!(\"Матрица смежности =\");\n        println!(\"[\");\n        for row in &self.adj_mat {\n            println!(\"  {:?},\", row);\n        }\n        println!(\"]\")\n    }\n}\n
        graph_adjacency_matrix.c
        /* Структура неориентированного графа на основе матрицы смежности */\ntypedef struct {\n    int vertices[MAX_SIZE];\n    int adjMat[MAX_SIZE][MAX_SIZE];\n    int size;\n} GraphAdjMat;\n\n/* Конструктор */\nGraphAdjMat *newGraphAdjMat() {\n    GraphAdjMat *graph = (GraphAdjMat *)malloc(sizeof(GraphAdjMat));\n    graph->size = 0;\n    for (int i = 0; i < MAX_SIZE; i++) {\n        for (int j = 0; j < MAX_SIZE; j++) {\n            graph->adjMat[i][j] = 0;\n        }\n    }\n    return graph;\n}\n\n/* Деструктор */\nvoid delGraphAdjMat(GraphAdjMat *graph) {\n    free(graph);\n}\n\n/* Добавление вершины */\nvoid addVertex(GraphAdjMat *graph, int val) {\n    if (graph->size == MAX_SIZE) {\n        fprintf(stderr, \"Количество вершин графа уже достигло максимума\\n\");\n        return;\n    }\n    // Добавить n-ю вершину и обнулить n-ю строку и столбец\n    int n = graph->size;\n    graph->vertices[n] = val;\n    for (int i = 0; i <= n; i++) {\n        graph->adjMat[n][i] = graph->adjMat[i][n] = 0;\n    }\n    graph->size++;\n}\n\n/* Удаление вершины */\nvoid removeVertex(GraphAdjMat *graph, int index) {\n    if (index < 0 || index >= graph->size) {\n        fprintf(stderr, \"индекс вершины выходит за границы\\n\");\n        return;\n    }\n    // Удалить вершину с индексом index из списка вершин\n    for (int i = index; i < graph->size - 1; i++) {\n        graph->vertices[i] = graph->vertices[i + 1];\n    }\n    // Удалить строку с индексом index из матрицы смежности\n    for (int i = index; i < graph->size - 1; i++) {\n        for (int j = 0; j < graph->size; j++) {\n            graph->adjMat[i][j] = graph->adjMat[i + 1][j];\n        }\n    }\n    // Удалить столбец с индексом index из матрицы смежности\n    for (int i = 0; i < graph->size; i++) {\n        for (int j = index; j < graph->size - 1; j++) {\n            graph->adjMat[i][j] = graph->adjMat[i][j + 1];\n        }\n    }\n    graph->size--;\n}\n\n/* Добавление ребра */\n// Параметры i и j соответствуют индексам элементов vertices\nvoid addEdge(GraphAdjMat *graph, int i, int j) {\n    if (i < 0 || j < 0 || i >= graph->size || j >= graph->size || i == j) {\n        fprintf(stderr, \"индексы ребра выходят за границы или совпадают\\n\");\n        return;\n    }\n    graph->adjMat[i][j] = 1;\n    graph->adjMat[j][i] = 1;\n}\n\n/* Удаление ребра */\n// Параметры i и j соответствуют индексам элементов vertices\nvoid removeEdge(GraphAdjMat *graph, int i, int j) {\n    if (i < 0 || j < 0 || i >= graph->size || j >= graph->size || i == j) {\n        fprintf(stderr, \"индексы ребра выходят за границы или совпадают\\n\");\n        return;\n    }\n    graph->adjMat[i][j] = 0;\n    graph->adjMat[j][i] = 0;\n}\n\n/* Вывести матрицу смежности */\nvoid printGraphAdjMat(GraphAdjMat *graph) {\n    printf(\"Список вершин = \");\n    printArray(graph->vertices, graph->size);\n    printf(\"Матрица смежности =\\n\");\n    for (int i = 0; i < graph->size; i++) {\n        printArray(graph->adjMat[i], graph->size);\n    }\n}\n
        graph_adjacency_matrix.kt
        /* Класс неориентированного графа на основе матрицы смежности */\nclass GraphAdjMat(vertices: IntArray, edges: Array<IntArray>) {\n    val vertices = mutableListOf<Int>() // Список вершин: элементы представляют «значения вершин», а индексы — «индексы вершин»\n    val adjMat = mutableListOf<MutableList<Int>>() // Матрица смежности, где индексы строк и столбцов соответствуют «индексам вершин»\n\n    /* Конструктор */\n    init {\n        // Добавление вершины\n        for (vertex in vertices) {\n            addVertex(vertex)\n        }\n        // Добавить ребра\n        // Обратите внимание: элементы edges представляют собой индексы вершин, то есть соответствуют индексам элементов vertices\n        for (edge in edges) {\n            addEdge(edge[0], edge[1])\n        }\n    }\n\n    /* Получить число вершин */\n    fun size(): Int {\n        return vertices.size\n    }\n\n    /* Добавление вершины */\n    fun addVertex(_val: Int) {\n        val n = size()\n        // Добавить значение новой вершины в список вершин\n        vertices.add(_val)\n        // Добавить строку в матрицу смежности\n        val newRow = mutableListOf<Int>()\n        for (j in 0..<n) {\n            newRow.add(0)\n        }\n        adjMat.add(newRow)\n        // Добавить столбец в матрицу смежности\n        for (row in adjMat) {\n            row.add(0)\n        }\n    }\n\n    /* Удаление вершины */\n    fun removeVertex(index: Int) {\n        if (index >= size())\n            throw IndexOutOfBoundsException()\n        // Удалить вершину с индексом index из списка вершин\n        vertices.removeAt(index)\n        // Удалить строку с индексом index из матрицы смежности\n        adjMat.removeAt(index)\n        // Удалить столбец с индексом index из матрицы смежности\n        for (row in adjMat) {\n            row.removeAt(index)\n        }\n    }\n\n    /* Добавление ребра */\n    // Параметры i и j соответствуют индексам элементов vertices\n    fun addEdge(i: Int, j: Int) {\n        // Обработка выхода индекса за границы и случая равенства\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j)\n            throw IndexOutOfBoundsException()\n        // В неориентированном графе матрица смежности симметрична относительно главной диагонали, то есть выполняется (i, j) == (j, i)\n        adjMat[i][j] = 1\n        adjMat[j][i] = 1\n    }\n\n    /* Удаление ребра */\n    // Параметры i и j соответствуют индексам элементов vertices\n    fun removeEdge(i: Int, j: Int) {\n        // Обработка выхода индекса за границы и случая равенства\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j)\n            throw IndexOutOfBoundsException()\n        adjMat[i][j] = 0\n        adjMat[j][i] = 0\n    }\n\n    /* Вывести матрицу смежности */\n    fun print() {\n        print(\"Список вершин = \")\n        println(vertices)\n        println(\"Матрица смежности =\")\n        printMatrix(adjMat)\n    }\n}\n
        graph_adjacency_matrix.rb
        ### Класс неориентированного графа на основе матрицы смежности ###\nclass GraphAdjMat\n  def initialize(vertices, edges)\n    ### Конструктор ###\n    # Список вершин: элементы представляют «значения вершин», а индексы — «индексы вершин»\n    @vertices = []\n    # Матрица смежности, где индексы строк и столбцов соответствуют «индексам вершин»\n    @adj_mat = []\n    # Добавление вершины\n    vertices.each { |val| add_vertex(val) }\n    # Добавить ребра\n    # Обратите внимание: элементы edges представляют собой индексы вершин, то есть соответствуют индексам элементов vertices\n    edges.each { |e| add_edge(e[0], e[1]) }\n  end\n\n  ### Получение числа вершин ###\n  def size\n    @vertices.length\n  end\n\n  ### Добавление вершины ###\n  def add_vertex(val)\n    n = size\n    # Добавить значение новой вершины в список вершин\n    @vertices << val\n    # Добавить строку в матрицу смежности\n    new_row = Array.new(n, 0)\n    @adj_mat << new_row\n    # Добавить столбец в матрицу смежности\n    @adj_mat.each { |row| row << 0 }\n  end\n\n  ### Удаление вершины ###\n  def remove_vertex(index)\n    raise IndexError if index >= size\n\n    # Удалить вершину с индексом index из списка вершин\n    @vertices.delete_at(index)\n    # Удалить строку с индексом index из матрицы смежности\n    @adj_mat.delete_at(index)\n    # Удалить столбец с индексом index из матрицы смежности\n    @adj_mat.each { |row| row.delete_at(index) }\n  end\n\n  ### Добавление ребра ###\n  def add_edge(i, j)\n    # Параметры i и j соответствуют индексам элементов vertices\n    # Обработка выхода индекса за границы и случая равенства\n    if i < 0 || j < 0 || i >= size || j >= size || i == j\n      raise IndexError\n    end\n    # В неориентированном графе матрица смежности симметрична относительно главной диагонали, то есть выполняется (i, j) == (j, i)\n    @adj_mat[i][j] = 1\n    @adj_mat[j][i] = 1\n  end\n\n  ### Удаление ребра ###\n  def remove_edge(i, j)\n    # Параметры i и j соответствуют индексам элементов vertices\n    # Обработка выхода индекса за границы и случая равенства\n    if i < 0 || j < 0 || i >= size || j >= size || i == j\n      raise IndexError\n    end\n    @adj_mat[i][j] = 0\n    @adj_mat[j][i] = 0\n  end\n\n  ### Вывести матрицу смежности ###\n  def __print__\n    puts \"Список вершин = #{@vertices}\"\n    puts 'Матрица смежности ='\n    print_matrix(@adj_mat)\n  end\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 9. Графы","9.2   Базовые операции графа"],"tags":[]},{"location":"chapter_graph/graph_operations/#922","level":2,"title":"9.2.2   Реализация на основе списка смежности","text":"

        Пусть неориентированный граф содержит в сумме \\(n\\) вершин и \\(m\\) ребер. Тогда различные операции можно реализовать способом, показанным на рисунке 9-8.

        • Добавление ребра: достаточно добавить ребро в конец списка, соответствующего вершине. Это требует \\(O(1)\\) времени. Поскольку граф неориентированный, необходимо одновременно добавить ребра в обоих направлениях.
        • Удаление ребра: нужно найти и удалить указанное ребро в списке, соответствующем вершине. Это требует \\(O(m)\\) времени. В неориентированном графе необходимо удалить ребра в обоих направлениях.
        • Добавление вершины: в список смежности добавляется еще один список, а новая вершина становится его головным узлом. Это требует \\(O(1)\\) времени.
        • Удаление вершины: требуется пройти по всему списку смежности и удалить все ребра, содержащие указанную вершину. Это требует \\(O(n + m)\\) времени.
        • Инициализация: в списке смежности создаются \\(n\\) вершин и \\(2m\\) ребер. Это требует \\(O(n + m)\\) времени.
        <1><2><3><4><5>

        Рисунок 9-8   Инициализация списка смежности, добавление и удаление ребер и вершин

        Ниже приведен код списка смежности. По сравнению с тем, что показано на рисунке 9-8, реальная реализация имеет следующие отличия.

        • Чтобы упростить добавление и удаление вершин, а также сделать код проще, мы используем список, то есть динамический массив, вместо связного списка.
        • Для хранения списка смежности используется хеш-таблица, где key - это экземпляр вершины, а value - список смежных вершин данной вершины.

        Кроме того, в списке смежности используется класс Vertex для представления вершины. Причина в том, что если, как и в матрице смежности, различать вершины по индексам списка, то при удалении вершины с индексом \\(i\\) пришлось бы обходить весь список смежности и уменьшать на \\(1\\) все индексы, большие \\(i\\) , что крайне неэффективно. Если же каждая вершина является уникальным экземпляром Vertex , то после удаления одной вершины остальные вершины менять уже не требуется.

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_adjacency_list.py
        class GraphAdjList:\n    \"\"\"Класс неориентированного графа на основе списка смежности\"\"\"\n\n    def __init__(self, edges: list[list[Vertex]]):\n        \"\"\"Конструктор\"\"\"\n        # Список смежности, где key — вершина, а value — все смежные ей вершины\n        self.adj_list = dict[Vertex, list[Vertex]]()\n        # Добавить все вершины и ребра\n        for edge in edges:\n            self.add_vertex(edge[0])\n            self.add_vertex(edge[1])\n            self.add_edge(edge[0], edge[1])\n\n    def size(self) -> int:\n        \"\"\"Получить число вершин\"\"\"\n        return len(self.adj_list)\n\n    def add_edge(self, vet1: Vertex, vet2: Vertex):\n        \"\"\"Добавление ребра\"\"\"\n        if vet1 not in self.adj_list or vet2 not in self.adj_list or vet1 == vet2:\n            raise ValueError()\n        # Добавить ребро vet1 - vet2\n        self.adj_list[vet1].append(vet2)\n        self.adj_list[vet2].append(vet1)\n\n    def remove_edge(self, vet1: Vertex, vet2: Vertex):\n        \"\"\"Удаление ребра\"\"\"\n        if vet1 not in self.adj_list or vet2 not in self.adj_list or vet1 == vet2:\n            raise ValueError()\n        # Удалить ребро vet1 - vet2\n        self.adj_list[vet1].remove(vet2)\n        self.adj_list[vet2].remove(vet1)\n\n    def add_vertex(self, vet: Vertex):\n        \"\"\"Добавление вершины\"\"\"\n        if vet in self.adj_list:\n            return\n        # Добавить новый список в список смежности\n        self.adj_list[vet] = []\n\n    def remove_vertex(self, vet: Vertex):\n        \"\"\"Удаление вершины\"\"\"\n        if vet not in self.adj_list:\n            raise ValueError()\n        # Удалить из списка смежности список, соответствующий вершине vet\n        self.adj_list.pop(vet)\n        # Обойти списки других вершин и удалить все ребра, содержащие vet\n        for vertex in self.adj_list:\n            if vet in self.adj_list[vertex]:\n                self.adj_list[vertex].remove(vet)\n\n    def print(self):\n        \"\"\"Вывести список смежности\"\"\"\n        print(\"Список смежности =\")\n        for vertex in self.adj_list:\n            tmp = [v.val for v in self.adj_list[vertex]]\n            print(f\"{vertex.val}: {tmp},\")\n
        graph_adjacency_list.cpp
        /* Класс неориентированного графа на основе списка смежности */\nclass GraphAdjList {\n  public:\n    // Список смежности, где key — вершина, а value — все смежные ей вершины\n    unordered_map<Vertex *, vector<Vertex *>> adjList;\n\n    /* Удалить указанный узел из vector */\n    void remove(vector<Vertex *> &vec, Vertex *vet) {\n        for (int i = 0; i < vec.size(); i++) {\n            if (vec[i] == vet) {\n                vec.erase(vec.begin() + i);\n                break;\n            }\n        }\n    }\n\n    /* Конструктор */\n    GraphAdjList(const vector<vector<Vertex *>> &edges) {\n        // Добавить все вершины и ребра\n        for (const vector<Vertex *> &edge : edges) {\n            addVertex(edge[0]);\n            addVertex(edge[1]);\n            addEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* Получить число вершин */\n    int size() {\n        return adjList.size();\n    }\n\n    /* Добавление ребра */\n    void addEdge(Vertex *vet1, Vertex *vet2) {\n        if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2)\n            throw invalid_argument(\"вершина не существует\");\n        // Добавить ребро vet1 - vet2\n        adjList[vet1].push_back(vet2);\n        adjList[vet2].push_back(vet1);\n    }\n\n    /* Удаление ребра */\n    void removeEdge(Vertex *vet1, Vertex *vet2) {\n        if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2)\n            throw invalid_argument(\"вершина не существует\");\n        // Удалить ребро vet1 - vet2\n        remove(adjList[vet1], vet2);\n        remove(adjList[vet2], vet1);\n    }\n\n    /* Добавление вершины */\n    void addVertex(Vertex *vet) {\n        if (adjList.count(vet))\n            return;\n        // Добавить новый список в список смежности\n        adjList[vet] = vector<Vertex *>();\n    }\n\n    /* Удаление вершины */\n    void removeVertex(Vertex *vet) {\n        if (!adjList.count(vet))\n            throw invalid_argument(\"вершина не существует\");\n        // Удалить из списка смежности список, соответствующий вершине vet\n        adjList.erase(vet);\n        // Обойти списки других вершин и удалить все ребра, содержащие vet\n        for (auto &adj : adjList) {\n            remove(adj.second, vet);\n        }\n    }\n\n    /* Вывести список смежности */\n    void print() {\n        cout << \"Список смежности =\" << endl;\n        for (auto &adj : adjList) {\n            const auto &key = adj.first;\n            const auto &vec = adj.second;\n            cout << key->val << \": \";\n            printVector(vetsToVals(vec));\n        }\n    }\n};\n
        graph_adjacency_list.java
        /* Класс неориентированного графа на основе списка смежности */\nclass GraphAdjList {\n    // Список смежности, где key — вершина, а value — все смежные ей вершины\n    Map<Vertex, List<Vertex>> adjList;\n\n    /* Конструктор */\n    public GraphAdjList(Vertex[][] edges) {\n        this.adjList = new HashMap<>();\n        // Добавить все вершины и ребра\n        for (Vertex[] edge : edges) {\n            addVertex(edge[0]);\n            addVertex(edge[1]);\n            addEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* Получить число вершин */\n    public int size() {\n        return adjList.size();\n    }\n\n    /* Добавление ребра */\n    public void addEdge(Vertex vet1, Vertex vet2) {\n        if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n            throw new IllegalArgumentException();\n        // Добавить ребро vet1 - vet2\n        adjList.get(vet1).add(vet2);\n        adjList.get(vet2).add(vet1);\n    }\n\n    /* Удаление ребра */\n    public void removeEdge(Vertex vet1, Vertex vet2) {\n        if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n            throw new IllegalArgumentException();\n        // Удалить ребро vet1 - vet2\n        adjList.get(vet1).remove(vet2);\n        adjList.get(vet2).remove(vet1);\n    }\n\n    /* Добавление вершины */\n    public void addVertex(Vertex vet) {\n        if (adjList.containsKey(vet))\n            return;\n        // Добавить новый список в список смежности\n        adjList.put(vet, new ArrayList<>());\n    }\n\n    /* Удаление вершины */\n    public void removeVertex(Vertex vet) {\n        if (!adjList.containsKey(vet))\n            throw new IllegalArgumentException();\n        // Удалить из списка смежности список, соответствующий вершине vet\n        adjList.remove(vet);\n        // Обойти списки других вершин и удалить все ребра, содержащие vet\n        for (List<Vertex> list : adjList.values()) {\n            list.remove(vet);\n        }\n    }\n\n    /* Вывести список смежности */\n    public void print() {\n        System.out.println(\"Список смежности =\");\n        for (Map.Entry<Vertex, List<Vertex>> pair : adjList.entrySet()) {\n            List<Integer> tmp = new ArrayList<>();\n            for (Vertex vertex : pair.getValue())\n                tmp.add(vertex.val);\n            System.out.println(pair.getKey().val + \": \" + tmp + \",\");\n        }\n    }\n}\n
        graph_adjacency_list.cs
        /* Класс неориентированного графа на основе списка смежности */\nclass GraphAdjList {\n    // Список смежности, где key — вершина, а value — все смежные ей вершины\n    public Dictionary<Vertex, List<Vertex>> adjList;\n\n    /* Конструктор */\n    public GraphAdjList(Vertex[][] edges) {\n        adjList = [];\n        // Добавить все вершины и ребра\n        foreach (Vertex[] edge in edges) {\n            AddVertex(edge[0]);\n            AddVertex(edge[1]);\n            AddEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* Получить число вершин */\n    int Size() {\n        return adjList.Count;\n    }\n\n    /* Добавление ребра */\n    public void AddEdge(Vertex vet1, Vertex vet2) {\n        if (!adjList.ContainsKey(vet1) || !adjList.ContainsKey(vet2) || vet1 == vet2)\n            throw new InvalidOperationException();\n        // Добавить ребро vet1 - vet2\n        adjList[vet1].Add(vet2);\n        adjList[vet2].Add(vet1);\n    }\n\n    /* Удаление ребра */\n    public void RemoveEdge(Vertex vet1, Vertex vet2) {\n        if (!adjList.ContainsKey(vet1) || !adjList.ContainsKey(vet2) || vet1 == vet2)\n            throw new InvalidOperationException();\n        // Удалить ребро vet1 - vet2\n        adjList[vet1].Remove(vet2);\n        adjList[vet2].Remove(vet1);\n    }\n\n    /* Добавление вершины */\n    public void AddVertex(Vertex vet) {\n        if (adjList.ContainsKey(vet))\n            return;\n        // Добавить новый список в список смежности\n        adjList.Add(vet, []);\n    }\n\n    /* Удаление вершины */\n    public void RemoveVertex(Vertex vet) {\n        if (!adjList.ContainsKey(vet))\n            throw new InvalidOperationException();\n        // Удалить из списка смежности список, соответствующий вершине vet\n        adjList.Remove(vet);\n        // Обойти списки других вершин и удалить все ребра, содержащие vet\n        foreach (List<Vertex> list in adjList.Values) {\n            list.Remove(vet);\n        }\n    }\n\n    /* Вывести список смежности */\n    public void Print() {\n        Console.WriteLine(\"Список смежности =\");\n        foreach (KeyValuePair<Vertex, List<Vertex>> pair in adjList) {\n            List<int> tmp = [];\n            foreach (Vertex vertex in pair.Value)\n                tmp.Add(vertex.val);\n            Console.WriteLine(pair.Key.val + \": [\" + string.Join(\", \", tmp) + \"],\");\n        }\n    }\n}\n
        graph_adjacency_list.go
        /* Класс неориентированного графа на основе списка смежности */\ntype graphAdjList struct {\n    // Список смежности, где key — вершина, а value — все смежные ей вершины\n    adjList map[Vertex][]Vertex\n}\n\n/* Конструктор */\nfunc newGraphAdjList(edges [][]Vertex) *graphAdjList {\n    g := &graphAdjList{\n        adjList: make(map[Vertex][]Vertex),\n    }\n    // Добавить все вершины и ребра\n    for _, edge := range edges {\n        g.addVertex(edge[0])\n        g.addVertex(edge[1])\n        g.addEdge(edge[0], edge[1])\n    }\n    return g\n}\n\n/* Получить число вершин */\nfunc (g *graphAdjList) size() int {\n    return len(g.adjList)\n}\n\n/* Добавление ребра */\nfunc (g *graphAdjList) addEdge(vet1 Vertex, vet2 Vertex) {\n    _, ok1 := g.adjList[vet1]\n    _, ok2 := g.adjList[vet2]\n    if !ok1 || !ok2 || vet1 == vet2 {\n        panic(\"error\")\n    }\n    // Добавить ребро vet1 - vet2, добавив анонимную struct{}\n    g.adjList[vet1] = append(g.adjList[vet1], vet2)\n    g.adjList[vet2] = append(g.adjList[vet2], vet1)\n}\n\n/* Удаление ребра */\nfunc (g *graphAdjList) removeEdge(vet1 Vertex, vet2 Vertex) {\n    _, ok1 := g.adjList[vet1]\n    _, ok2 := g.adjList[vet2]\n    if !ok1 || !ok2 || vet1 == vet2 {\n        panic(\"error\")\n    }\n    // Удалить ребро vet1 - vet2\n    g.adjList[vet1] = DeleteSliceElms(g.adjList[vet1], vet2)\n    g.adjList[vet2] = DeleteSliceElms(g.adjList[vet2], vet1)\n}\n\n/* Добавление вершины */\nfunc (g *graphAdjList) addVertex(vet Vertex) {\n    _, ok := g.adjList[vet]\n    if ok {\n        return\n    }\n    // Добавить новый список в список смежности\n    g.adjList[vet] = make([]Vertex, 0)\n}\n\n/* Удаление вершины */\nfunc (g *graphAdjList) removeVertex(vet Vertex) {\n    _, ok := g.adjList[vet]\n    if !ok {\n        panic(\"error\")\n    }\n    // Удалить из списка смежности список, соответствующий вершине vet\n    delete(g.adjList, vet)\n    // Обойти списки других вершин и удалить все ребра, содержащие vet\n    for v, list := range g.adjList {\n        g.adjList[v] = DeleteSliceElms(list, vet)\n    }\n}\n\n/* Вывести список смежности */\nfunc (g *graphAdjList) print() {\n    var builder strings.Builder\n    fmt.Printf(\"Список смежности = \\n\")\n    for k, v := range g.adjList {\n        builder.WriteString(\"\\t\\t\" + strconv.Itoa(k.Val) + \": \")\n        for _, vet := range v {\n            builder.WriteString(strconv.Itoa(vet.Val) + \" \")\n        }\n        fmt.Println(builder.String())\n        builder.Reset()\n    }\n}\n
        graph_adjacency_list.swift
        /* Класс неориентированного графа на основе списка смежности */\nclass GraphAdjList {\n    // Список смежности, где key — вершина, а value — все смежные ей вершины\n    public private(set) var adjList: [Vertex: [Vertex]]\n\n    /* Конструктор */\n    public init(edges: [[Vertex]]) {\n        adjList = [:]\n        // Добавить все вершины и ребра\n        for edge in edges {\n            addVertex(vet: edge[0])\n            addVertex(vet: edge[1])\n            addEdge(vet1: edge[0], vet2: edge[1])\n        }\n    }\n\n    /* Получить число вершин */\n    public func size() -> Int {\n        adjList.count\n    }\n\n    /* Добавление ребра */\n    public func addEdge(vet1: Vertex, vet2: Vertex) {\n        if adjList[vet1] == nil || adjList[vet2] == nil || vet1 == vet2 {\n            fatalError(\"Неверный аргумент\")\n        }\n        // Добавить ребро vet1 - vet2\n        adjList[vet1]?.append(vet2)\n        adjList[vet2]?.append(vet1)\n    }\n\n    /* Удаление ребра */\n    public func removeEdge(vet1: Vertex, vet2: Vertex) {\n        if adjList[vet1] == nil || adjList[vet2] == nil || vet1 == vet2 {\n            fatalError(\"Неверный аргумент\")\n        }\n        // Удалить ребро vet1 - vet2\n        adjList[vet1]?.removeAll { $0 == vet2 }\n        adjList[vet2]?.removeAll { $0 == vet1 }\n    }\n\n    /* Добавление вершины */\n    public func addVertex(vet: Vertex) {\n        if adjList[vet] != nil {\n            return\n        }\n        // Добавить новый список в список смежности\n        adjList[vet] = []\n    }\n\n    /* Удаление вершины */\n    public func removeVertex(vet: Vertex) {\n        if adjList[vet] == nil {\n            fatalError(\"Неверный аргумент\")\n        }\n        // Удалить из списка смежности список, соответствующий вершине vet\n        adjList.removeValue(forKey: vet)\n        // Обойти списки других вершин и удалить все ребра, содержащие vet\n        for key in adjList.keys {\n            adjList[key]?.removeAll { $0 == vet }\n        }\n    }\n\n    /* Вывести список смежности */\n    public func print() {\n        Swift.print(\"Список смежности =\")\n        for (vertex, list) in adjList {\n            let list = list.map { $0.val }\n            Swift.print(\"\\(vertex.val): \\(list),\")\n        }\n    }\n}\n
        graph_adjacency_list.js
        /* Класс неориентированного графа на основе списка смежности */\nclass GraphAdjList {\n    // Список смежности, где key — вершина, а value — все смежные ей вершины\n    adjList;\n\n    /* Конструктор */\n    constructor(edges) {\n        this.adjList = new Map();\n        // Добавить все вершины и ребра\n        for (const edge of edges) {\n            this.addVertex(edge[0]);\n            this.addVertex(edge[1]);\n            this.addEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* Получить число вершин */\n    size() {\n        return this.adjList.size;\n    }\n\n    /* Добавление ребра */\n    addEdge(vet1, vet2) {\n        if (\n            !this.adjList.has(vet1) ||\n            !this.adjList.has(vet2) ||\n            vet1 === vet2\n        ) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // Добавить ребро vet1 - vet2\n        this.adjList.get(vet1).push(vet2);\n        this.adjList.get(vet2).push(vet1);\n    }\n\n    /* Удаление ребра */\n    removeEdge(vet1, vet2) {\n        if (\n            !this.adjList.has(vet1) ||\n            !this.adjList.has(vet2) ||\n            vet1 === vet2 ||\n            this.adjList.get(vet1).indexOf(vet2) === -1\n        ) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // Удалить ребро vet1 - vet2\n        this.adjList.get(vet1).splice(this.adjList.get(vet1).indexOf(vet2), 1);\n        this.adjList.get(vet2).splice(this.adjList.get(vet2).indexOf(vet1), 1);\n    }\n\n    /* Добавление вершины */\n    addVertex(vet) {\n        if (this.adjList.has(vet)) return;\n        // Добавить новый список в список смежности\n        this.adjList.set(vet, []);\n    }\n\n    /* Удаление вершины */\n    removeVertex(vet) {\n        if (!this.adjList.has(vet)) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // Удалить из списка смежности список, соответствующий вершине vet\n        this.adjList.delete(vet);\n        // Обойти списки других вершин и удалить все ребра, содержащие vet\n        for (const set of this.adjList.values()) {\n            const index = set.indexOf(vet);\n            if (index > -1) {\n                set.splice(index, 1);\n            }\n        }\n    }\n\n    /* Вывести список смежности */\n    print() {\n        console.log('Список смежности =');\n        for (const [key, value] of this.adjList) {\n            const tmp = [];\n            for (const vertex of value) {\n                tmp.push(vertex.val);\n            }\n            console.log(key.val + ': ' + tmp.join());\n        }\n    }\n}\n
        graph_adjacency_list.ts
        /* Класс неориентированного графа на основе списка смежности */\nclass GraphAdjList {\n    // Список смежности, где key — вершина, а value — все смежные ей вершины\n    adjList: Map<Vertex, Vertex[]>;\n\n    /* Конструктор */\n    constructor(edges: Vertex[][]) {\n        this.adjList = new Map();\n        // Добавить все вершины и ребра\n        for (const edge of edges) {\n            this.addVertex(edge[0]);\n            this.addVertex(edge[1]);\n            this.addEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* Получить число вершин */\n    size(): number {\n        return this.adjList.size;\n    }\n\n    /* Добавление ребра */\n    addEdge(vet1: Vertex, vet2: Vertex): void {\n        if (\n            !this.adjList.has(vet1) ||\n            !this.adjList.has(vet2) ||\n            vet1 === vet2\n        ) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // Добавить ребро vet1 - vet2\n        this.adjList.get(vet1).push(vet2);\n        this.adjList.get(vet2).push(vet1);\n    }\n\n    /* Удаление ребра */\n    removeEdge(vet1: Vertex, vet2: Vertex): void {\n        if (\n            !this.adjList.has(vet1) ||\n            !this.adjList.has(vet2) ||\n            vet1 === vet2 ||\n            this.adjList.get(vet1).indexOf(vet2) === -1\n        ) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // Удалить ребро vet1 - vet2\n        this.adjList.get(vet1).splice(this.adjList.get(vet1).indexOf(vet2), 1);\n        this.adjList.get(vet2).splice(this.adjList.get(vet2).indexOf(vet1), 1);\n    }\n\n    /* Добавление вершины */\n    addVertex(vet: Vertex): void {\n        if (this.adjList.has(vet)) return;\n        // Добавить новый список в список смежности\n        this.adjList.set(vet, []);\n    }\n\n    /* Удаление вершины */\n    removeVertex(vet: Vertex): void {\n        if (!this.adjList.has(vet)) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // Удалить из списка смежности список, соответствующий вершине vet\n        this.adjList.delete(vet);\n        // Обойти списки других вершин и удалить все ребра, содержащие vet\n        for (const set of this.adjList.values()) {\n            const index: number = set.indexOf(vet);\n            if (index > -1) {\n                set.splice(index, 1);\n            }\n        }\n    }\n\n    /* Вывести список смежности */\n    print(): void {\n        console.log('Список смежности =');\n        for (const [key, value] of this.adjList.entries()) {\n            const tmp = [];\n            for (const vertex of value) {\n                tmp.push(vertex.val);\n            }\n            console.log(key.val + ': ' + tmp.join());\n        }\n    }\n}\n
        graph_adjacency_list.dart
        /* Класс неориентированного графа на основе списка смежности */\nclass GraphAdjList {\n  // Список смежности, где key — вершина, а value — все смежные ей вершины\n  Map<Vertex, List<Vertex>> adjList = {};\n\n  /* Конструктор */\n  GraphAdjList(List<List<Vertex>> edges) {\n    for (List<Vertex> edge in edges) {\n      addVertex(edge[0]);\n      addVertex(edge[1]);\n      addEdge(edge[0], edge[1]);\n    }\n  }\n\n  /* Получить число вершин */\n  int size() {\n    return adjList.length;\n  }\n\n  /* Добавление ребра */\n  void addEdge(Vertex vet1, Vertex vet2) {\n    if (!adjList.containsKey(vet1) ||\n        !adjList.containsKey(vet2) ||\n        vet1 == vet2) {\n      throw ArgumentError;\n    }\n    // Добавить ребро vet1 - vet2\n    adjList[vet1]!.add(vet2);\n    adjList[vet2]!.add(vet1);\n  }\n\n  /* Удаление ребра */\n  void removeEdge(Vertex vet1, Vertex vet2) {\n    if (!adjList.containsKey(vet1) ||\n        !adjList.containsKey(vet2) ||\n        vet1 == vet2) {\n      throw ArgumentError;\n    }\n    // Удалить ребро vet1 - vet2\n    adjList[vet1]!.remove(vet2);\n    adjList[vet2]!.remove(vet1);\n  }\n\n  /* Добавление вершины */\n  void addVertex(Vertex vet) {\n    if (adjList.containsKey(vet)) return;\n    // Добавить новый список в список смежности\n    adjList[vet] = [];\n  }\n\n  /* Удаление вершины */\n  void removeVertex(Vertex vet) {\n    if (!adjList.containsKey(vet)) {\n      throw ArgumentError;\n    }\n    // Удалить из списка смежности список, соответствующий вершине vet\n    adjList.remove(vet);\n    // Обойти списки других вершин и удалить все ребра, содержащие vet\n    adjList.forEach((key, value) {\n      value.remove(vet);\n    });\n  }\n\n  /* Вывести список смежности */\n  void printAdjList() {\n    print(\"Список смежности =\");\n    adjList.forEach((key, value) {\n      List<int> tmp = [];\n      for (Vertex vertex in value) {\n        tmp.add(vertex.val);\n      }\n      print(\"${key.val}: $tmp,\");\n    });\n  }\n}\n
        graph_adjacency_list.rs
        /* Тип неориентированного графа на основе списка смежности */\npub struct GraphAdjList {\n    // Список смежности, где key — вершина, а value — все смежные ей вершины\n    pub adj_list: HashMap<Vertex, Vec<Vertex>>, // maybe HashSet<Vertex> for value part is better?\n}\n\nimpl GraphAdjList {\n    /* Конструктор */\n    pub fn new(edges: Vec<[Vertex; 2]>) -> Self {\n        let mut graph = GraphAdjList {\n            adj_list: HashMap::new(),\n        };\n        // Добавить все вершины и ребра\n        for edge in edges {\n            graph.add_vertex(edge[0]);\n            graph.add_vertex(edge[1]);\n            graph.add_edge(edge[0], edge[1]);\n        }\n\n        graph\n    }\n\n    /* Получить число вершин */\n    #[allow(unused)]\n    pub fn size(&self) -> usize {\n        self.adj_list.len()\n    }\n\n    /* Добавление ребра */\n    pub fn add_edge(&mut self, vet1: Vertex, vet2: Vertex) {\n        if vet1 == vet2 {\n            panic!(\"value error\");\n        }\n        // Добавить ребро vet1 - vet2\n        self.adj_list.entry(vet1).or_default().push(vet2);\n        self.adj_list.entry(vet2).or_default().push(vet1);\n    }\n\n    /* Удаление ребра */\n    #[allow(unused)]\n    pub fn remove_edge(&mut self, vet1: Vertex, vet2: Vertex) {\n        if vet1 == vet2 {\n            panic!(\"value error\");\n        }\n        // Удалить ребро vet1 - vet2\n        self.adj_list\n            .entry(vet1)\n            .and_modify(|v| v.retain(|&e| e != vet2));\n        self.adj_list\n            .entry(vet2)\n            .and_modify(|v| v.retain(|&e| e != vet1));\n    }\n\n    /* Добавление вершины */\n    pub fn add_vertex(&mut self, vet: Vertex) {\n        if self.adj_list.contains_key(&vet) {\n            return;\n        }\n        // Добавить новый список в список смежности\n        self.adj_list.insert(vet, vec![]);\n    }\n\n    /* Удаление вершины */\n    #[allow(unused)]\n    pub fn remove_vertex(&mut self, vet: Vertex) {\n        // Удалить из списка смежности список, соответствующий вершине vet\n        self.adj_list.remove(&vet);\n        // Обойти списки других вершин и удалить все ребра, содержащие vet\n        for list in self.adj_list.values_mut() {\n            list.retain(|&v| v != vet);\n        }\n    }\n\n    /* Вывести список смежности */\n    pub fn print(&self) {\n        println!(\"Список смежности =\");\n        for (vertex, list) in &self.adj_list {\n            let list = list.iter().map(|vertex| vertex.val).collect::<Vec<i32>>();\n            println!(\"{}: {:?},\", vertex.val, list);\n        }\n    }\n}\n
        graph_adjacency_list.c
        /* Структура узла */\ntypedef struct AdjListNode {\n    Vertex *vertex;           // Вершина\n    struct AdjListNode *next; // Узел-преемник\n} AdjListNode;\n\n/* Найти узел, соответствующий вершине */\nAdjListNode *findNode(GraphAdjList *graph, Vertex *vet) {\n    for (int i = 0; i < graph->size; i++) {\n        if (graph->heads[i]->vertex == vet) {\n            return graph->heads[i];\n        }\n    }\n    return NULL;\n}\n\n/* Вспомогательная функция добавления ребра */\nvoid addEdgeHelper(AdjListNode *head, Vertex *vet) {\n    AdjListNode *node = (AdjListNode *)malloc(sizeof(AdjListNode));\n    node->vertex = vet;\n    // Вставка в голову\n    node->next = head->next;\n    head->next = node;\n}\n\n/* Вспомогательная функция удаления ребра */\nvoid removeEdgeHelper(AdjListNode *head, Vertex *vet) {\n    AdjListNode *pre = head;\n    AdjListNode *cur = head->next;\n    // Искать в связном списке узел, соответствующий vet\n    while (cur != NULL && cur->vertex != vet) {\n        pre = cur;\n        cur = cur->next;\n    }\n    if (cur == NULL)\n        return;\n    // Удалить из связного списка узел, соответствующий vet\n    pre->next = cur->next;\n    // Освободить память\n    free(cur);\n}\n\n/* Класс неориентированного графа на основе списка смежности */\ntypedef struct {\n    AdjListNode *heads[MAX_SIZE]; // Массив узлов\n    int size;                     // Количество узлов\n} GraphAdjList;\n\n/* Конструктор */\nGraphAdjList *newGraphAdjList() {\n    GraphAdjList *graph = (GraphAdjList *)malloc(sizeof(GraphAdjList));\n    if (!graph) {\n        return NULL;\n    }\n    graph->size = 0;\n    for (int i = 0; i < MAX_SIZE; i++) {\n        graph->heads[i] = NULL;\n    }\n    return graph;\n}\n\n/* Деструктор */\nvoid delGraphAdjList(GraphAdjList *graph) {\n    for (int i = 0; i < graph->size; i++) {\n        AdjListNode *cur = graph->heads[i];\n        while (cur != NULL) {\n            AdjListNode *next = cur->next;\n            if (cur != graph->heads[i]) {\n                free(cur);\n            }\n            cur = next;\n        }\n        free(graph->heads[i]->vertex);\n        free(graph->heads[i]);\n    }\n    free(graph);\n}\n\n/* Найти узел, соответствующий вершине */\nAdjListNode *findNode(GraphAdjList *graph, Vertex *vet) {\n    for (int i = 0; i < graph->size; i++) {\n        if (graph->heads[i]->vertex == vet) {\n            return graph->heads[i];\n        }\n    }\n    return NULL;\n}\n\n/* Добавление ребра */\nvoid addEdge(GraphAdjList *graph, Vertex *vet1, Vertex *vet2) {\n    AdjListNode *head1 = findNode(graph, vet1);\n    AdjListNode *head2 = findNode(graph, vet2);\n    assert(head1 != NULL && head2 != NULL && head1 != head2);\n    // Добавить ребро vet1 - vet2\n    addEdgeHelper(head1, vet2);\n    addEdgeHelper(head2, vet1);\n}\n\n/* Удаление ребра */\nvoid removeEdge(GraphAdjList *graph, Vertex *vet1, Vertex *vet2) {\n    AdjListNode *head1 = findNode(graph, vet1);\n    AdjListNode *head2 = findNode(graph, vet2);\n    assert(head1 != NULL && head2 != NULL);\n    // Удалить ребро vet1 - vet2\n    removeEdgeHelper(head1, head2->vertex);\n    removeEdgeHelper(head2, head1->vertex);\n}\n\n/* Добавление вершины */\nvoid addVertex(GraphAdjList *graph, Vertex *vet) {\n    assert(graph != NULL && graph->size < MAX_SIZE);\n    AdjListNode *head = (AdjListNode *)malloc(sizeof(AdjListNode));\n    head->vertex = vet;\n    head->next = NULL;\n    // Добавить новый список в список смежности\n    graph->heads[graph->size++] = head;\n}\n\n/* Удаление вершины */\nvoid removeVertex(GraphAdjList *graph, Vertex *vet) {\n    AdjListNode *node = findNode(graph, vet);\n    assert(node != NULL);\n    // Удалить из списка смежности список, соответствующий вершине vet\n    AdjListNode *cur = node, *pre = NULL;\n    while (cur) {\n        pre = cur;\n        cur = cur->next;\n        free(pre);\n    }\n    // Обойти списки других вершин и удалить все ребра, содержащие vet\n    for (int i = 0; i < graph->size; i++) {\n        cur = graph->heads[i];\n        pre = NULL;\n        while (cur) {\n            pre = cur;\n            cur = cur->next;\n            if (cur && cur->vertex == vet) {\n                pre->next = cur->next;\n                free(cur);\n                break;\n            }\n        }\n    }\n    // Сдвинуть вершины после данной вперед, чтобы заполнить образовавшийся пробел\n    int i;\n    for (i = 0; i < graph->size; i++) {\n        if (graph->heads[i] == node)\n            break;\n    }\n    for (int j = i; j < graph->size - 1; j++) {\n        graph->heads[j] = graph->heads[j + 1];\n    }\n    graph->size--;\n    free(vet);\n}\n
        graph_adjacency_list.kt
        /* Класс неориентированного графа на основе списка смежности */\nclass GraphAdjList(edges: Array<Array<Vertex?>>) {\n    // Список смежности, где key — вершина, а value — все смежные ей вершины\n    val adjList = HashMap<Vertex, MutableList<Vertex>>()\n\n    /* Конструктор */\n    init {\n        // Добавить все вершины и ребра\n        for (edge in edges) {\n            addVertex(edge[0]!!)\n            addVertex(edge[1]!!)\n            addEdge(edge[0]!!, edge[1]!!)\n        }\n    }\n\n    /* Получить число вершин */\n    fun size(): Int {\n        return adjList.size\n    }\n\n    /* Добавление ребра */\n    fun addEdge(vet1: Vertex, vet2: Vertex) {\n        if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n            throw IllegalArgumentException()\n        // Добавить ребро vet1 - vet2\n        adjList[vet1]?.add(vet2)\n        adjList[vet2]?.add(vet1)\n    }\n\n    /* Удаление ребра */\n    fun removeEdge(vet1: Vertex, vet2: Vertex) {\n        if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n            throw IllegalArgumentException()\n        // Удалить ребро vet1 - vet2\n        adjList[vet1]?.remove(vet2)\n        adjList[vet2]?.remove(vet1)\n    }\n\n    /* Добавление вершины */\n    fun addVertex(vet: Vertex) {\n        if (adjList.containsKey(vet))\n            return\n        // Добавить новый список в список смежности\n        adjList[vet] = mutableListOf()\n    }\n\n    /* Удаление вершины */\n    fun removeVertex(vet: Vertex) {\n        if (!adjList.containsKey(vet))\n            throw IllegalArgumentException()\n        // Удалить из списка смежности список, соответствующий вершине vet\n        adjList.remove(vet)\n        // Обойти списки других вершин и удалить все ребра, содержащие vet\n        for (list in adjList.values) {\n            list.remove(vet)\n        }\n    }\n\n    /* Вывести список смежности */\n    fun print() {\n        println(\"Список смежности =\")\n        for (pair in adjList.entries) {\n            val tmp = mutableListOf<Int>()\n            for (vertex in pair.value) {\n                tmp.add(vertex._val)\n            }\n            println(\"${pair.key._val}: $tmp,\")\n        }\n    }\n}\n
        graph_adjacency_list.rb
        ### Класс неориентированного графа на основе списка смежности ###\nclass GraphAdjList\n  attr_reader :adj_list\n\n  ### Конструктор ###\n  def initialize(edges)\n    # Список смежности, где key — вершина, а value — все смежные ей вершины\n    @adj_list = {}\n    # Добавить все вершины и ребра\n    for edge in edges\n      add_vertex(edge[0])\n      add_vertex(edge[1])\n      add_edge(edge[0], edge[1])\n    end\n  end\n\n  ### Получение числа вершин ###\n  def size\n    @adj_list.length\n  end\n\n  ### Добавление ребра ###\n  def add_edge(vet1, vet2)\n    raise ArgumentError if !@adj_list.include?(vet1) || !@adj_list.include?(vet2)\n\n    @adj_list[vet1] << vet2\n    @adj_list[vet2] << vet1\n  end\n\n  ### Удаление ребра ###\n  def remove_edge(vet1, vet2)\n    raise ArgumentError if !@adj_list.include?(vet1) || !@adj_list.include?(vet2)\n\n    # Удалить ребро vet1 - vet2\n    @adj_list[vet1].delete(vet2)\n    @adj_list[vet2].delete(vet1)\n  end\n\n  ### Добавление вершины ###\n  def add_vertex(vet)\n    return if @adj_list.include?(vet)\n\n    # Добавить новый список в список смежности\n    @adj_list[vet] = []\n  end\n\n  ### Удаление вершины ###\n  def remove_vertex(vet)\n    raise ArgumentError unless @adj_list.include?(vet)\n\n    # Удалить из списка смежности список, соответствующий вершине vet\n    @adj_list.delete(vet)\n    # Обойти списки других вершин и удалить все ребра, содержащие vet\n    for vertex in @adj_list\n      @adj_list[vertex.first].delete(vet) if @adj_list[vertex.first].include?(vet)\n    end\n  end\n\n  ### Вывести список смежности ###\n  def __print__\n    puts 'Список смежности ='\n    for vertex in @adj_list\n      tmp = @adj_list[vertex.first].map { |v| v.val }\n      puts \"#{vertex.first.val}: #{tmp},\"\n    end\n  end\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 9. Графы","9.2   Базовые операции графа"],"tags":[]},{"location":"chapter_graph/graph_operations/#923","level":2,"title":"9.2.3   Сравнение эффективности","text":"

        Пусть в графе имеется \\(n\\) вершин и \\(m\\) ребер. В таблице 9-2 сравниваются временная и пространственная эффективность матрицы смежности и списка смежности. Обратите внимание: список смежности (связный список) соответствует реализации из этой статьи, а список смежности (хеш-таблица) означает вариант, в котором все списки заменены хеш-таблицами.

        Таблица 9-2   Сравнение матрицы смежности и списка смежности

        Матрица смежности Список смежности (связный список) Список смежности (хеш-таблица) Проверка смежности \\(O(1)\\) \\(O(n)\\) \\(O(1)\\) Добавление ребра \\(O(1)\\) \\(O(1)\\) \\(O(1)\\) Удаление ребра \\(O(1)\\) \\(O(n)\\) \\(O(1)\\) Добавление вершины \\(O(n)\\) \\(O(1)\\) \\(O(1)\\) Удаление вершины \\(O(n^2)\\) \\(O(n + m)\\) \\(O(n)\\) Занимаемая память \\(O(n^2)\\) \\(O(n + m)\\) \\(O(n + m)\\)

        Если судить только по данным в таблице 9-2, может показаться, что список смежности на основе хеш-таблицы является лучшим и по времени, и по памяти. Но на практике операции над ребрами в матрице смежности обычно выполняются быстрее, потому что там нужен лишь один доступ к массиву или одно присваивание. В целом матрица смежности воплощает принцип «обмена пространства на время», а список смежности - принцип «обмена времени на пространство».

        ","path":["Глава 9. Графы","9.2   Базовые операции графа"],"tags":[]},{"location":"chapter_graph/graph_traversal/","level":1,"title":"9.3   Обход графа","text":"

        Дерево представляет отношение «один ко многим», тогда как граф обладает большей свободой и может выражать произвольные отношения «многие ко многим». Поэтому дерево можно рассматривать как частный случай графа. Очевидно, что операции обхода дерева также являются частным случаем операций обхода графа.

        И графы, и деревья требуют применения алгоритмов обхода. Способы обхода графа также делятся на два типа: обход в ширину и обход в глубину.

        ","path":["Глава 9. Графы","9.3   Обход графа"],"tags":[]},{"location":"chapter_graph/graph_traversal/#931","level":2,"title":"9.3.1   Обход в ширину","text":"

        Обход в ширину - это способ обхода от ближнего к дальнему, при котором начиная с некоторого узла сначала посещают ближайшие вершины, а затем слой за слоем расширяются наружу. Как показано на рисунке 9-9, начиная с вершины в левом верхнем углу, мы сначала обходим все смежные вершины этой вершины, затем все смежные вершины следующей вершины и так далее, пока не будут посещены все вершины.

        Рисунок 9-9   Обход графа в ширину

        ","path":["Глава 9. Графы","9.3   Обход графа"],"tags":[]},{"location":"chapter_graph/graph_traversal/#1","level":3,"title":"1.   Реализация алгоритма","text":"

        BFS обычно реализуется с помощью очереди, код приведен ниже. Очередь обладает свойством «первым пришел - первым вышел», что хорошо соответствует идее BFS «от ближнего к дальнему».

        1. Поместить стартовую вершину обхода startVet в очередь и запустить цикл.
        2. На каждой итерации цикла извлекать вершину из головы очереди и записывать ее посещение, после чего добавлять все смежные вершины этой вершины в хвост очереди.
        3. Повторять шаг 2. до тех пор, пока не будут посещены все вершины.

        Чтобы предотвратить повторный обход вершин, нам нужно хеш-множество visited , в котором записывается, какие вершины уже посещены.

        Tip

        Хеш-множество можно рассматривать как хеш-таблицу, которая хранит только key и не хранит value . Оно позволяет выполнять добавление, удаление и проверку наличия key за \\(O(1)\\) времени. Благодаря уникальности key хеш-множество обычно используется, например, для устранения повторов.

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_bfs.py
        def graph_bfs(graph: GraphAdjList, start_vet: Vertex) -> list[Vertex]:\n    \"\"\"Обход в ширину\"\"\"\n    # Использовать список смежности для представления графа, чтобы получать все смежные вершины заданной вершины\n    # Последовательность обхода вершин\n    res = []\n    # Хеш-множество для хранения уже посещенных вершин\n    visited = set[Vertex]([start_vet])\n    # Очередь используется для реализации BFS\n    que = deque[Vertex]([start_vet])\n    # Начиная с вершины vet, продолжать цикл, пока не будут посещены все вершины\n    while len(que) > 0:\n        vet = que.popleft()  # Извлечь головную вершину из очереди\n        res.append(vet)  # Отметить посещенную вершину\n        # Обойти все смежные вершины данной вершины\n        for adj_vet in graph.adj_list[vet]:\n            if adj_vet in visited:\n                continue  # Пропустить уже посещенную вершину\n            que.append(adj_vet)  # Помещать в очередь только непосещенные вершины\n            visited.add(adj_vet)  # Отметить эту вершину как посещенную\n    # Вернуть последовательность обхода вершин\n    return res\n
        graph_bfs.cpp
        /* Обход в ширину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nvector<Vertex *> graphBFS(GraphAdjList &graph, Vertex *startVet) {\n    // Последовательность обхода вершин\n    vector<Vertex *> res;\n    // Хеш-множество для хранения уже посещенных вершин\n    unordered_set<Vertex *> visited = {startVet};\n    // Очередь используется для реализации BFS\n    queue<Vertex *> que;\n    que.push(startVet);\n    // Начиная с вершины vet, продолжать цикл, пока не будут посещены все вершины\n    while (!que.empty()) {\n        Vertex *vet = que.front();\n        que.pop();          // Извлечь головную вершину из очереди\n        res.push_back(vet); // Отметить посещенную вершину\n        // Обойти все смежные вершины данной вершины\n        for (auto adjVet : graph.adjList[vet]) {\n            if (visited.count(adjVet))\n                continue;            // Пропустить уже посещенную вершину\n            que.push(adjVet);        // Помещать в очередь только непосещенные вершины\n            visited.emplace(adjVet); // Отметить эту вершину как посещенную\n        }\n    }\n    // Вернуть последовательность обхода вершин\n    return res;\n}\n
        graph_bfs.java
        /* Обход в ширину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nList<Vertex> graphBFS(GraphAdjList graph, Vertex startVet) {\n    // Последовательность обхода вершин\n    List<Vertex> res = new ArrayList<>();\n    // Хеш-множество для хранения уже посещенных вершин\n    Set<Vertex> visited = new HashSet<>();\n    visited.add(startVet);\n    // Очередь используется для реализации BFS\n    Queue<Vertex> que = new LinkedList<>();\n    que.offer(startVet);\n    // Начиная с вершины vet, продолжать цикл, пока не будут посещены все вершины\n    while (!que.isEmpty()) {\n        Vertex vet = que.poll(); // Извлечь головную вершину из очереди\n        res.add(vet);            // Отметить посещенную вершину\n        // Обойти все смежные вершины данной вершины\n        for (Vertex adjVet : graph.adjList.get(vet)) {\n            if (visited.contains(adjVet))\n                continue;        // Пропустить уже посещенную вершину\n            que.offer(adjVet);   // Помещать в очередь только непосещенные вершины\n            visited.add(adjVet); // Отметить эту вершину как посещенную\n        }\n    }\n    // Вернуть последовательность обхода вершин\n    return res;\n}\n
        graph_bfs.cs
        /* Обход в ширину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nList<Vertex> GraphBFS(GraphAdjList graph, Vertex startVet) {\n    // Последовательность обхода вершин\n    List<Vertex> res = [];\n    // Хеш-множество для хранения уже посещенных вершин\n    HashSet<Vertex> visited = [startVet];\n    // Очередь используется для реализации BFS\n    Queue<Vertex> que = new();\n    que.Enqueue(startVet);\n    // Начиная с вершины vet, продолжать цикл, пока не будут посещены все вершины\n    while (que.Count > 0) {\n        Vertex vet = que.Dequeue(); // Извлечь головную вершину из очереди\n        res.Add(vet);               // Отметить посещенную вершину\n        foreach (Vertex adjVet in graph.adjList[vet]) {\n            if (visited.Contains(adjVet)) {\n                continue;          // Пропустить уже посещенную вершину\n            }\n            que.Enqueue(adjVet);   // Помещать в очередь только непосещенные вершины\n            visited.Add(adjVet);   // Отметить эту вершину как посещенную\n        }\n    }\n\n    // Вернуть последовательность обхода вершин\n    return res;\n}\n
        graph_bfs.go
        /* Обход в ширину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nfunc graphBFS(g *graphAdjList, startVet Vertex) []Vertex {\n    // Последовательность обхода вершин\n    res := make([]Vertex, 0)\n    // Хеш-множество для хранения уже посещенных вершин\n    visited := make(map[Vertex]struct{})\n    visited[startVet] = struct{}{}\n    // Очередь используется для реализации BFS, срез используется для имитации очереди\n    queue := make([]Vertex, 0)\n    queue = append(queue, startVet)\n    // Начиная с вершины vet, продолжать цикл, пока не будут посещены все вершины\n    for len(queue) > 0 {\n        // Извлечь головную вершину из очереди\n        vet := queue[0]\n        queue = queue[1:]\n        // Отметить посещенную вершину\n        res = append(res, vet)\n        // Обойти все смежные вершины данной вершины\n        for _, adjVet := range g.adjList[vet] {\n            _, isExist := visited[adjVet]\n            // Помещать в очередь только непосещенные вершины\n            if !isExist {\n                queue = append(queue, adjVet)\n                visited[adjVet] = struct{}{}\n            }\n        }\n    }\n    // Вернуть последовательность обхода вершин\n    return res\n}\n
        graph_bfs.swift
        /* Обход в ширину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nfunc graphBFS(graph: GraphAdjList, startVet: Vertex) -> [Vertex] {\n    // Последовательность обхода вершин\n    var res: [Vertex] = []\n    // Хеш-множество для хранения уже посещенных вершин\n    var visited: Set<Vertex> = [startVet]\n    // Очередь используется для реализации BFS\n    var que: [Vertex] = [startVet]\n    // Начиная с вершины vet, продолжать цикл, пока не будут посещены все вершины\n    while !que.isEmpty {\n        let vet = que.removeFirst() // Извлечь головную вершину из очереди\n        res.append(vet) // Отметить посещенную вершину\n        // Обойти все смежные вершины данной вершины\n        for adjVet in graph.adjList[vet] ?? [] {\n            if visited.contains(adjVet) {\n                continue // Пропустить уже посещенную вершину\n            }\n            que.append(adjVet) // Помещать в очередь только непосещенные вершины\n            visited.insert(adjVet) // Отметить эту вершину как посещенную\n        }\n    }\n    // Вернуть последовательность обхода вершин\n    return res\n}\n
        graph_bfs.js
        /* Обход в ширину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nfunction graphBFS(graph, startVet) {\n    // Последовательность обхода вершин\n    const res = [];\n    // Хеш-множество для хранения уже посещенных вершин\n    const visited = new Set();\n    visited.add(startVet);\n    // Очередь используется для реализации BFS\n    const que = [startVet];\n    // Начиная с вершины vet, продолжать цикл, пока не будут посещены все вершины\n    while (que.length) {\n        const vet = que.shift(); // Извлечь головную вершину из очереди\n        res.push(vet); // Отметить посещенную вершину\n        // Обойти все смежные вершины данной вершины\n        for (const adjVet of graph.adjList.get(vet) ?? []) {\n            if (visited.has(adjVet)) {\n                continue; // Пропустить уже посещенную вершину\n            }\n            que.push(adjVet); // Помещать в очередь только непосещенные вершины\n            visited.add(adjVet); // Отметить эту вершину как посещенную\n        }\n    }\n    // Вернуть последовательность обхода вершин\n    return res;\n}\n
        graph_bfs.ts
        /* Обход в ширину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nfunction graphBFS(graph: GraphAdjList, startVet: Vertex): Vertex[] {\n    // Последовательность обхода вершин\n    const res: Vertex[] = [];\n    // Хеш-множество для хранения уже посещенных вершин\n    const visited: Set<Vertex> = new Set();\n    visited.add(startVet);\n    // Очередь используется для реализации BFS\n    const que = [startVet];\n    // Начиная с вершины vet, продолжать цикл, пока не будут посещены все вершины\n    while (que.length) {\n        const vet = que.shift(); // Извлечь головную вершину из очереди\n        res.push(vet); // Отметить посещенную вершину\n        // Обойти все смежные вершины данной вершины\n        for (const adjVet of graph.adjList.get(vet) ?? []) {\n            if (visited.has(adjVet)) {\n                continue; // Пропустить уже посещенную вершину\n            }\n            que.push(adjVet); // Помещать в очередь только непосещенные вершины\n            visited.add(adjVet); // Отметить эту вершину как посещенную\n        }\n    }\n    // Вернуть последовательность обхода вершин\n    return res;\n}\n
        graph_bfs.dart
        /* Обход в ширину */\nList<Vertex> graphBFS(GraphAdjList graph, Vertex startVet) {\n  // Использовать список смежности для представления графа, чтобы получать все смежные вершины заданной вершины\n  // Последовательность обхода вершин\n  List<Vertex> res = [];\n  // Хеш-множество для хранения уже посещенных вершин\n  Set<Vertex> visited = {};\n  visited.add(startVet);\n  // Очередь используется для реализации BFS\n  Queue<Vertex> que = Queue();\n  que.add(startVet);\n  // Начиная с вершины vet, продолжать цикл, пока не будут посещены все вершины\n  while (que.isNotEmpty) {\n    Vertex vet = que.removeFirst(); // Извлечь головную вершину из очереди\n    res.add(vet); // Отметить посещенную вершину\n    // Обойти все смежные вершины данной вершины\n    for (Vertex adjVet in graph.adjList[vet]!) {\n      if (visited.contains(adjVet)) {\n        continue; // Пропустить уже посещенную вершину\n      }\n      que.add(adjVet); // Помещать в очередь только непосещенные вершины\n      visited.add(adjVet); // Отметить эту вершину как посещенную\n    }\n  }\n  // Вернуть последовательность обхода вершин\n  return res;\n}\n
        graph_bfs.rs
        /* Обход в ширину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nfn graph_bfs(graph: GraphAdjList, start_vet: Vertex) -> Vec<Vertex> {\n    // Последовательность обхода вершин\n    let mut res = vec![];\n    // Хеш-множество для хранения уже посещенных вершин\n    let mut visited = HashSet::new();\n    visited.insert(start_vet);\n    // Очередь используется для реализации BFS\n    let mut que = VecDeque::new();\n    que.push_back(start_vet);\n    // Начиная с вершины vet, продолжать цикл, пока не будут посещены все вершины\n    while let Some(vet) = que.pop_front() {\n        res.push(vet); // Отметить посещенную вершину\n\n        // Обойти все смежные вершины данной вершины\n        if let Some(adj_vets) = graph.adj_list.get(&vet) {\n            for &adj_vet in adj_vets {\n                if visited.contains(&adj_vet) {\n                    continue; // Пропустить уже посещенную вершину\n                }\n                que.push_back(adj_vet); // Помещать в очередь только непосещенные вершины\n                visited.insert(adj_vet); // Отметить эту вершину как посещенную\n            }\n        }\n    }\n    // Вернуть последовательность обхода вершин\n    res\n}\n
        graph_bfs.c
        /* Структура очереди узлов */\ntypedef struct {\n    Vertex *vertices[MAX_SIZE];\n    int front, rear, size;\n} Queue;\n\n/* Конструктор */\nQueue *newQueue() {\n    Queue *q = (Queue *)malloc(sizeof(Queue));\n    q->front = q->rear = q->size = 0;\n    return q;\n}\n\n/* Проверка, пуста ли очередь */\nint isEmpty(Queue *q) {\n    return q->size == 0;\n}\n\n/* Операция добавления в очередь */\nvoid enqueue(Queue *q, Vertex *vet) {\n    q->vertices[q->rear] = vet;\n    q->rear = (q->rear + 1) % MAX_SIZE;\n    q->size++;\n}\n\n/* Операция извлечения из очереди */\nVertex *dequeue(Queue *q) {\n    Vertex *vet = q->vertices[q->front];\n    q->front = (q->front + 1) % MAX_SIZE;\n    q->size--;\n    return vet;\n}\n\n/* Проверить, была ли вершина уже посещена */\nint isVisited(Vertex **visited, int size, Vertex *vet) {\n    // Искать узел обходом за O(n) времени\n    for (int i = 0; i < size; i++) {\n        if (visited[i] == vet)\n            return 1;\n    }\n    return 0;\n}\n\n/* Обход в ширину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nvoid graphBFS(GraphAdjList *graph, Vertex *startVet, Vertex **res, int *resSize, Vertex **visited, int *visitedSize) {\n    // Очередь используется для реализации BFS\n    Queue *queue = newQueue();\n    enqueue(queue, startVet);\n    visited[(*visitedSize)++] = startVet;\n    // Начиная с вершины vet, продолжать цикл, пока не будут посещены все вершины\n    while (!isEmpty(queue)) {\n        Vertex *vet = dequeue(queue); // Извлечь головную вершину из очереди\n        res[(*resSize)++] = vet;      // Отметить посещенную вершину\n        // Обойти все смежные вершины данной вершины\n        AdjListNode *node = findNode(graph, vet);\n        while (node != NULL) {\n            // Пропустить уже посещенную вершину\n            if (!isVisited(visited, *visitedSize, node->vertex)) {\n                enqueue(queue, node->vertex);             // Помещать в очередь только непосещенные вершины\n                visited[(*visitedSize)++] = node->vertex; // Отметить эту вершину как посещенную\n            }\n            node = node->next;\n        }\n    }\n    // Освободить память\n    free(queue);\n}\n
        graph_bfs.kt
        /* Обход в ширину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nfun graphBFS(graph: GraphAdjList, startVet: Vertex): MutableList<Vertex?> {\n    // Последовательность обхода вершин\n    val res = mutableListOf<Vertex?>()\n    // Хеш-множество для хранения уже посещенных вершин\n    val visited = HashSet<Vertex>()\n    visited.add(startVet)\n    // Очередь используется для реализации BFS\n    val que = LinkedList<Vertex>()\n    que.offer(startVet)\n    // Начиная с вершины vet, продолжать цикл, пока не будут посещены все вершины\n    while (!que.isEmpty()) {\n        val vet = que.poll() // Извлечь головную вершину из очереди\n        res.add(vet)         // Отметить посещенную вершину\n        // Обойти все смежные вершины данной вершины\n        for (adjVet in graph.adjList[vet]!!) {\n            if (visited.contains(adjVet))\n                continue        // Пропустить уже посещенную вершину\n            que.offer(adjVet)   // Помещать в очередь только непосещенные вершины\n            visited.add(adjVet) // Отметить эту вершину как посещенную\n        }\n    }\n    // Вернуть последовательность обхода вершин\n    return res\n}\n
        graph_bfs.rb
        ### Обход в ширину ###\ndef graph_bfs(graph, start_vet)\n  # Использовать список смежности для представления графа, чтобы получать все смежные вершины заданной вершины\n  # Последовательность обхода вершин\n  res = []\n  # Хеш-множество для хранения уже посещенных вершин\n  visited = Set.new([start_vet])\n  # Очередь используется для реализации BFS\n  que = [start_vet]\n  # Начиная с вершины vet, продолжать цикл, пока не будут посещены все вершины\n  while que.length > 0\n    vet = que.shift # Извлечь головную вершину из очереди\n    res << vet # Отметить посещенную вершину\n    # Обойти все смежные вершины данной вершины\n    for adj_vet in graph.adj_list[vet]\n      next if visited.include?(adj_vet) # Пропустить уже посещенную вершину\n      que << adj_vet # Помещать в очередь только непосещенные вершины\n      visited.add(adj_vet) # Отметить эту вершину как посещенную\n    end\n  end\n  # Вернуть последовательность обхода вершин\n  res\nend\n
        Визуализация кода

        Во весь экран >

        Код сравнительно абстрактен, поэтому для лучшего понимания рекомендуется сопоставлять его с тем, что показано на рисунке 9-10.

        <1><2><3><4><5><6><7><8><9><10><11>

        Рисунок 9-10   Шаги обхода графа в ширину

        Является ли последовательность обхода в ширину единственной?

        Нет. Обход в ширину требует только соблюдения порядка «от ближнего к дальнему», а порядок обхода нескольких вершин на одинаковом расстоянии может произвольно меняться. Например, на рисунке 9-10 можно поменять местами порядок посещения вершин \\(1\\) и \\(3\\) , а вершины \\(2\\), \\(4\\), \\(6\\) также можно переставлять произвольно.

        ","path":["Глава 9. Графы","9.3   Обход графа"],"tags":[]},{"location":"chapter_graph/graph_traversal/#2","level":3,"title":"2.   Анализ сложности","text":"

        Временная сложность: все вершины по одному разу помещаются в очередь и извлекаются из нее, что требует \\(O(|V|)\\) времени. При обходе смежных вершин, поскольку граф неориентированный, все ребра будут посещены по \\(2\\) раза, что требует \\(O(2|E|)\\) времени. В сумме получается \\(O(|V| + |E|)\\) .

        Пространственная сложность: список res , хеш-множество visited и очередь que в худшем случае могут содержать до \\(|V|\\) вершин, поэтому требуется \\(O(|V|)\\) памяти.

        ","path":["Глава 9. Графы","9.3   Обход графа"],"tags":[]},{"location":"chapter_graph/graph_traversal/#932","level":2,"title":"9.3.2   Обход в глубину","text":"

        Обход в глубину - это способ обхода, при котором сначала идут до самого конца, а когда дальше идти нельзя, возвращаются назад. Как показано на рисунке 9-11, начиная с вершины в левом верхнем углу, мы выбираем некоторую смежную вершину текущей вершины, идем до упора, затем возвращаемся назад, снова идем до упора и так далее, пока не будут посещены все вершины.

        Рисунок 9-11   Обход графа в глубину

        ","path":["Глава 9. Графы","9.3   Обход графа"],"tags":[]},{"location":"chapter_graph/graph_traversal/#1_1","level":3,"title":"1.   Реализация алгоритма","text":"

        Такой алгоритмический шаблон «дойти до конца и вернуться» обычно реализуется через рекурсию. Подобно обходу в ширину, в обходе в глубину мы также используем хеш-множество visited для записи уже посещенных вершин и тем самым избегаем повторного посещения.

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_dfs.py
        def dfs(graph: GraphAdjList, visited: set[Vertex], res: list[Vertex], vet: Vertex):\n    \"\"\"Вспомогательная функция обхода в глубину\"\"\"\n    res.append(vet)  # Отметить посещенную вершину\n    visited.add(vet)  # Отметить эту вершину как посещенную\n    # Обойти все смежные вершины данной вершины\n    for adjVet in graph.adj_list[vet]:\n        if adjVet in visited:\n            continue  # Пропустить уже посещенную вершину\n        # Рекурсивно обходить смежные вершины\n        dfs(graph, visited, res, adjVet)\n\ndef graph_dfs(graph: GraphAdjList, start_vet: Vertex) -> list[Vertex]:\n    \"\"\"Обход в глубину\"\"\"\n    # Использовать список смежности для представления графа, чтобы получать все смежные вершины заданной вершины\n    # Последовательность обхода вершин\n    res = []\n    # Хеш-множество для хранения уже посещенных вершин\n    visited = set[Vertex]()\n    dfs(graph, visited, res, start_vet)\n    return res\n
        graph_dfs.cpp
        /* Вспомогательная функция обхода в глубину */\nvoid dfs(GraphAdjList &graph, unordered_set<Vertex *> &visited, vector<Vertex *> &res, Vertex *vet) {\n    res.push_back(vet);   // Отметить посещенную вершину\n    visited.emplace(vet); // Отметить эту вершину как посещенную\n    // Обойти все смежные вершины данной вершины\n    for (Vertex *adjVet : graph.adjList[vet]) {\n        if (visited.count(adjVet))\n            continue; // Пропустить уже посещенную вершину\n        // Рекурсивно обходить смежные вершины\n        dfs(graph, visited, res, adjVet);\n    }\n}\n\n/* Обход в глубину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nvector<Vertex *> graphDFS(GraphAdjList &graph, Vertex *startVet) {\n    // Последовательность обхода вершин\n    vector<Vertex *> res;\n    // Хеш-множество для хранения уже посещенных вершин\n    unordered_set<Vertex *> visited;\n    dfs(graph, visited, res, startVet);\n    return res;\n}\n
        graph_dfs.java
        /* Вспомогательная функция обхода в глубину */\nvoid dfs(GraphAdjList graph, Set<Vertex> visited, List<Vertex> res, Vertex vet) {\n    res.add(vet);     // Отметить посещенную вершину\n    visited.add(vet); // Отметить эту вершину как посещенную\n    // Обойти все смежные вершины данной вершины\n    for (Vertex adjVet : graph.adjList.get(vet)) {\n        if (visited.contains(adjVet))\n            continue; // Пропустить уже посещенную вершину\n        // Рекурсивно обходить смежные вершины\n        dfs(graph, visited, res, adjVet);\n    }\n}\n\n/* Обход в глубину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nList<Vertex> graphDFS(GraphAdjList graph, Vertex startVet) {\n    // Последовательность обхода вершин\n    List<Vertex> res = new ArrayList<>();\n    // Хеш-множество для хранения уже посещенных вершин\n    Set<Vertex> visited = new HashSet<>();\n    dfs(graph, visited, res, startVet);\n    return res;\n}\n
        graph_dfs.cs
        /* Вспомогательная функция обхода в глубину */\nvoid DFS(GraphAdjList graph, HashSet<Vertex> visited, List<Vertex> res, Vertex vet) {\n    res.Add(vet);     // Отметить посещенную вершину\n    visited.Add(vet); // Отметить эту вершину как посещенную\n    // Обойти все смежные вершины данной вершины\n    foreach (Vertex adjVet in graph.adjList[vet]) {\n        if (visited.Contains(adjVet)) {\n            continue; // Пропустить уже посещенную вершину\n        }\n        // Рекурсивно обходить смежные вершины\n        DFS(graph, visited, res, adjVet);\n    }\n}\n\n/* Обход в глубину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nList<Vertex> GraphDFS(GraphAdjList graph, Vertex startVet) {\n    // Последовательность обхода вершин\n    List<Vertex> res = [];\n    // Хеш-множество для хранения уже посещенных вершин\n    HashSet<Vertex> visited = [];\n    DFS(graph, visited, res, startVet);\n    return res;\n}\n
        graph_dfs.go
        /* Вспомогательная функция обхода в глубину */\nfunc dfs(g *graphAdjList, visited map[Vertex]struct{}, res *[]Vertex, vet Vertex) {\n    // Операция append возвращает новую ссылку, поэтому исходную ссылку нужно заново присвоить новому срезу\n    *res = append(*res, vet)\n    visited[vet] = struct{}{}\n    // Обойти все смежные вершины данной вершины\n    for _, adjVet := range g.adjList[vet] {\n        _, isExist := visited[adjVet]\n        // Рекурсивно обходить смежные вершины\n        if !isExist {\n            dfs(g, visited, res, adjVet)\n        }\n    }\n}\n\n/* Обход в глубину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nfunc graphDFS(g *graphAdjList, startVet Vertex) []Vertex {\n    // Последовательность обхода вершин\n    res := make([]Vertex, 0)\n    // Хеш-множество для хранения уже посещенных вершин\n    visited := make(map[Vertex]struct{})\n    dfs(g, visited, &res, startVet)\n    // Вернуть последовательность обхода вершин\n    return res\n}\n
        graph_dfs.swift
        /* Вспомогательная функция обхода в глубину */\nfunc dfs(graph: GraphAdjList, visited: inout Set<Vertex>, res: inout [Vertex], vet: Vertex) {\n    res.append(vet) // Отметить посещенную вершину\n    visited.insert(vet) // Отметить эту вершину как посещенную\n    // Обойти все смежные вершины данной вершины\n    for adjVet in graph.adjList[vet] ?? [] {\n        if visited.contains(adjVet) {\n            continue // Пропустить уже посещенную вершину\n        }\n        // Рекурсивно обходить смежные вершины\n        dfs(graph: graph, visited: &visited, res: &res, vet: adjVet)\n    }\n}\n\n/* Обход в глубину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nfunc graphDFS(graph: GraphAdjList, startVet: Vertex) -> [Vertex] {\n    // Последовательность обхода вершин\n    var res: [Vertex] = []\n    // Хеш-множество для хранения уже посещенных вершин\n    var visited: Set<Vertex> = []\n    dfs(graph: graph, visited: &visited, res: &res, vet: startVet)\n    return res\n}\n
        graph_dfs.js
        /* Обход в глубину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nfunction dfs(graph, visited, res, vet) {\n    res.push(vet); // Отметить посещенную вершину\n    visited.add(vet); // Отметить эту вершину как посещенную\n    // Обойти все смежные вершины данной вершины\n    for (const adjVet of graph.adjList.get(vet)) {\n        if (visited.has(adjVet)) {\n            continue; // Пропустить уже посещенную вершину\n        }\n        // Рекурсивно обходить смежные вершины\n        dfs(graph, visited, res, adjVet);\n    }\n}\n\n/* Обход в глубину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nfunction graphDFS(graph, startVet) {\n    // Последовательность обхода вершин\n    const res = [];\n    // Хеш-множество для хранения уже посещенных вершин\n    const visited = new Set();\n    dfs(graph, visited, res, startVet);\n    return res;\n}\n
        graph_dfs.ts
        /* Вспомогательная функция обхода в глубину */\nfunction dfs(\n    graph: GraphAdjList,\n    visited: Set<Vertex>,\n    res: Vertex[],\n    vet: Vertex\n): void {\n    res.push(vet); // Отметить посещенную вершину\n    visited.add(vet); // Отметить эту вершину как посещенную\n    // Обойти все смежные вершины данной вершины\n    for (const adjVet of graph.adjList.get(vet)) {\n        if (visited.has(adjVet)) {\n            continue; // Пропустить уже посещенную вершину\n        }\n        // Рекурсивно обходить смежные вершины\n        dfs(graph, visited, res, adjVet);\n    }\n}\n\n/* Обход в глубину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nfunction graphDFS(graph: GraphAdjList, startVet: Vertex): Vertex[] {\n    // Последовательность обхода вершин\n    const res: Vertex[] = [];\n    // Хеш-множество для хранения уже посещенных вершин\n    const visited: Set<Vertex> = new Set();\n    dfs(graph, visited, res, startVet);\n    return res;\n}\n
        graph_dfs.dart
        /* Вспомогательная функция обхода в глубину */\nvoid dfs(\n  GraphAdjList graph,\n  Set<Vertex> visited,\n  List<Vertex> res,\n  Vertex vet,\n) {\n  res.add(vet); // Отметить посещенную вершину\n  visited.add(vet); // Отметить эту вершину как посещенную\n  // Обойти все смежные вершины данной вершины\n  for (Vertex adjVet in graph.adjList[vet]!) {\n    if (visited.contains(adjVet)) {\n      continue; // Пропустить уже посещенную вершину\n    }\n    // Рекурсивно обходить смежные вершины\n    dfs(graph, visited, res, adjVet);\n  }\n}\n\n/* Обход в глубину */\nList<Vertex> graphDFS(GraphAdjList graph, Vertex startVet) {\n  // Последовательность обхода вершин\n  List<Vertex> res = [];\n  // Хеш-множество для хранения уже посещенных вершин\n  Set<Vertex> visited = {};\n  dfs(graph, visited, res, startVet);\n  return res;\n}\n
        graph_dfs.rs
        /* Вспомогательная функция обхода в глубину */\nfn dfs(graph: &GraphAdjList, visited: &mut HashSet<Vertex>, res: &mut Vec<Vertex>, vet: Vertex) {\n    res.push(vet); // Отметить посещенную вершину\n    visited.insert(vet); // Отметить эту вершину как посещенную\n                         // Обойти все смежные вершины данной вершины\n    if let Some(adj_vets) = graph.adj_list.get(&vet) {\n        for &adj_vet in adj_vets {\n            if visited.contains(&adj_vet) {\n                continue; // Пропустить уже посещенную вершину\n            }\n            // Рекурсивно обходить смежные вершины\n            dfs(graph, visited, res, adj_vet);\n        }\n    }\n}\n\n/* Обход в глубину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nfn graph_dfs(graph: GraphAdjList, start_vet: Vertex) -> Vec<Vertex> {\n    // Последовательность обхода вершин\n    let mut res = vec![];\n    // Хеш-множество для хранения уже посещенных вершин\n    let mut visited = HashSet::new();\n    dfs(&graph, &mut visited, &mut res, start_vet);\n\n    res\n}\n
        graph_dfs.c
        /* Проверить, была ли вершина уже посещена */\nint isVisited(Vertex **res, int size, Vertex *vet) {\n    // Искать узел обходом за O(n) времени\n    for (int i = 0; i < size; i++) {\n        if (res[i] == vet) {\n            return 1;\n        }\n    }\n    return 0;\n}\n\n/* Вспомогательная функция обхода в глубину */\nvoid dfs(GraphAdjList *graph, Vertex **res, int *resSize, Vertex *vet) {\n    // Отметить посещенную вершину\n    res[(*resSize)++] = vet;\n    // Обойти все смежные вершины данной вершины\n    AdjListNode *node = findNode(graph, vet);\n    while (node != NULL) {\n        // Пропустить уже посещенную вершину\n        if (!isVisited(res, *resSize, node->vertex)) {\n            // Рекурсивно обходить смежные вершины\n            dfs(graph, res, resSize, node->vertex);\n        }\n        node = node->next;\n    }\n}\n\n/* Обход в глубину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nvoid graphDFS(GraphAdjList *graph, Vertex *startVet, Vertex **res, int *resSize) {\n    dfs(graph, res, resSize, startVet);\n}\n
        graph_dfs.kt
        /* Вспомогательная функция обхода в глубину */\nfun dfs(\n    graph: GraphAdjList,\n    visited: MutableSet<Vertex?>,\n    res: MutableList<Vertex?>,\n    vet: Vertex?\n) {\n    res.add(vet)     // Отметить посещенную вершину\n    visited.add(vet) // Отметить эту вершину как посещенную\n    // Обойти все смежные вершины данной вершины\n    for (adjVet in graph.adjList[vet]!!) {\n        if (visited.contains(adjVet))\n            continue  // Пропустить уже посещенную вершину\n        // Рекурсивно обходить смежные вершины\n        dfs(graph, visited, res, adjVet)\n    }\n}\n\n/* Обход в глубину */\n// Использовать список смежности для представления графа, чтобы получить все смежные вершины заданной вершины\nfun graphDFS(graph: GraphAdjList, startVet: Vertex?): MutableList<Vertex?> {\n    // Последовательность обхода вершин\n    val res = mutableListOf<Vertex?>()\n    // Хеш-множество для хранения уже посещенных вершин\n    val visited = HashSet<Vertex?>()\n    dfs(graph, visited, res, startVet)\n    return res\n}\n
        graph_dfs.rb
        ### Вспомогательная функция обхода в глубину ###\ndef dfs(graph, visited, res, vet)\n  res << vet # Отметить посещенную вершину\n  visited.add(vet) # Отметить эту вершину как посещенную\n  # Обойти все смежные вершины данной вершины\n  for adj_vet in graph.adj_list[vet]\n    next if visited.include?(adj_vet) # Пропустить уже посещенную вершину\n    # Рекурсивно обходить смежные вершины\n    dfs(graph, visited, res, adj_vet)\n  end\nend\n\n### Обход в глубину ###\ndef graph_dfs(graph, start_vet)\n  # Использовать список смежности для представления графа, чтобы получать все смежные вершины заданной вершины\n  # Последовательность обхода вершин\n  res = []\n  # Хеш-множество для хранения уже посещенных вершин\n  visited = Set.new\n  dfs(graph, visited, res, start_vet)\n  res\nend\n
        Визуализация кода

        Во весь экран >

        Алгоритмический процесс обхода в глубину показан на рисунке 9-12.

        • Прямая пунктирная линия обозначает нисходящую рекурсию , то есть запуск нового рекурсивного метода для посещения новой вершины.
        • Изогнутая пунктирная линия обозначает восходящую рекурсию , то есть данный рекурсивный метод завершился и управление вернулось туда, откуда он был вызван.

        Чтобы лучше понять алгоритм, рекомендуется сопоставить код с тем, что показано на рисунке 9-12, и мысленно проследить весь процесс DFS, включая моменты запуска и возврата каждого рекурсивного вызова.

        <1><2><3><4><5><6><7><8><9><10><11>

        Рисунок 9-12   Шаги обхода графа в глубину

        Является ли последовательность обхода в глубину единственной?

        Как и в случае обхода в ширину, последовательность DFS тоже не является единственной. Для заданной вершины допустимо сначала углубиться в любое направление, то есть порядок смежных вершин может быть произвольным, и все такие варианты будут корректными обходами в глубину.

        Если взять в качестве примера обход дерева, то варианты «корень \\(\\rightarrow\\) лево \\(\\rightarrow\\) право», «лево \\(\\rightarrow\\) корень \\(\\rightarrow\\) право» и «лево \\(\\rightarrow\\) право \\(\\rightarrow\\) корень» соответствуют прямому, симметричному и обратному обходам соответственно. Они показывают три разных приоритета обхода, но все они относятся к обходу в глубину.

        ","path":["Глава 9. Графы","9.3   Обход графа"],"tags":[]},{"location":"chapter_graph/graph_traversal/#2_1","level":3,"title":"2.   Анализ сложности","text":"

        Временная сложность: все вершины будут посещены по \\(1\\) разу, что требует \\(O(|V|)\\) времени. Все ребра будут посещены по \\(2\\) раза, что требует \\(O(2|E|)\\) времени. Суммарно получается \\(O(|V| + |E|)\\) .

        Пространственная сложность: число вершин в списке res и хеш-множестве visited в худшем случае достигает \\(|V|\\) , максимальная глубина рекурсии тоже равна \\(|V|\\) , поэтому требуется \\(O(|V|)\\) памяти.

        ","path":["Глава 9. Графы","9.3   Обход графа"],"tags":[]},{"location":"chapter_graph/summary/","level":1,"title":"9.4   Краткие итоги","text":"","path":["Глава 9. Графы","9.4   Краткие итоги"],"tags":[]},{"location":"chapter_graph/summary/#1","level":3,"title":"1.   Основные моменты","text":"
        • Граф состоит из вершин и ребер и может быть задан как множество вершин и множество ребер.
        • По сравнению с линейными отношениями (связный список) и отношениями разделения (дерево), сетевые отношения (граф) обладают большей свободой и потому более сложны.
        • Ребра ориентированного графа имеют направление, в связном графе любые вершины достижимы, а во взвешенном графе каждое ребро содержит переменную веса.
        • Матрица смежности использует матрицу для представления графа: каждая строка и каждый столбец соответствуют вершине, а элементы матрицы показывают, есть между двумя вершинами ребро или нет. Матрица смежности эффективна в операциях добавления, удаления, поиска и изменения, но расходует больше памяти.
        • Список смежности использует несколько списков для представления графа. \\(i\\)-й список соответствует вершине \\(i\\) и хранит все ее смежные вершины. По сравнению с матрицей смежности список смежности экономит пространство, но для поиска ребра в нем приходится обходить список, поэтому по времени он уступает.
        • Когда списки в списке смежности становятся слишком длинными, их можно преобразовать в красно-черное дерево или хеш-таблицу, чтобы повысить эффективность поиска.
        • С точки зрения алгоритмической идеи матрица смежности отражает принцип «обмена пространства на время», а список смежности - принцип «обмена времени на пространство».
        • Графы можно использовать для моделирования различных реальных систем, таких как социальные сети, линии метро и так далее.
        • Дерево является частным случаем графа, а обход дерева - частным случаем обхода графа.
        • Обход графа в ширину представляет собой способ поиска, который расширяется от ближнего к дальнему и обычно реализуется с помощью очереди.
        • Обход графа в глубину представляет собой способ поиска, который сначала идет до самого конца, а затем возвращается назад, когда путь исчерпан. Обычно он реализуется на основе рекурсии.
        ","path":["Глава 9. Графы","9.4   Краткие итоги"],"tags":[]},{"location":"chapter_graph/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

        Q: Что считается путем: последовательность вершин или последовательность ребер?

        Определение в разных языковых версиях Википедии различается: в английской версии путь определяется как «последовательность ребер», а в русской версии - как «последовательность вершин». В английской версии исходная формулировка выглядит так: In graph theory, a path in a graph is a finite or infinite sequence of edges which joins a sequence of vertices.

        В этой книге путь рассматривается как последовательность ребер, а не как последовательность вершин. Причина в том, что между двумя вершинами может существовать несколько ребер, и в таком случае каждому ребру соответствует свой путь.

        Q: Есть ли в несвязном графе вершины, до которых нельзя дойти?

        В несвязном графе, начиная из некоторой вершины, по крайней мере одна вершина оказывается недостижимой. Чтобы обойти весь несвязный граф, нужно задать несколько стартовых точек и обойти все связные компоненты графа.

        Q: Есть ли требования к порядку вершин в списке «всех вершин, соединенных с данной вершиной» в списке смежности?

        Порядок может быть произвольным. Но на практике может понадобиться сортировка по определенному правилу, например по порядку добавления вершин или по возрастанию значений вершин. Это помогает быстро находить вершины с некоторым экстремальным свойством.

        ","path":["Глава 9. Графы","9.4   Краткие итоги"],"tags":[]},{"location":"chapter_greedy/","level":1,"title":"Глава 15.   Жадность","text":"

        Abstract

        Подсолнух поворачивается к солнцу, постоянно стремясь к наилучшим условиям для роста.

        Жадная стратегия через цепочку простых выборов постепенно приводит к наилучшему ответу.

        ","path":["Глава 15. Жадность","Глава 15.   Жадность"],"tags":[]},{"location":"chapter_greedy/#_1","level":2,"title":"Содержание главы","text":"
        • 15.1   Жадный алгоритм
        • 15.2   Задача о дробном рюкзаке
        • 15.3   Задача о максимальной вместимости
        • 15.4   Задача о максимальном произведении разбиения
        • 15.5   Резюме
        ","path":["Глава 15. Жадность","Глава 15.   Жадность"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/","level":1,"title":"15.2   Задача о дробном рюкзаке","text":"

        Question

        Дано \\(n\\) предметов. Вес предмета \\(i\\) равен \\(wgt[i-1]\\), ценность равна \\(val[i-1]\\), также дан рюкзак вместимостью \\(cap\\). Каждый предмет можно выбрать только один раз, но разрешается взять лишь часть предмета, а ценность вычисляется пропорционально взятому весу. Требуется найти максимальную ценность предметов в рюкзаке при ограниченной вместимости. Пример показан на рисунке 15-3.

        Рисунок 15-3   Пример данных для задачи о дробном рюкзаке

        Задача о дробном рюкзаке в целом очень похожа на задачу о рюкзаке 0-1: состояние включает текущий предмет \\(i\\) и вместимость \\(c\\), а цель состоит в нахождении максимальной ценности при заданной вместимости рюкзака.

        Отличие в том, что здесь разрешено брать только часть предмета. Как показано на рисунке 15-4, мы можем произвольно делить предмет и вычислять соответствующую ценность пропорционально весу.

        1. Для предмета \\(i\\) его ценность на единицу веса равна \\(val[i-1] / wgt[i-1]\\), сокращенно - удельная ценность.
        2. Если взять часть предмета \\(i\\) весом \\(w\\), то ценность рюкзака увеличится на \\(w \\times val[i-1] / wgt[i-1]\\).

        Рисунок 15-4   Ценность предмета на единицу веса

        ","path":["Глава 15. Жадность","15.2   Задача о дробном рюкзаке"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/#1","level":3,"title":"1.   Определение жадной стратегии","text":"

        Максимизация общей ценности предметов в рюкзаке по сути равносильна максимизации ценности на единицу веса. Отсюда естественно выводится жадная стратегия, показанная на рисунке 15-5.

        1. Отсортировать предметы по убыванию удельной ценности.
        2. Перебирать все предметы и на каждом шаге жадно выбирать предмет с наибольшей удельной ценностью.
        3. Если оставшейся вместимости рюкзака недостаточно, взять часть текущего предмета, чтобы заполнить рюкзак.

        Рисунок 15-5   Жадная стратегия для задачи о дробном рюкзаке

        ","path":["Глава 15. Жадность","15.2   Задача о дробном рюкзаке"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/#2","level":3,"title":"2.   Код реализации","text":"

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

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby fractional_knapsack.py
        class Item:\n    \"\"\"Предмет\"\"\"\n\n    def __init__(self, w: int, v: int):\n        self.w = w  # Вес предмета\n        self.v = v  # Стоимость предмета\n\ndef fractional_knapsack(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"Дробный рюкзак: жадный алгоритм\"\"\"\n    # Создать список предметов с двумя свойствами: вес и стоимость\n    items = [Item(w, v) for w, v in zip(wgt, val)]\n    # Отсортировать по удельной стоимости item.v / item.w в порядке убывания\n    items.sort(key=lambda item: item.v / item.w, reverse=True)\n    # Циклический жадный выбор\n    res = 0\n    for item in items:\n        if item.w <= cap:\n            # Если оставшейся вместимости достаточно, положить в рюкзак текущий предмет целиком\n            res += item.v\n            cap -= item.w\n        else:\n            # Если оставшейся вместимости недостаточно, положить в рюкзак часть текущего предмета\n            res += (item.v / item.w) * cap\n            # Свободной вместимости больше не осталось, поэтому выйти из цикла\n            break\n    return res\n
        fractional_knapsack.cpp
        /* Предмет */\nclass Item {\n  public:\n    int w; // Вес предмета\n    int v; // Стоимость предмета\n\n    Item(int w, int v) : w(w), v(v) {\n    }\n};\n\n/* Дробный рюкзак: жадный алгоритм */\ndouble fractionalKnapsack(vector<int> &wgt, vector<int> &val, int cap) {\n    // Создать список предметов с двумя свойствами: вес и стоимость\n    vector<Item> items;\n    for (int i = 0; i < wgt.size(); i++) {\n        items.push_back(Item(wgt[i], val[i]));\n    }\n    // Отсортировать по удельной стоимости item.v / item.w в порядке убывания\n    sort(items.begin(), items.end(), [](Item &a, Item &b) { return (double)a.v / a.w > (double)b.v / b.w; });\n    // Циклический жадный выбор\n    double res = 0;\n    for (auto &item : items) {\n        if (item.w <= cap) {\n            // Если оставшейся вместимости достаточно, положить в рюкзак текущий предмет целиком\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // Если оставшейся вместимости недостаточно, положить в рюкзак часть текущего предмета\n            res += (double)item.v / item.w * cap;\n            // Свободной вместимости больше не осталось, поэтому выйти из цикла\n            break;\n        }\n    }\n    return res;\n}\n
        fractional_knapsack.java
        /* Предмет */\nclass Item {\n    int w; // Вес предмета\n    int v; // Стоимость предмета\n\n    public Item(int w, int v) {\n        this.w = w;\n        this.v = v;\n    }\n}\n\n/* Дробный рюкзак: жадный алгоритм */\ndouble fractionalKnapsack(int[] wgt, int[] val, int cap) {\n    // Создать список предметов с двумя свойствами: вес и стоимость\n    Item[] items = new Item[wgt.length];\n    for (int i = 0; i < wgt.length; i++) {\n        items[i] = new Item(wgt[i], val[i]);\n    }\n    // Отсортировать по удельной стоимости item.v / item.w в порядке убывания\n    Arrays.sort(items, Comparator.comparingDouble(item -> -((double) item.v / item.w)));\n    // Циклический жадный выбор\n    double res = 0;\n    for (Item item : items) {\n        if (item.w <= cap) {\n            // Если оставшейся вместимости достаточно, положить в рюкзак текущий предмет целиком\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // Если оставшейся вместимости недостаточно, положить в рюкзак часть текущего предмета\n            res += (double) item.v / item.w * cap;\n            // Свободной вместимости больше не осталось, поэтому выйти из цикла\n            break;\n        }\n    }\n    return res;\n}\n
        fractional_knapsack.cs
        /* Предмет */\nclass Item(int w, int v) {\n    public int w = w; // Вес предмета\n    public int v = v; // Стоимость предмета\n}\n\n/* Дробный рюкзак: жадный алгоритм */\ndouble FractionalKnapsack(int[] wgt, int[] val, int cap) {\n    // Создать список предметов с двумя свойствами: вес и стоимость\n    Item[] items = new Item[wgt.Length];\n    for (int i = 0; i < wgt.Length; i++) {\n        items[i] = new Item(wgt[i], val[i]);\n    }\n    // Отсортировать по удельной стоимости item.v / item.w в порядке убывания\n    Array.Sort(items, (x, y) => (y.v / y.w).CompareTo(x.v / x.w));\n    // Циклический жадный выбор\n    double res = 0;\n    foreach (Item item in items) {\n        if (item.w <= cap) {\n            // Если оставшейся вместимости достаточно, положить в рюкзак текущий предмет целиком\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // Если оставшейся вместимости недостаточно, положить в рюкзак часть текущего предмета\n            res += (double)item.v / item.w * cap;\n            // Свободной вместимости больше не осталось, поэтому выйти из цикла\n            break;\n        }\n    }\n    return res;\n}\n
        fractional_knapsack.go
        /* Предмет */\ntype Item struct {\n    w int // Вес предмета\n    v int // Стоимость предмета\n}\n\n/* Дробный рюкзак: жадный алгоритм */\nfunc fractionalKnapsack(wgt []int, val []int, cap int) float64 {\n    // Создать список предметов с двумя свойствами: вес и стоимость\n    items := make([]Item, len(wgt))\n    for i := 0; i < len(wgt); i++ {\n        items[i] = Item{wgt[i], val[i]}\n    }\n    // Отсортировать по удельной стоимости item.v / item.w в порядке убывания\n    sort.Slice(items, func(i, j int) bool {\n        return float64(items[i].v)/float64(items[i].w) > float64(items[j].v)/float64(items[j].w)\n    })\n    // Циклический жадный выбор\n    res := 0.0\n    for _, item := range items {\n        if item.w <= cap {\n            // Если оставшейся вместимости достаточно, положить в рюкзак текущий предмет целиком\n            res += float64(item.v)\n            cap -= item.w\n        } else {\n            // Если оставшейся вместимости недостаточно, положить в рюкзак часть текущего предмета\n            res += float64(item.v) / float64(item.w) * float64(cap)\n            // Свободной вместимости больше не осталось, поэтому выйти из цикла\n            break\n        }\n    }\n    return res\n}\n
        fractional_knapsack.swift
        /* Предмет */\nclass Item {\n    var w: Int // Вес предмета\n    var v: Int // Стоимость предмета\n\n    init(w: Int, v: Int) {\n        self.w = w\n        self.v = v\n    }\n}\n\n/* Дробный рюкзак: жадный алгоритм */\nfunc fractionalKnapsack(wgt: [Int], val: [Int], cap: Int) -> Double {\n    // Создать список предметов с двумя свойствами: вес и стоимость\n    var items = zip(wgt, val).map { Item(w: $0, v: $1) }\n    // Отсортировать по удельной стоимости item.v / item.w в порядке убывания\n    items.sort { -(Double($0.v) / Double($0.w)) < -(Double($1.v) / Double($1.w)) }\n    // Циклический жадный выбор\n    var res = 0.0\n    var cap = cap\n    for item in items {\n        if item.w <= cap {\n            // Если оставшейся вместимости достаточно, положить в рюкзак текущий предмет целиком\n            res += Double(item.v)\n            cap -= item.w\n        } else {\n            // Если оставшейся вместимости недостаточно, положить в рюкзак часть текущего предмета\n            res += Double(item.v) / Double(item.w) * Double(cap)\n            // Свободной вместимости больше не осталось, поэтому выйти из цикла\n            break\n        }\n    }\n    return res\n}\n
        fractional_knapsack.js
        /* Предмет */\nclass Item {\n    constructor(w, v) {\n        this.w = w; // Вес предмета\n        this.v = v; // Стоимость предмета\n    }\n}\n\n/* Дробный рюкзак: жадный алгоритм */\nfunction fractionalKnapsack(wgt, val, cap) {\n    // Создать список предметов с двумя свойствами: вес и стоимость\n    const items = wgt.map((w, i) => new Item(w, val[i]));\n    // Отсортировать по удельной стоимости item.v / item.w в порядке убывания\n    items.sort((a, b) => b.v / b.w - a.v / a.w);\n    // Циклический жадный выбор\n    let res = 0;\n    for (const item of items) {\n        if (item.w <= cap) {\n            // Если оставшейся вместимости достаточно, положить в рюкзак текущий предмет целиком\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // Если оставшейся вместимости недостаточно, положить в рюкзак часть текущего предмета\n            res += (item.v / item.w) * cap;\n            // Свободной вместимости больше не осталось, поэтому выйти из цикла\n            break;\n        }\n    }\n    return res;\n}\n
        fractional_knapsack.ts
        /* Предмет */\nclass Item {\n    w: number; // Вес предмета\n    v: number; // Стоимость предмета\n\n    constructor(w: number, v: number) {\n        this.w = w;\n        this.v = v;\n    }\n}\n\n/* Дробный рюкзак: жадный алгоритм */\nfunction fractionalKnapsack(wgt: number[], val: number[], cap: number): number {\n    // Создать список предметов с двумя свойствами: вес и стоимость\n    const items: Item[] = wgt.map((w, i) => new Item(w, val[i]));\n    // Отсортировать по удельной стоимости item.v / item.w в порядке убывания\n    items.sort((a, b) => b.v / b.w - a.v / a.w);\n    // Циклический жадный выбор\n    let res = 0;\n    for (const item of items) {\n        if (item.w <= cap) {\n            // Если оставшейся вместимости достаточно, положить в рюкзак текущий предмет целиком\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // Если оставшейся вместимости недостаточно, положить в рюкзак часть текущего предмета\n            res += (item.v / item.w) * cap;\n            // Свободной вместимости больше не осталось, поэтому выйти из цикла\n            break;\n        }\n    }\n    return res;\n}\n
        fractional_knapsack.dart
        /* Предмет */\nclass Item {\n  int w; // Вес предмета\n  int v; // Стоимость предмета\n\n  Item(this.w, this.v);\n}\n\n/* Дробный рюкзак: жадный алгоритм */\ndouble fractionalKnapsack(List<int> wgt, List<int> val, int cap) {\n  // Создать список предметов с двумя свойствами: вес и стоимость\n  List<Item> items = List.generate(wgt.length, (i) => Item(wgt[i], val[i]));\n  // Отсортировать по удельной стоимости item.v / item.w в порядке убывания\n  items.sort((a, b) => (b.v / b.w).compareTo(a.v / a.w));\n  // Циклический жадный выбор\n  double res = 0;\n  for (Item item in items) {\n    if (item.w <= cap) {\n      // Если оставшейся вместимости достаточно, положить в рюкзак текущий предмет целиком\n      res += item.v;\n      cap -= item.w;\n    } else {\n      // Если оставшейся вместимости недостаточно, положить в рюкзак часть текущего предмета\n      res += item.v / item.w * cap;\n      // Свободной вместимости больше не осталось, поэтому выйти из цикла\n      break;\n    }\n  }\n  return res;\n}\n
        fractional_knapsack.rs
        /* Предмет */\nstruct Item {\n    w: i32, // Вес предмета\n    v: i32, // Стоимость предмета\n}\n\nimpl Item {\n    fn new(w: i32, v: i32) -> Self {\n        Self { w, v }\n    }\n}\n\n/* Дробный рюкзак: жадный алгоритм */\nfn fractional_knapsack(wgt: &[i32], val: &[i32], mut cap: i32) -> f64 {\n    // Создать список предметов с двумя свойствами: вес и стоимость\n    let mut items = wgt\n        .iter()\n        .zip(val.iter())\n        .map(|(&w, &v)| Item::new(w, v))\n        .collect::<Vec<Item>>();\n    // Отсортировать по удельной стоимости item.v / item.w в порядке убывания\n    items.sort_by(|a, b| {\n        (b.v as f64 / b.w as f64)\n            .partial_cmp(&(a.v as f64 / a.w as f64))\n            .unwrap()\n    });\n    // Циклический жадный выбор\n    let mut res = 0.0;\n    for item in &items {\n        if item.w <= cap {\n            // Если оставшейся вместимости достаточно, положить в рюкзак текущий предмет целиком\n            res += item.v as f64;\n            cap -= item.w;\n        } else {\n            // Если оставшейся вместимости недостаточно, положить в рюкзак часть текущего предмета\n            res += item.v as f64 / item.w as f64 * cap as f64;\n            // Свободной вместимости больше не осталось, поэтому выйти из цикла\n            break;\n        }\n    }\n    res\n}\n
        fractional_knapsack.c
        /* Предмет */\ntypedef struct {\n    int w; // Вес предмета\n    int v; // Стоимость предмета\n} Item;\n\n/* Дробный рюкзак: жадный алгоритм */\nfloat fractionalKnapsack(int wgt[], int val[], int itemCount, int cap) {\n    // Создать список предметов с двумя свойствами: вес и стоимость\n    Item *items = malloc(sizeof(Item) * itemCount);\n    for (int i = 0; i < itemCount; i++) {\n        items[i] = (Item){.w = wgt[i], .v = val[i]};\n    }\n    // Отсортировать по удельной стоимости item.v / item.w в порядке убывания\n    qsort(items, (size_t)itemCount, sizeof(Item), sortByValueDensity);\n    // Циклический жадный выбор\n    float res = 0.0;\n    for (int i = 0; i < itemCount; i++) {\n        if (items[i].w <= cap) {\n            // Если оставшейся вместимости достаточно, положить в рюкзак текущий предмет целиком\n            res += items[i].v;\n            cap -= items[i].w;\n        } else {\n            // Если оставшейся вместимости недостаточно, положить в рюкзак часть текущего предмета\n            res += (float)cap / items[i].w * items[i].v;\n            cap = 0;\n            break;\n        }\n    }\n    free(items);\n    return res;\n}\n
        fractional_knapsack.kt
        /* Предмет */\nclass Item(\n    val w: Int, // Предмет\n    val v: Int  // Стоимость предмета\n)\n\n/* Дробный рюкзак: жадный алгоритм */\nfun fractionalKnapsack(wgt: IntArray, _val: IntArray, c: Int): Double {\n    // Создать список предметов с двумя свойствами: вес и стоимость\n    var cap = c\n    val items = arrayOfNulls<Item>(wgt.size)\n    for (i in wgt.indices) {\n        items[i] = Item(wgt[i], _val[i])\n    }\n    // Отсортировать по удельной стоимости item.v / item.w в порядке убывания\n    items.sortBy { item: Item? -> -(item!!.v.toDouble() / item.w) }\n    // Циклический жадный выбор\n    var res = 0.0\n    for (item in items) {\n        if (item!!.w <= cap) {\n            // Если оставшейся вместимости достаточно, положить в рюкзак текущий предмет целиком\n            res += item.v\n            cap -= item.w\n        } else {\n            // Если оставшейся вместимости недостаточно, положить в рюкзак часть текущего предмета\n            res += item.v.toDouble() / item.w * cap\n            // Свободной вместимости больше не осталось, поэтому выйти из цикла\n            break\n        }\n    }\n    return res\n}\n
        fractional_knapsack.rb
        ### Предмет ###\nclass Item\n  attr_accessor :w # Вес предмета\n  attr_accessor :v # Стоимость предмета\n\n  def initialize(w, v)\n    @w = w\n    @v = v\n  end\nend\n\n### Дробный рюкзак: жадный алгоритм ###\ndef fractional_knapsack(wgt, val, cap)\n  # Создать список предметов с двумя свойствами: вес и стоимость\n  items = wgt.each_with_index.map { |w, i| Item.new(w, val[i]) }\n  # Отсортировать по удельной стоимости item.v / item.w в порядке убывания\n  items.sort! { |a, b| (b.v.to_f / b.w) <=> (a.v.to_f / a.w) }\n  # Циклический жадный выбор\n  res = 0\n  for item in items\n    if item.w <= cap\n      # Если оставшейся вместимости достаточно, положить в рюкзак текущий предмет целиком\n      res += item.v\n      cap -= item.w\n    else\n      # Если оставшейся вместимости недостаточно, положить в рюкзак часть текущего предмета\n      res += (item.v.to_f / item.w) * cap\n      # Свободной вместимости больше не осталось, поэтому выйти из цикла\n      break\n    end\n  end\n  res\nend\n
        Визуализация кода

        Во весь экран >

        Встроенный алгоритм сортировки обычно имеет временную сложность \\(O(n \\log n)\\), а пространственная сложность обычно равна \\(O(\\log n)\\) или \\(O(n)\\), в зависимости от конкретной реализации в языке программирования.

        Помимо сортировки, в худшем случае потребуется пройти весь список предметов, но это не меняет асимптотику, поэтому итоговая временная сложность равна \\(O(n \\log n)\\), где \\(n\\) - число предметов.

        Поскольку инициализируется список объектов Item, пространственная сложность равна \\(O(n)\\).

        ","path":["Глава 15. Жадность","15.2   Задача о дробном рюкзаке"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/#3","level":3,"title":"3.   Доказательство корректности","text":"

        Используем доказательство от противного. Предположим, что предмет \\(x\\) имеет наибольшую удельную ценность, некоторый алгоритм получил максимальную ценность res, но в найденном решении предмет \\(x\\) отсутствует.

        Теперь вынем из рюкзака произвольный предмет единичного веса и заменим его на предмет \\(x\\) того же веса. Поскольку предмет \\(x\\) имеет наибольшую удельную ценность, общая ценность после замены обязательно станет больше res. Это противоречит тому, что res является оптимальным решением, а значит оптимальное решение обязательно содержит предмет \\(x\\).

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

        Как показано на рисунке 15-6, если рассматривать вес предметов и их удельную ценность как горизонтальную и вертикальную оси двумерной диаграммы, то задачу о дробном рюкзаке можно интерпретировать как «поиск максимальной площади, ограниченной конечным отрезком по горизонтали». Эта аналогия помогает понять корректность жадной стратегии с геометрической точки зрения.

        Рисунок 15-6   Геометрическая интерпретация задачи о дробном рюкзаке

        ","path":["Глава 15. Жадность","15.2   Задача о дробном рюкзаке"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/","level":1,"title":"15.1   Жадный алгоритм","text":"

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

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

        • Динамическое программирование при получении текущего решения учитывает все предыдущие решения и использует ответы для прошлых подзадач, чтобы построить ответ для текущей подзадачи.
        • Жадный алгоритм не учитывает предыдущие решения, а просто движется вперед, каждый раз делая жадный выбор и постепенно сужая область задачи, пока она не будет решена.

        Чтобы лучше понять принцип работы жадного алгоритма, разберем его на примере задачи «размен монет». Эта задача уже встречалась в разделе «задача о полном рюкзаке», поэтому она наверняка вам знакома.

        Question

        Дано \\(n\\) видов монет. Номинал монеты \\(i\\) равен \\(coins[i - 1]\\), целевая сумма равна \\(amt\\), причем каждую монету можно брать неограниченное число раз. Требуется найти минимальное число монет, которыми можно набрать целевую сумму. Если набрать сумму невозможно, верните \\(-1\\).

        Жадная стратегия для этой задачи показана на рисунке 15-1. Для заданной целевой суммы мы жадно выбираем монету, которая не превышает ее и находится к ней ближе всего, и повторяем этот шаг, пока не получим нужную сумму.

        Рисунок 15-1   Жадная стратегия для задачи о размене монет

        Ниже приведен код реализации.

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change_greedy.py
        def coin_change_greedy(coins: list[int], amt: int) -> int:\n    \"\"\"Размен монет: жадный алгоритм\"\"\"\n    # Предположить, что список coins упорядочен\n    i = len(coins) - 1\n    count = 0\n    # Циклически выполнять жадный выбор, пока не останется суммы\n    while amt > 0:\n        # Найти монету, которая меньше остатка суммы и наиболее к нему близка\n        while i > 0 and coins[i] > amt:\n            i -= 1\n        # Выбрать coins[i]\n        amt -= coins[i]\n        count += 1\n    # Если допустимое решение не найдено, вернуть -1\n    return count if amt == 0 else -1\n
        coin_change_greedy.cpp
        /* Размен монет: жадный алгоритм */\nint coinChangeGreedy(vector<int> &coins, int amt) {\n    // Предположить, что список coins упорядочен\n    int i = coins.size() - 1;\n    int count = 0;\n    // Циклически выполнять жадный выбор, пока не останется суммы\n    while (amt > 0) {\n        // Найти монету, которая меньше остатка суммы и наиболее к нему близка\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // Выбрать coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // Если допустимое решение не найдено, вернуть -1\n    return amt == 0 ? count : -1;\n}\n
        coin_change_greedy.java
        /* Размен монет: жадный алгоритм */\nint coinChangeGreedy(int[] coins, int amt) {\n    // Предположить, что список coins упорядочен\n    int i = coins.length - 1;\n    int count = 0;\n    // Циклически выполнять жадный выбор, пока не останется суммы\n    while (amt > 0) {\n        // Найти монету, которая меньше остатка суммы и наиболее к нему близка\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // Выбрать coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // Если допустимое решение не найдено, вернуть -1\n    return amt == 0 ? count : -1;\n}\n
        coin_change_greedy.cs
        /* Размен монет: жадный алгоритм */\nint CoinChangeGreedy(int[] coins, int amt) {\n    // Предположить, что список coins упорядочен\n    int i = coins.Length - 1;\n    int count = 0;\n    // Циклически выполнять жадный выбор, пока не останется суммы\n    while (amt > 0) {\n        // Найти монету, которая меньше остатка суммы и наиболее к нему близка\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // Выбрать coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // Если допустимое решение не найдено, вернуть -1\n    return amt == 0 ? count : -1;\n}\n
        coin_change_greedy.go
        /* Размен монет: жадный алгоритм */\nfunc coinChangeGreedy(coins []int, amt int) int {\n    // Предположить, что список coins упорядочен\n    i := len(coins) - 1\n    count := 0\n    // Циклически выполнять жадный выбор, пока не останется суммы\n    for amt > 0 {\n        // Найти монету, которая меньше остатка суммы и наиболее к нему близка\n        for i > 0 && coins[i] > amt {\n            i--\n        }\n        // Выбрать coins[i]\n        amt -= coins[i]\n        count++\n    }\n    // Если допустимое решение не найдено, вернуть -1\n    if amt != 0 {\n        return -1\n    }\n    return count\n}\n
        coin_change_greedy.swift
        /* Размен монет: жадный алгоритм */\nfunc coinChangeGreedy(coins: [Int], amt: Int) -> Int {\n    // Предположить, что список coins упорядочен\n    var i = coins.count - 1\n    var count = 0\n    var amt = amt\n    // Циклически выполнять жадный выбор, пока не останется суммы\n    while amt > 0 {\n        // Найти монету, которая меньше остатка суммы и наиболее к нему близка\n        while i > 0 && coins[i] > amt {\n            i -= 1\n        }\n        // Выбрать coins[i]\n        amt -= coins[i]\n        count += 1\n    }\n    // Если допустимое решение не найдено, вернуть -1\n    return amt == 0 ? count : -1\n}\n
        coin_change_greedy.js
        /* Размен монет: жадный алгоритм */\nfunction coinChangeGreedy(coins, amt) {\n    // Предположить, что массив coins упорядочен\n    let i = coins.length - 1;\n    let count = 0;\n    // Циклически выполнять жадный выбор, пока не останется суммы\n    while (amt > 0) {\n        // Найти монету, которая меньше остатка суммы и наиболее к нему близка\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // Выбрать coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // Если допустимое решение не найдено, вернуть -1\n    return amt === 0 ? count : -1;\n}\n
        coin_change_greedy.ts
        /* Размен монет: жадный алгоритм */\nfunction coinChangeGreedy(coins: number[], amt: number): number {\n    // Предположить, что массив coins упорядочен\n    let i = coins.length - 1;\n    let count = 0;\n    // Циклически выполнять жадный выбор, пока не останется суммы\n    while (amt > 0) {\n        // Найти монету, которая меньше остатка суммы и наиболее к нему близка\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // Выбрать coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // Если допустимое решение не найдено, вернуть -1\n    return amt === 0 ? count : -1;\n}\n
        coin_change_greedy.dart
        /* Размен монет: жадный алгоритм */\nint coinChangeGreedy(List<int> coins, int amt) {\n  // Предположить, что список coins упорядочен\n  int i = coins.length - 1;\n  int count = 0;\n  // Циклически выполнять жадный выбор, пока не останется суммы\n  while (amt > 0) {\n    // Найти монету, которая меньше остатка суммы и наиболее к нему близка\n    while (i > 0 && coins[i] > amt) {\n      i--;\n    }\n    // Выбрать coins[i]\n    amt -= coins[i];\n    count++;\n  }\n  // Если допустимое решение не найдено, вернуть -1\n  return amt == 0 ? count : -1;\n}\n
        coin_change_greedy.rs
        /* Размен монет: жадный алгоритм */\nfn coin_change_greedy(coins: &[i32], mut amt: i32) -> i32 {\n    // Предположить, что список coins упорядочен\n    let mut i = coins.len() - 1;\n    let mut count = 0;\n    // Циклически выполнять жадный выбор, пока не останется суммы\n    while amt > 0 {\n        // Найти монету, которая меньше остатка суммы и наиболее к нему близка\n        while i > 0 && coins[i] > amt {\n            i -= 1;\n        }\n        // Выбрать coins[i]\n        amt -= coins[i];\n        count += 1;\n    }\n    // Если допустимое решение не найдено, вернуть -1\n    if amt == 0 {\n        count\n    } else {\n        -1\n    }\n}\n
        coin_change_greedy.c
        /* Размен монет: жадный алгоритм */\nint coinChangeGreedy(int *coins, int size, int amt) {\n    // Предположить, что список coins упорядочен\n    int i = size - 1;\n    int count = 0;\n    // Циклически выполнять жадный выбор, пока не останется суммы\n    while (amt > 0) {\n        // Найти монету, которая меньше остатка суммы и наиболее к нему близка\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // Выбрать coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // Если допустимое решение не найдено, вернуть -1\n    return amt == 0 ? count : -1;\n}\n
        coin_change_greedy.kt
        /* Размен монет: жадный алгоритм */\nfun coinChangeGreedy(coins: IntArray, amt: Int): Int {\n    // Предположить, что список coins упорядочен\n    var am = amt\n    var i = coins.size - 1\n    var count = 0\n    // Циклически выполнять жадный выбор, пока не останется суммы\n    while (am > 0) {\n        // Найти монету, которая меньше остатка суммы и наиболее к нему близка\n        while (i > 0 && coins[i] > am) {\n            i--\n        }\n        // Выбрать coins[i]\n        am -= coins[i]\n        count++\n    }\n    // Если допустимое решение не найдено, вернуть -1\n    return if (am == 0) count else -1\n}\n
        coin_change_greedy.rb
        ### Размен монет: жадный алгоритм ###\ndef coin_change_greedy(coins, amt)\n  # Предположить, что список coins упорядочен\n  i = coins.length - 1\n  count = 0\n  # Циклически выполнять жадный выбор, пока не останется суммы\n  while amt > 0\n    # Найти монету, которая меньше остатка суммы и наиболее к нему близка\n    while i > 0 && coins[i] > amt\n      i -= 1\n    end\n    # Выбрать coins[i]\n    amt -= coins[i]\n    count += 1\n  end\n  # Если допустимое решение не найдено, вернуть -1\n  amt == 0 ? count : -1\nend\n
        Визуализация кода

        Во весь экран >

        У вас может невольно вырваться: «Эврика!» Жадный алгоритм решает задачу размена монет всего примерно десятью строками кода.

        ","path":["Глава 15. Жадность","15.1   Жадный алгоритм"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1511","level":2,"title":"15.1.1   Преимущества и ограничения жадного алгоритма","text":"

        Жадный алгоритм не только прост в реализации, но и обычно очень эффективен. В приведенном выше коде обозначим минимальный номинал монеты через \\(\\min(coins)\\), тогда жадный выбор выполняется не более чем \\(amt / \\min(coins)\\) раз, а временная сложность равна \\(O(amt / \\min(coins))\\). Это на порядок меньше, чем временная сложность решения через динамическое программирование \\(O(n \\times amt)\\).

        Однако для некоторых наборов номиналов монет жадный алгоритм не может найти оптимальный ответ. На рисунке 15-2 показаны два примера.

        • Положительный пример \\(coins = [1, 5, 10, 20, 50, 100]\\): для такого набора монет при любом \\(amt\\) жадный алгоритм находит оптимальное решение.
        • Отрицательный пример \\(coins = [1, 20, 50]\\): пусть \\(amt = 60\\). Жадный алгоритм найдет только комбинацию \\(50 + 1 \\times 10\\), то есть всего \\(11\\) монет, тогда как динамическое программирование находит оптимум \\(20 + 20 + 20\\), где требуется лишь \\(3\\) монеты.
        • Отрицательный пример \\(coins = [1, 49, 50]\\): пусть \\(amt = 98\\). Жадный алгоритм найдет только комбинацию \\(50 + 1 \\times 48\\), то есть всего \\(49\\) монет, тогда как динамическое программирование находит оптимум \\(49 + 49\\), где требуется лишь \\(2\\) монеты.

        Рисунок 15-2   Примеры, где жадный алгоритм не находит оптимального решения

        Иными словами, в задаче о размене монет жадный алгоритм не гарантирует нахождение глобально оптимального решения и иногда может приводить к очень плохому ответу. Для этой задачи больше подходит динамическое программирование.

        В общем случае жадный алгоритм применим в двух следующих ситуациях.

        1. Можно гарантировать нахождение оптимального решения: в таком случае жадный алгоритм часто является лучшим выбором, поскольку обычно он эффективнее, чем поиск с возвратом и динамическое программирование.
        2. Можно найти приближенно оптимальное решение: в таком случае жадный алгоритм тоже полезен. Для многих сложных задач поиск глобального оптимума очень труден, и возможность быстро найти субоптимальный ответ уже весьма ценна.
        ","path":["Глава 15. Жадность","15.1   Жадный алгоритм"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1512","level":2,"title":"15.1.2   Свойства жадного алгоритма","text":"

        Тогда возникает вопрос: какие задачи подходят для решения жадным алгоритмом? Или, другими словами, в каких случаях жадный алгоритм может гарантировать оптимальный ответ?

        По сравнению с динамическим программированием условия применения жадного алгоритма более строгие. В основном нас интересуют два свойства задачи.

        • Свойство жадного выбора: только когда локально оптимальный выбор всегда может привести к глобально оптимальному решению, жадный алгоритм способен гарантировать оптимум.
        • Оптимальная подструктура: оптимальное решение исходной задачи содержит оптимальные решения подзадач.

        Оптимальная подструктура уже обсуждалась в главе «Динамическое программирование», поэтому здесь не будем повторяться. Стоит отметить, что у некоторых задач оптимальная подструктура не столь очевидна, но их все равно можно решать жадным алгоритмом.

        Основное внимание мы уделяем тому, как определить свойство жадного выбора. Хотя формулировка выглядит довольно простой, на практике для многих задач доказать свойство жадного выбора совсем не легко.

        Например, в задаче о размене монет легко привести контрпример и опровергнуть свойство жадного выбора, но вот доказать его истинность намного сложнее. Если спросить: для каких наборов монет можно использовать жадный алгоритм? - обычно удается дать лишь интуитивный или примерный ответ, а не строгое математическое доказательство.

        Quote

        Существует статья, в которой приводится алгоритм со временной сложностью \\(O(n^3)\\) для определения того, можно ли с помощью жадного алгоритма находить оптимальный размен для любой суммы в заданной системе монет.

        Pearson, D. A polynomial-time algorithm for the change-making problem[J]. Operations Research Letters, 2005, 33(3): 231-234.

        ","path":["Глава 15. Жадность","15.1   Жадный алгоритм"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1513","level":2,"title":"15.1.3   Этапы решения задач жадным алгоритмом","text":"

        Процесс решения жадной задачи в общем виде можно разбить на три шага.

        1. Анализ задачи: разобраться в свойствах задачи, включая определение состояний, целевой функции и ограничений. Этот этап присутствует и в поиске с возвратом, и в динамическом программировании.
        2. Определение жадной стратегии: определить, какой жадный выбор следует делать на каждом шаге. Эта стратегия должна уменьшать размер задачи на каждом этапе и в итоге привести к решению всей задачи.
        3. Доказательство корректности: обычно требуется доказать, что задача обладает свойством жадного выбора и оптимальной подструктурой. На этом этапе может понадобиться математическое доказательство, например индукция или доказательство от противного.

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

        • Жадные стратегии для разных задач сильно различаются. Для многих задач стратегия довольно очевидна, и до нее можно дойти за счет общих рассуждений и нескольких проб. Но в более сложных задачах жадная стратегия может быть очень скрытой, и тут уже многое зависит от опыта решения задач и алгоритмической подготовки.
        • Некоторые жадные стратегии выглядят убедительно, но оказываются обманчивыми. Бывает, что мы с уверенностью придумали жадную стратегию, написали код и отправили его на проверку, а часть тестов не проходит. Причина в том, что спроектированная стратегия лишь «частично верна», и описанная выше задача о размене монет - типичный пример.

        Чтобы гарантировать корректность, нужно дать строгое математическое доказательство жадной стратегии, обычно с использованием доказательства от противного или математической индукции.

        Однако и доказательство корректности может оказаться непростой задачей. Если идей нет, мы обычно начинаем отлаживать код на тестовых примерах, постепенно меняя и проверяя жадную стратегию.

        ","path":["Глава 15. Жадность","15.1   Жадный алгоритм"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1514","level":2,"title":"15.1.4   Типичные задачи для жадного алгоритма","text":"

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

        • Задача о размене монет: при некоторых системах монет жадный алгоритм всегда дает оптимальный ответ.
        • Задача о расписании интервалов: пусть есть несколько задач, каждая выполняется в некотором временном интервале, и требуется завершить как можно больше задач. Если каждый раз выбирать задачу с самым ранним временем окончания, то жадный алгоритм дает оптимальный ответ.
        • Задача о дробном рюкзаке: дана группа предметов и грузоподъемность. Требуется выбрать предметы так, чтобы их общий вес не превышал ограничение, а общая ценность была максимальной. Если каждый раз выбирать предмет с наилучшим отношением стоимости к весу, то в некоторых случаях жадный алгоритм дает оптимальный ответ.
        • Задача о покупке и продаже акций: дана история цен акции. Можно совершать несколько сделок, но если акция уже куплена, то до продажи покупать снова нельзя. Цель - получить максимальную прибыль.
        • Код Хаффмана: это жадный алгоритм для сжатия данных без потерь. Построив дерево Хаффмана и каждый раз объединяя два узла с наименьшей частотой, мы получаем дерево с минимальной взвешенной длиной пути, то есть минимальной длиной кодирования.
        • Алгоритм Дейкстры: это жадный алгоритм решения задачи о кратчайших путях от заданной исходной вершины до всех остальных вершин.
        ","path":["Глава 15. Жадность","15.1   Жадный алгоритм"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/","level":1,"title":"15.3   Задача о максимальной вместимости","text":"

        Question

        Дан массив \\(ht\\), где каждый элемент обозначает высоту вертикальной перегородки. Любые две перегородки в массиве вместе с пространством между ними образуют контейнер.

        Вместимость контейнера равна произведению высоты и ширины (площади), где высота определяется более короткой перегородкой, а ширина - разностью индексов двух перегородок в массиве.

        Требуется выбрать две перегородки так, чтобы образованный ими контейнер имел максимальную вместимость. Пример показан на рисунке 15-7.

        Рисунок 15-7   Пример данных для задачи о максимальной вместимости

        Контейнер образуется произвольными двумя перегородками, поэтому состоянием задачи служит пара индексов этих перегородок, обозначим ее как \\([i, j]\\).

        Согласно условию, вместимость равна произведению высоты на ширину, где высота определяется короткой перегородкой, а ширина - разностью индексов двух перегородок. Обозначим вместимость через \\(cap[i, j]\\), тогда формула принимает вид:

        \\[ cap[i, j] = \\min(ht[i], ht[j]) \\times (j - i) \\]

        Пусть длина массива равна \\(n\\). Тогда число пар перегородок, то есть общее число состояний, равно \\(C_n^2 = \\frac{n(n - 1)}{2}\\). Самый прямолинейный подход - перебрать все состояния, после чего найти максимальную вместимость. Его временная сложность равна \\(O(n^2)\\).

        ","path":["Глава 15. Жадность","15.3   Задача о максимальной вместимости"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/#1","level":3,"title":"1.   Определение жадной стратегии","text":"

        У этой задачи есть и более эффективное решение. Как показано на рисунке 15-8, рассмотрим состояние \\([i, j]\\), где индексы удовлетворяют \\(i < j\\), а высоты - условию \\(ht[i] < ht[j]\\), то есть \\(i\\) - короткая перегородка, а \\(j\\) - длинная.

        Рисунок 15-8   Начальное состояние

        Как показано на рисунке 15-9, если в этот момент сдвинуть длинную перегородку \\(j\\) ближе к короткой перегородке \\(i\\), то вместимость обязательно уменьшится.

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

        Рисунок 15-9   Состояние после перемещения длинной перегородки внутрь

        Рассуждая в обратную сторону, только сдвигая короткую перегородку \\(i\\) внутрь, мы можем получить шанс увеличить вместимость. Хотя ширина при этом обязательно уменьшится, высота может возрасти (если после перемещения короткая перегородка \\(i\\) станет выше). Например, на рисунке 15-10 после перемещения короткой перегородки площадь увеличивается.

        Рисунок 15-10   Состояние после перемещения короткой перегородки внутрь

        Отсюда и выводится жадная стратегия для этой задачи: инициализировать два указателя по краям контейнера и на каждом шаге сдвигать внутрь указатель, соответствующий короткой перегородке, пока указатели не встретятся.

        На рисунке 15-11 показан процесс выполнения этой жадной стратегии.

        1. В начальном состоянии указатели \\(i\\) и \\(j\\) стоят на двух концах массива.
        2. Вычислить вместимость текущего состояния \\(cap[i, j]\\) и обновить максимальную вместимость.
        3. Сравнить высоты перегородок \\(i\\) и \\(j\\), после чего сдвинуть короткую перегородку на одну позицию внутрь.
        4. Повторять шаги 2. и 3. до тех пор, пока \\(i\\) и \\(j\\) не встретятся.
        <1><2><3><4><5><6><7><8><9>

        Рисунок 15-11   Жадный процесс решения задачи о максимальной вместимости

        ","path":["Глава 15. Жадность","15.3   Задача о максимальной вместимости"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/#2","level":3,"title":"2.   Код реализации","text":"

        Цикл в коде выполняется не более \\(n\\) раз, поэтому временная сложность равна \\(O(n)\\).

        Переменные \\(i\\), \\(j\\), \\(res\\) используют дополнительную память постоянного размера, поэтому пространственная сложность равна \\(O(1)\\).

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby max_capacity.py
        def max_capacity(ht: list[int]) -> int:\n    \"\"\"Максимальная вместимость: жадный алгоритм\"\"\"\n    # Инициализировать i и j так, чтобы они располагались по двум концам массива\n    i, j = 0, len(ht) - 1\n    # Начальная максимальная вместимость равна 0\n    res = 0\n    # Выполнять жадный выбор в цикле, пока две доски не встретятся\n    while i < j:\n        # Обновить максимальную вместимость\n        cap = min(ht[i], ht[j]) * (j - i)\n        res = max(res, cap)\n        # Сдвигать внутрь более короткую сторону\n        if ht[i] < ht[j]:\n            i += 1\n        else:\n            j -= 1\n    return res\n
        max_capacity.cpp
        /* Максимальная вместимость: жадный алгоритм */\nint maxCapacity(vector<int> &ht) {\n    // Инициализировать i и j так, чтобы они располагались по двум концам массива\n    int i = 0, j = ht.size() - 1;\n    // Начальная максимальная вместимость равна 0\n    int res = 0;\n    // Выполнять жадный выбор в цикле, пока две доски не встретятся\n    while (i < j) {\n        // Обновить максимальную вместимость\n        int cap = min(ht[i], ht[j]) * (j - i);\n        res = max(res, cap);\n        // Сдвигать внутрь более короткую сторону\n        if (ht[i] < ht[j]) {\n            i++;\n        } else {\n            j--;\n        }\n    }\n    return res;\n}\n
        max_capacity.java
        /* Максимальная вместимость: жадный алгоритм */\nint maxCapacity(int[] ht) {\n    // Инициализировать i и j так, чтобы они располагались по двум концам массива\n    int i = 0, j = ht.length - 1;\n    // Начальная максимальная вместимость равна 0\n    int res = 0;\n    // Выполнять жадный выбор в цикле, пока две доски не встретятся\n    while (i < j) {\n        // Обновить максимальную вместимость\n        int cap = Math.min(ht[i], ht[j]) * (j - i);\n        res = Math.max(res, cap);\n        // Сдвигать внутрь более короткую сторону\n        if (ht[i] < ht[j]) {\n            i++;\n        } else {\n            j--;\n        }\n    }\n    return res;\n}\n
        max_capacity.cs
        /* Максимальная вместимость: жадный алгоритм */\nint MaxCapacity(int[] ht) {\n    // Инициализировать i и j так, чтобы они располагались по двум концам массива\n    int i = 0, j = ht.Length - 1;\n    // Начальная максимальная вместимость равна 0\n    int res = 0;\n    // Выполнять жадный выбор в цикле, пока две доски не встретятся\n    while (i < j) {\n        // Обновить максимальную вместимость\n        int cap = Math.Min(ht[i], ht[j]) * (j - i);\n        res = Math.Max(res, cap);\n        // Сдвигать внутрь более короткую сторону\n        if (ht[i] < ht[j]) {\n            i++;\n        } else {\n            j--;\n        }\n    }\n    return res;\n}\n
        max_capacity.go
        /* Максимальная вместимость: жадный алгоритм */\nfunc maxCapacity(ht []int) int {\n    // Инициализировать i и j так, чтобы они располагались по двум концам массива\n    i, j := 0, len(ht)-1\n    // Начальная максимальная вместимость равна 0\n    res := 0\n    // Выполнять жадный выбор в цикле, пока две доски не встретятся\n    for i < j {\n        // Обновить максимальную вместимость\n        capacity := int(math.Min(float64(ht[i]), float64(ht[j]))) * (j - i)\n        res = int(math.Max(float64(res), float64(capacity)))\n        // Сдвигать внутрь более короткую сторону\n        if ht[i] < ht[j] {\n            i++\n        } else {\n            j--\n        }\n    }\n    return res\n}\n
        max_capacity.swift
        /* Максимальная вместимость: жадный алгоритм */\nfunc maxCapacity(ht: [Int]) -> Int {\n    // Инициализировать i и j так, чтобы они располагались по двум концам массива\n    var i = ht.startIndex, j = ht.endIndex - 1\n    // Начальная максимальная вместимость равна 0\n    var res = 0\n    // Выполнять жадный выбор в цикле, пока две доски не встретятся\n    while i < j {\n        // Обновить максимальную вместимость\n        let cap = min(ht[i], ht[j]) * (j - i)\n        res = max(res, cap)\n        // Сдвигать внутрь более короткую сторону\n        if ht[i] < ht[j] {\n            i += 1\n        } else {\n            j -= 1\n        }\n    }\n    return res\n}\n
        max_capacity.js
        /* Максимальная вместимость: жадный алгоритм */\nfunction maxCapacity(ht) {\n    // Инициализировать i и j так, чтобы они располагались по двум концам массива\n    let i = 0,\n        j = ht.length - 1;\n    // Начальная максимальная вместимость равна 0\n    let res = 0;\n    // Выполнять жадный выбор в цикле, пока две доски не встретятся\n    while (i < j) {\n        // Обновить максимальную вместимость\n        const cap = Math.min(ht[i], ht[j]) * (j - i);\n        res = Math.max(res, cap);\n        // Сдвигать внутрь более короткую сторону\n        if (ht[i] < ht[j]) {\n            i += 1;\n        } else {\n            j -= 1;\n        }\n    }\n    return res;\n}\n
        max_capacity.ts
        /* Максимальная вместимость: жадный алгоритм */\nfunction maxCapacity(ht: number[]): number {\n    // Инициализировать i и j так, чтобы они располагались по двум концам массива\n    let i = 0,\n        j = ht.length - 1;\n    // Начальная максимальная вместимость равна 0\n    let res = 0;\n    // Выполнять жадный выбор в цикле, пока две доски не встретятся\n    while (i < j) {\n        // Обновить максимальную вместимость\n        const cap: number = Math.min(ht[i], ht[j]) * (j - i);\n        res = Math.max(res, cap);\n        // Сдвигать внутрь более короткую сторону\n        if (ht[i] < ht[j]) {\n            i += 1;\n        } else {\n            j -= 1;\n        }\n    }\n    return res;\n}\n
        max_capacity.dart
        /* Максимальная вместимость: жадный алгоритм */\nint maxCapacity(List<int> ht) {\n  // Инициализировать i и j так, чтобы они располагались по двум концам массива\n  int i = 0, j = ht.length - 1;\n  // Начальная максимальная вместимость равна 0\n  int res = 0;\n  // Выполнять жадный выбор в цикле, пока две доски не встретятся\n  while (i < j) {\n    // Обновить максимальную вместимость\n    int cap = min(ht[i], ht[j]) * (j - i);\n    res = max(res, cap);\n    // Сдвигать внутрь более короткую сторону\n    if (ht[i] < ht[j]) {\n      i++;\n    } else {\n      j--;\n    }\n  }\n  return res;\n}\n
        max_capacity.rs
        /* Максимальная вместимость: жадный алгоритм */\nfn max_capacity(ht: &[i32]) -> i32 {\n    // Инициализировать i и j так, чтобы они располагались по двум концам массива\n    let mut i = 0;\n    let mut j = ht.len() - 1;\n    // Начальная максимальная вместимость равна 0\n    let mut res = 0;\n    // Выполнять жадный выбор в цикле, пока две доски не встретятся\n    while i < j {\n        // Обновить максимальную вместимость\n        let cap = std::cmp::min(ht[i], ht[j]) * (j - i) as i32;\n        res = std::cmp::max(res, cap);\n        // Сдвигать внутрь более короткую сторону\n        if ht[i] < ht[j] {\n            i += 1;\n        } else {\n            j -= 1;\n        }\n    }\n    res\n}\n
        max_capacity.c
        /* Максимальная вместимость: жадный алгоритм */\nint maxCapacity(int ht[], int htLength) {\n    // Инициализировать i и j так, чтобы они располагались по двум концам массива\n    int i = 0;\n    int j = htLength - 1;\n    // Начальная максимальная вместимость равна 0\n    int res = 0;\n    // Выполнять жадный выбор в цикле, пока две доски не встретятся\n    while (i < j) {\n        // Обновить максимальную вместимость\n        int capacity = myMin(ht[i], ht[j]) * (j - i);\n        res = myMax(res, capacity);\n        // Сдвигать внутрь более короткую сторону\n        if (ht[i] < ht[j]) {\n            i++;\n        } else {\n            j--;\n        }\n    }\n    return res;\n}\n
        max_capacity.kt
        /* Максимальная вместимость: жадный алгоритм */\nfun maxCapacity(ht: IntArray): Int {\n    // Инициализировать i и j так, чтобы они располагались по двум концам массива\n    var i = 0\n    var j = ht.size - 1\n    // Начальная максимальная вместимость равна 0\n    var res = 0\n    // Выполнять жадный выбор в цикле, пока две доски не встретятся\n    while (i < j) {\n        // Обновить максимальную вместимость\n        val cap = min(ht[i], ht[j]) * (j - i)\n        res = max(res, cap)\n        // Сдвигать внутрь более короткую сторону\n        if (ht[i] < ht[j]) {\n            i++\n        } else {\n            j--\n        }\n    }\n    return res\n}\n
        max_capacity.rb
        ### Максимальная вместимость: жадный алгоритм ###\ndef max_capacity(ht)\n  # Инициализировать i и j так, чтобы они располагались по двум концам массива\n  i, j = 0, ht.length - 1\n  # Начальная максимальная вместимость равна 0\n  res = 0\n\n  # Выполнять жадный выбор в цикле, пока две доски не встретятся\n  while i < j\n    # Обновить максимальную вместимость\n    cap = [ht[i], ht[j]].min * (j - i)\n    res = [res, cap].max\n    # Сдвигать внутрь более короткую сторону\n    if ht[i] < ht[j]\n      i += 1\n    else\n      j -= 1\n    end\n  end\n\n  res\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 15. Жадность","15.3   Задача о максимальной вместимости"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/#3","level":3,"title":"3.   Доказательство корректности","text":"

        Жадный алгоритм быстрее полного перебора именно потому, что каждый жадный шаг «пропускает» часть состояний.

        Например, в состоянии \\(cap[i, j]\\) перегородка \\(i\\) является короткой, а \\(j\\) - длинной. Если жадно сдвинуть короткую перегородку \\(i\\) на одну позицию внутрь, то состояния, показанные на рисунке 15-12, будут «пропущены». Это означает, что позже мы уже не сможем проверить вместимость этих состояний.

        \\[ cap[i, i+1], cap[i, i+2], \\dots, cap[i, j-2], cap[i, j-1] \\]

        Рисунок 15-12   Состояния, пропущенные из-за смещения короткой перегородки

        Нетрудно заметить, что эти пропущенные состояния на самом деле и есть все состояния, в которых длинная перегородка \\(j\\) сдвигается внутрь. Ранее мы уже доказали, что перемещение длинной перегородки внутрь обязательно уменьшает вместимость. Иными словами, пропущенные состояния не могут быть оптимальным решением, поэтому их пропуск не приводит к потере оптимума.

        Приведенный анализ показывает, что операция перемещения короткой перегородки является «безопасной», а жадная стратегия действительно корректна.

        ","path":["Глава 15. Жадность","15.3   Задача о максимальной вместимости"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/","level":1,"title":"15.4   Задача о максимальном произведении разбиения","text":"

        Question

        Дан положительный целый \\(n\\). Требуется разложить его в сумму как минимум двух положительных целых чисел и найти максимально возможное произведение всех полученных чисел, как показано на рисунке 15-13.

        Рисунок 15-13   Определение задачи о максимальном произведении разбиения

        Предположим, что мы разбили \\(n\\) на \\(m\\) целочисленных множителей, где \\(i\\)-й множитель обозначим через \\(n_i\\), то есть

        \\[ n = \\sum_{i=1}^{m}n_i \\]

        Цель задачи - найти максимальное произведение всех целочисленных множителей, то есть

        \\[ \\max(\\prod_{i=1}^{m}n_i) \\]

        Нужно понять: каким должно быть число частей \\(m\\) и какими должны быть значения каждого \\(n_i\\)?

        ","path":["Глава 15. Жадность","15.4   Задача о максимальном произведении разбиения"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/#1","level":3,"title":"1.   Определение жадной стратегии","text":"

        Из опыта известно, что произведение двух целых чисел часто больше их суммы. Предположим, что мы выделяем из \\(n\\) множитель \\(2\\), тогда произведение равно \\(2(n-2)\\). Сравним это выражение с \\(n\\):

        \\[ \\begin{aligned} 2(n-2) & \\geq n \\newline 2n - n - 4 & \\geq 0 \\newline n & \\geq 4 \\end{aligned} \\]

        Как показано на рисунке 15-14, когда \\(n \\geq 4\\), выделение множителя \\(2\\) увеличивает произведение. Это означает, что все целые числа, большие либо равные \\(4\\), следует продолжать разбивать.

        Жадная стратегия 1: если в схеме разбиения присутствует множитель \\(\\geq 4\\), то его нужно дальше разбивать. В конечной схеме разбиения должны остаться только множители \\(1\\), \\(2\\), \\(3\\).

        Рисунок 15-14   Разбиение увеличивает произведение

        Теперь подумаем, какой множитель является наилучшим. Среди \\(1\\), \\(2\\), \\(3\\) очевидно худшим является \\(1\\), потому что всегда выполняется \\(1 \\times (n-1) < n\\), то есть выделение \\(1\\) уменьшает произведение.

        Как показано на рисунке 15-15, при \\(n = 6\\) имеем \\(3 \\times 3 > 2 \\times 2 \\times 2\\). Это означает, что выделять \\(3\\) выгоднее, чем выделять \\(2\\).

        Жадная стратегия 2: в схеме разбиения должно быть не более двух множителей \\(2\\). Потому что три двойки всегда можно заменить двумя тройками и получить большее произведение.

        Рисунок 15-15   Оптимальные множители разбиения

        Итак, получаем следующую жадную стратегию.

        1. Для заданного целого \\(n\\) непрерывно выделять из него множитель \\(3\\), пока остаток не станет равным \\(0\\), \\(1\\) или \\(2\\).
        2. Если остаток равен \\(0\\), это означает, что \\(n\\) кратно \\(3\\), и больше ничего делать не нужно.
        3. Если остаток равен \\(2\\), дальнейшее разбиение не требуется, его нужно сохранить.
        4. Если остаток равен \\(1\\), то поскольку \\(2 \\times 2 > 1 \\times 3\\), последний множитель \\(3\\) следует заменить на \\(2\\).
        ","path":["Глава 15. Жадность","15.4   Задача о максимальном произведении разбиения"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/#2","level":3,"title":"2.   Код реализации","text":"

        Как показано на рисунке 15-16, нам не нужен цикл, чтобы выполнять разбиение числа. Можно использовать целочисленное деление, чтобы получить число троек \\(a\\), и операцию взятия остатка, чтобы получить остаток \\(b\\). Тогда имеем:

        \\[ n = 3 a + b \\]

        Обратите внимание, что для граничного случая \\(n \\leq 3\\) необходимо выделить множитель \\(1\\), и тогда произведение равно \\(1 \\times (n - 1)\\).

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby max_product_cutting.py
        def max_product_cutting(n: int) -> int:\n    \"\"\"Максимальное произведение разрезания: жадный алгоритм\"\"\"\n    # Когда n <= 3, обязательно нужно выделить одну 1\n    if n <= 3:\n        return 1 * (n - 1)\n    # Жадно выделить множители 3, где a — число троек, а b — остаток\n    a, b = n // 3, n % 3\n    if b == 1:\n        # Если остаток равен 1, преобразовать одну пару 1 * 3 в 2 * 2\n        return int(math.pow(3, a - 1)) * 2 * 2\n    if b == 2:\n        # Если остаток равен 2, ничего не делать\n        return int(math.pow(3, a)) * 2\n    # Если остаток равен 0, ничего не делать\n    return int(math.pow(3, a))\n
        max_product_cutting.cpp
        /* Максимальное произведение разрезания: жадный алгоритм */\nint maxProductCutting(int n) {\n    // Когда n <= 3, обязательно нужно выделить одну 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // Жадно выделить множители 3, где a — число троек, а b — остаток\n    int a = n / 3;\n    int b = n % 3;\n    if (b == 1) {\n        // Если остаток равен 1, преобразовать одну пару 1 * 3 в 2 * 2\n        return (int)pow(3, a - 1) * 2 * 2;\n    }\n    if (b == 2) {\n        // Если остаток равен 2, ничего не делать\n        return (int)pow(3, a) * 2;\n    }\n    // Если остаток равен 0, ничего не делать\n    return (int)pow(3, a);\n}\n
        max_product_cutting.java
        /* Максимальное произведение разрезания: жадный алгоритм */\nint maxProductCutting(int n) {\n    // Когда n <= 3, обязательно нужно выделить одну 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // Жадно выделить множители 3, где a — число троек, а b — остаток\n    int a = n / 3;\n    int b = n % 3;\n    if (b == 1) {\n        // Если остаток равен 1, преобразовать одну пару 1 * 3 в 2 * 2\n        return (int) Math.pow(3, a - 1) * 2 * 2;\n    }\n    if (b == 2) {\n        // Если остаток равен 2, ничего не делать\n        return (int) Math.pow(3, a) * 2;\n    }\n    // Если остаток равен 0, ничего не делать\n    return (int) Math.pow(3, a);\n}\n
        max_product_cutting.cs
        /* Максимальное произведение разрезания: жадный алгоритм */\nint MaxProductCutting(int n) {\n    // Когда n <= 3, обязательно нужно выделить одну 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // Жадно выделить множители 3, где a — число троек, а b — остаток\n    int a = n / 3;\n    int b = n % 3;\n    if (b == 1) {\n        // Если остаток равен 1, преобразовать одну пару 1 * 3 в 2 * 2\n        return (int)Math.Pow(3, a - 1) * 2 * 2;\n    }\n    if (b == 2) {\n        // Если остаток равен 2, ничего не делать\n        return (int)Math.Pow(3, a) * 2;\n    }\n    // Если остаток равен 0, ничего не делать\n    return (int)Math.Pow(3, a);\n}\n
        max_product_cutting.go
        /* Максимальное произведение разрезания: жадный алгоритм */\nfunc maxProductCutting(n int) int {\n    // Когда n <= 3, обязательно нужно выделить одну 1\n    if n <= 3 {\n        return 1 * (n - 1)\n    }\n    // Жадно выделить множители 3, где a — число троек, а b — остаток\n    a := n / 3\n    b := n % 3\n    if b == 1 {\n        // Если остаток равен 1, преобразовать одну пару 1 * 3 в 2 * 2\n        return int(math.Pow(3, float64(a-1))) * 2 * 2\n    }\n    if b == 2 {\n        // Если остаток равен 2, ничего не делать\n        return int(math.Pow(3, float64(a))) * 2\n    }\n    // Если остаток равен 0, ничего не делать\n    return int(math.Pow(3, float64(a)))\n}\n
        max_product_cutting.swift
        /* Максимальное произведение разрезания: жадный алгоритм */\nfunc maxProductCutting(n: Int) -> Int {\n    // Когда n <= 3, обязательно нужно выделить одну 1\n    if n <= 3 {\n        return 1 * (n - 1)\n    }\n    // Жадно выделить множители 3, где a — число троек, а b — остаток\n    let a = n / 3\n    let b = n % 3\n    if b == 1 {\n        // Если остаток равен 1, преобразовать одну пару 1 * 3 в 2 * 2\n        return pow(3, a - 1) * 2 * 2\n    }\n    if b == 2 {\n        // Если остаток равен 2, ничего не делать\n        return pow(3, a) * 2\n    }\n    // Если остаток равен 0, ничего не делать\n    return pow(3, a)\n}\n
        max_product_cutting.js
        /* Максимальное произведение разрезания: жадный алгоритм */\nfunction maxProductCutting(n) {\n    // Когда n <= 3, обязательно нужно выделить одну 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // Жадно выделить множители 3, где a — число троек, а b — остаток\n    let a = Math.floor(n / 3);\n    let b = n % 3;\n    if (b === 1) {\n        // Если остаток равен 1, преобразовать одну пару 1 * 3 в 2 * 2\n        return Math.pow(3, a - 1) * 2 * 2;\n    }\n    if (b === 2) {\n        // Если остаток равен 2, ничего не делать\n        return Math.pow(3, a) * 2;\n    }\n    // Если остаток равен 0, ничего не делать\n    return Math.pow(3, a);\n}\n
        max_product_cutting.ts
        /* Максимальное произведение разрезания: жадный алгоритм */\nfunction maxProductCutting(n: number): number {\n    // Когда n <= 3, обязательно нужно выделить одну 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // Жадно выделить множители 3, где a — число троек, а b — остаток\n    let a: number = Math.floor(n / 3);\n    let b: number = n % 3;\n    if (b === 1) {\n        // Если остаток равен 1, преобразовать одну пару 1 * 3 в 2 * 2\n        return Math.pow(3, a - 1) * 2 * 2;\n    }\n    if (b === 2) {\n        // Если остаток равен 2, ничего не делать\n        return Math.pow(3, a) * 2;\n    }\n    // Если остаток равен 0, ничего не делать\n    return Math.pow(3, a);\n}\n
        max_product_cutting.dart
        /* Максимальное произведение разрезания: жадный алгоритм */\nint maxProductCutting(int n) {\n  // Когда n <= 3, обязательно нужно выделить одну 1\n  if (n <= 3) {\n    return 1 * (n - 1);\n  }\n  // Жадно выделить множители 3, где a — число троек, а b — остаток\n  int a = n ~/ 3;\n  int b = n % 3;\n  if (b == 1) {\n    // Если остаток равен 1, преобразовать одну пару 1 * 3 в 2 * 2\n    return (pow(3, a - 1) * 2 * 2).toInt();\n  }\n  if (b == 2) {\n    // Если остаток равен 2, ничего не делать\n    return (pow(3, a) * 2).toInt();\n  }\n  // Если остаток равен 0, ничего не делать\n  return pow(3, a).toInt();\n}\n
        max_product_cutting.rs
        /* Максимальное произведение разрезания: жадный алгоритм */\nfn max_product_cutting(n: i32) -> i32 {\n    // Когда n <= 3, обязательно нужно выделить одну 1\n    if n <= 3 {\n        return 1 * (n - 1);\n    }\n    // Жадно выделить множители 3, где a — число троек, а b — остаток\n    let a = n / 3;\n    let b = n % 3;\n    if b == 1 {\n        // Если остаток равен 1, преобразовать одну пару 1 * 3 в 2 * 2\n        3_i32.pow(a as u32 - 1) * 2 * 2\n    } else if b == 2 {\n        // Если остаток равен 2, ничего не делать\n        3_i32.pow(a as u32) * 2\n    } else {\n        // Если остаток равен 0, ничего не делать\n        3_i32.pow(a as u32)\n    }\n}\n
        max_product_cutting.c
        /* Максимальное произведение разрезания: жадный алгоритм */\nint maxProductCutting(int n) {\n    // Когда n <= 3, обязательно нужно выделить одну 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // Жадно выделить множители 3, где a — число троек, а b — остаток\n    int a = n / 3;\n    int b = n % 3;\n    if (b == 1) {\n        // Если остаток равен 1, преобразовать одну пару 1 * 3 в 2 * 2\n        return pow(3, a - 1) * 2 * 2;\n    }\n    if (b == 2) {\n        // Если остаток равен 2, ничего не делать\n        return pow(3, a) * 2;\n    }\n    // Если остаток равен 0, ничего не делать\n    return pow(3, a);\n}\n
        max_product_cutting.kt
        /* Максимальное произведение разрезания: жадный алгоритм */\nfun maxProductCutting(n: Int): Int {\n    // Когда n <= 3, обязательно нужно выделить одну 1\n    if (n <= 3) {\n        return 1 * (n - 1)\n    }\n    // Жадно выделить множители 3, где a — число троек, а b — остаток\n    val a = n / 3\n    val b = n % 3\n    if (b == 1) {\n        // Если остаток равен 1, преобразовать одну пару 1 * 3 в 2 * 2\n        return 3.0.pow((a - 1)).toInt() * 2 * 2\n    }\n    if (b == 2) {\n        // Если остаток равен 2, ничего не делать\n        return 3.0.pow(a).toInt() * 2 * 2\n    }\n    // Если остаток равен 0, ничего не делать\n    return 3.0.pow(a).toInt()\n}\n
        max_product_cutting.rb
        ### Максимальное произведение разрезания: жадный алгоритм ###\ndef max_product_cutting(n)\n  # Когда n <= 3, обязательно нужно выделить одну 1\n  return 1 * (n - 1) if n <= 3\n  # Жадно выделить множители 3, где a — число троек, а b — остаток\n  a, b = n / 3, n % 3\n  # Если остаток равен 1, преобразовать одну пару 1 * 3 в 2 * 2\n  return (3.pow(a - 1) * 2 * 2).to_i if b == 1\n  # Если остаток равен 2, ничего не делать\n  return (3.pow(a) * 2).to_i if b == 2\n  # Если остаток равен 0, ничего не делать\n  3.pow(a).to_i\nend\n
        Визуализация кода

        Во весь экран >

        Рисунок 15-16   Метод вычисления максимального произведения разбиения

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

        • Оператор ** и функция pow() имеют временную сложность \\(O(\\log⁡ a)\\).
        • Функция math.pow() внутри вызывает функцию pow() из библиотеки C, выполняющую возведение в степень с плавающей точкой, и ее временная сложность равна \\(O(1)\\).

        Переменные \\(a\\) и \\(b\\) занимают дополнительную память постоянного размера, поэтому пространственная сложность равна \\(O(1)\\).

        ","path":["Глава 15. Жадность","15.4   Задача о максимальном произведении разбиения"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/#3","level":3,"title":"3.   Доказательство корректности","text":"

        Используем доказательство от противного и рассмотрим только случай \\(n \\geq 4\\).

        1. Все множители \\(\\leq 3\\): предположим, что в оптимальной схеме разбиения существует множитель \\(x \\geq 4\\). Тогда его можно дальше разложить в \\(2(x-2)\\) и получить большее или равное произведение. Это противоречит предположению.
        2. Схема разбиения не содержит \\(1\\): предположим, что в оптимальной схеме присутствует множитель \\(1\\). Тогда его можно объединить с другим множителем и получить большее произведение. Это противоречит предположению.
        3. Схема разбиения содержит не более двух \\(2\\): предположим, что в оптимальной схеме присутствуют три двойки. Тогда их можно заменить двумя тройками и получить большее произведение. Это противоречит предположению.
        ","path":["Глава 15. Жадность","15.4   Задача о максимальном произведении разбиения"],"tags":[]},{"location":"chapter_greedy/summary/","level":1,"title":"15.5   Резюме","text":"","path":["Глава 15. Жадность","15.5   Резюме"],"tags":[]},{"location":"chapter_greedy/summary/#1","level":3,"title":"1.   Ключевые моменты","text":"
        • Жадный алгоритм обычно используется для решения задач оптимизации. Его принцип состоит в том, чтобы на каждом этапе принятия решения делать локально оптимальный выбор в надежде получить глобально оптимальный ответ.
        • Жадный алгоритм итеративно делает один жадный выбор за другим, на каждом шаге превращая задачу в подзадачу меньшего размера, пока задача не будет полностью решена.
        • Жадный алгоритм не только прост в реализации, но и часто обладает высокой эффективностью. По сравнению с динамическим программированием его временная сложность обычно ниже.
        • В задаче о размене монет для некоторых наборов монет жадный алгоритм способен гарантировать оптимальный ответ, а для других наборов - нет: он может дать очень плохое решение.
        • Задачи, подходящие для жадного алгоритма, обладают двумя ключевыми свойствами: свойством жадного выбора и оптимальной подструктурой. Свойство жадного выбора отражает корректность жадной стратегии.
        • Для некоторых сложных задач доказать свойство жадного выбора непросто. Относительно легче найти контрпример и опровергнуть его, как это видно на примере задачи о размене монет.
        • Решение жадной задачи обычно состоит из трех шагов: анализ задачи, определение жадной стратегии и доказательство корректности. Из них ключевым является выбор жадной стратегии, а доказательство корректности часто оказывается самым трудным.
        • В задаче о дробном рюкзаке, в отличие от задачи о рюкзаке 0-1, разрешено брать часть предмета, поэтому ее можно решать жадным алгоритмом. Корректность жадной стратегии доказывается методом от противного.
        • Задачу о максимальной вместимости можно решать полным перебором со временной сложностью \\(O(n^2)\\). Разработав жадную стратегию со сдвигом короткой перегородки внутрь на каждом шаге, временную сложность можно оптимизировать до \\(O(n)\\).
        • В задаче о максимальном произведении разбиения мы последовательно выводим две жадные стратегии: все целые числа \\(\\geq 4\\) следует дальше разбивать, а оптимальным множителем разбиения является \\(3\\). В коде присутствуют операции возведения в степень, поэтому временная сложность зависит от способа их реализации и обычно равна \\(O(1)\\) или \\(O(\\log n)\\).
        ","path":["Глава 15. Жадность","15.5   Резюме"],"tags":[]},{"location":"chapter_hashing/","level":1,"title":"Глава 6.   Хеш-таблицы","text":"

        Abstract

        Хеш-таблица устанавливает соответствие между ключом и значением.

        Благодаря этому она позволяет получать нужное значение по ключу за очень короткое время.

        ","path":["Глава 6. Хеш-таблицы","Глава 6.   Хеш-таблицы"],"tags":[]},{"location":"chapter_hashing/#_1","level":2,"title":"Содержание главы","text":"
        • 6.1   Хеш-таблица
        • 6.2   Хеш-коллизии
        • 6.3   Алгоритмы хеширования
        • 6.4   Резюме
        ","path":["Глава 6. Хеш-таблицы","Глава 6.   Хеш-таблицы"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/","level":1,"title":"6.3   Алгоритмы хеширования","text":"

        В двух предыдущих разделах мы рассмотрели принципы работы хеш-таблицы и способы обработки хеш-коллизий. Однако и открытая адресация, и метод цепочек лишь позволяют хеш-таблице корректно работать при возникновении коллизий, но не уменьшают вероятность появления самих коллизий.

        Если хеш-коллизии происходят слишком часто, производительность хеш-таблицы резко деградирует. Как показано на рисунке 6-8, для хеш-таблицы с методом цепочек в идеальном случае пары ключ-значение равномерно распределены по всем бакетам, и это дает наилучшую эффективность поиска. В худшем же случае все пары ключ-значение оказываются в одном бакете, и временная сложность вырождается до \\(O(n)\\) .

        Рисунок 6-8   Лучший и худший случаи хеш-коллизий

        Распределение пар ключ-значение определяется хеш-функцией. Вспомним этапы вычисления хеш-функции: сначала вычисляется хеш-значение, затем оно берется по модулю длины массива:

        index = hash(key) % capacity\n

        Из этой формулы видно: при фиксированной емкости хеш-таблицы capacity **выходное значение определяет именно хеш-алгоритм hash() **, а значит, именно он определяет распределение пар ключ-значение в хеш-таблице.

        Это означает, что для уменьшения вероятности хеш-коллизий нам следует сосредоточиться на проектировании хеш-алгоритма hash() .

        ","path":["Глава 6. Хеш-таблицы","6.3   Алгоритмы хеширования"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#631-","level":2,"title":"6.3.1   Цели хеш-алгоритма","text":"

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

        • Детерминированность: для одинакового входа хеш-алгоритм всегда должен выдавать одинаковый результат. Только так хеш-таблица остается надежной.
        • Высокая эффективность: вычисление хеш-значения должно быть достаточно быстрым. Чем меньше вычислительные затраты, тем выше практическая ценность хеш-таблицы.
        • Равномерное распределение: хеш-алгоритм должен стараться распределять пары ключ-значение в хеш-таблице равномерно. Чем равномернее распределение, тем ниже вероятность хеш-коллизий.

        На практике хеш-алгоритмы используются не только для реализации хеш-таблиц, но и во многих других областях.

        • Хранение паролей: чтобы защищать пароли пользователей, система обычно хранит не сами пароли в открытом виде, а их хеш-значения. Когда пользователь вводит пароль, система вычисляет хеш-значение введенного пароля и сравнивает его с сохраненным значением. Если они совпадают, пароль считается правильным.
        • Проверка целостности данных: отправитель может вычислить хеш-значение данных и отправить его вместе с самими данными. Получатель затем вычисляет хеш-значение повторно и сравнивает его с полученным. Если они совпадают, данные считаются целостными.

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

        • Односторонность: по хеш-значению нельзя восстановить какую-либо информацию о входных данных.
        • Устойчивость к коллизиям: должно быть крайне трудно найти два разных входа, имеющих одинаковое хеш-значение.
        • Эффект лавины: даже небольшое изменение во входных данных должно приводить к заметному и непредсказуемому изменению результата.

        Обрати внимание: «равномерное распределение» и «устойчивость к коллизиям» - это два независимых понятия , и выполнение первого не означает автоматического выполнения второго. Например, при случайном распределении входных key хеш-функция key % 100 может выдавать достаточно равномерное распределение. Однако этот хеш-алгоритм слишком прост: все key с одинаковыми двумя последними цифрами будут иметь одинаковый результат, а значит, по хеш-значению можно легко подобрать подходящие key и, например, взломать пароль.

        ","path":["Глава 6. Хеш-таблицы","6.3   Алгоритмы хеширования"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#632-","level":2,"title":"6.3.2   Проектирование хеш-алгоритма","text":"

        Разработка хеш-алгоритма - это сложная задача, в которой нужно учитывать множество факторов. Однако для некоторых нетребовательных сценариев мы можем спроектировать и несколько простых хеш-алгоритмов.

        • Аддитивный хеш: складываем ASCII-коды всех символов входной строки и используем полученную сумму как хеш-значение.
        • Мультипликативный хеш: используем «некоррелированность» умножения. На каждом шаге умножаем текущее значение на константу и добавляем ASCII-код очередного символа.
        • XOR-хеш: последовательно накапливаем элементы входных данных в одном хеш-значении через операцию XOR.
        • Ротационный хеш: последовательно накапливаем ASCII-коды символов, причем перед каждым накоплением выполняем циклический сдвиг хеш-значения.
        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby simple_hash.py
        def add_hash(key: str) -> int:\n    \"\"\"Аддитивное хеширование\"\"\"\n    hash = 0\n    modulus = 1000000007\n    for c in key:\n        hash += ord(c)\n    return hash % modulus\n\ndef mul_hash(key: str) -> int:\n    \"\"\"Мультипликативное хеширование\"\"\"\n    hash = 0\n    modulus = 1000000007\n    for c in key:\n        hash = 31 * hash + ord(c)\n    return hash % modulus\n\ndef xor_hash(key: str) -> int:\n    \"\"\"XOR-хеширование\"\"\"\n    hash = 0\n    modulus = 1000000007\n    for c in key:\n        hash ^= ord(c)\n    return hash % modulus\n\ndef rot_hash(key: str) -> int:\n    \"\"\"Хеширование с циклическим сдвигом\"\"\"\n    hash = 0\n    modulus = 1000000007\n    for c in key:\n        hash = (hash << 4) ^ (hash >> 28) ^ ord(c)\n    return hash % modulus\n
        simple_hash.cpp
        /* Аддитивное хеширование */\nint addHash(string key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (unsigned char c : key) {\n        hash = (hash + (int)c) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* Мультипликативное хеширование */\nint mulHash(string key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (unsigned char c : key) {\n        hash = (31 * hash + (int)c) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* XOR-хеширование */\nint xorHash(string key) {\n    int hash = 0;\n    const int MODULUS = 1000000007;\n    for (unsigned char c : key) {\n        hash ^= (int)c;\n    }\n    return hash & MODULUS;\n}\n\n/* Хеширование с циклическим сдвигом */\nint rotHash(string key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (unsigned char c : key) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ (int)c) % MODULUS;\n    }\n    return (int)hash;\n}\n
        simple_hash.java
        /* Аддитивное хеширование */\nint addHash(String key) {\n    long hash = 0;\n    final int MODULUS = 1000000007;\n    for (char c : key.toCharArray()) {\n        hash = (hash + (int) c) % MODULUS;\n    }\n    return (int) hash;\n}\n\n/* Мультипликативное хеширование */\nint mulHash(String key) {\n    long hash = 0;\n    final int MODULUS = 1000000007;\n    for (char c : key.toCharArray()) {\n        hash = (31 * hash + (int) c) % MODULUS;\n    }\n    return (int) hash;\n}\n\n/* XOR-хеширование */\nint xorHash(String key) {\n    int hash = 0;\n    final int MODULUS = 1000000007;\n    for (char c : key.toCharArray()) {\n        hash ^= (int) c;\n    }\n    return hash & MODULUS;\n}\n\n/* Хеширование с циклическим сдвигом */\nint rotHash(String key) {\n    long hash = 0;\n    final int MODULUS = 1000000007;\n    for (char c : key.toCharArray()) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ (int) c) % MODULUS;\n    }\n    return (int) hash;\n}\n
        simple_hash.cs
        /* Аддитивное хеширование */\nint AddHash(string key) {\n    long hash = 0;\n    const int MODULUS = 1000000007;\n    foreach (char c in key) {\n        hash = (hash + c) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* Мультипликативное хеширование */\nint MulHash(string key) {\n    long hash = 0;\n    const int MODULUS = 1000000007;\n    foreach (char c in key) {\n        hash = (31 * hash + c) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* XOR-хеширование */\nint XorHash(string key) {\n    int hash = 0;\n    const int MODULUS = 1000000007;\n    foreach (char c in key) {\n        hash ^= c;\n    }\n    return hash & MODULUS;\n}\n\n/* Хеширование с циклическим сдвигом */\nint RotHash(string key) {\n    long hash = 0;\n    const int MODULUS = 1000000007;\n    foreach (char c in key) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ c) % MODULUS;\n    }\n    return (int)hash;\n}\n
        simple_hash.go
        /* Аддитивное хеширование */\nfunc addHash(key string) int {\n    var hash int64\n    var modulus int64\n\n    modulus = 1000000007\n    for _, b := range []byte(key) {\n        hash = (hash + int64(b)) % modulus\n    }\n    return int(hash)\n}\n\n/* Мультипликативное хеширование */\nfunc mulHash(key string) int {\n    var hash int64\n    var modulus int64\n\n    modulus = 1000000007\n    for _, b := range []byte(key) {\n        hash = (31*hash + int64(b)) % modulus\n    }\n    return int(hash)\n}\n\n/* XOR-хеширование */\nfunc xorHash(key string) int {\n    hash := 0\n    modulus := 1000000007\n    for _, b := range []byte(key) {\n        fmt.Println(int(b))\n        hash ^= int(b)\n        hash = (31*hash + int(b)) % modulus\n    }\n    return hash & modulus\n}\n\n/* Хеширование с циклическим сдвигом */\nfunc rotHash(key string) int {\n    var hash int64\n    var modulus int64\n\n    modulus = 1000000007\n    for _, b := range []byte(key) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ int64(b)) % modulus\n    }\n    return int(hash)\n}\n
        simple_hash.swift
        /* Аддитивное хеширование */\nfunc addHash(key: String) -> Int {\n    var hash = 0\n    let MODULUS = 1_000_000_007\n    for c in key {\n        for scalar in c.unicodeScalars {\n            hash = (hash + Int(scalar.value)) % MODULUS\n        }\n    }\n    return hash\n}\n\n/* Мультипликативное хеширование */\nfunc mulHash(key: String) -> Int {\n    var hash = 0\n    let MODULUS = 1_000_000_007\n    for c in key {\n        for scalar in c.unicodeScalars {\n            hash = (31 * hash + Int(scalar.value)) % MODULUS\n        }\n    }\n    return hash\n}\n\n/* XOR-хеширование */\nfunc xorHash(key: String) -> Int {\n    var hash = 0\n    let MODULUS = 1_000_000_007\n    for c in key {\n        for scalar in c.unicodeScalars {\n            hash ^= Int(scalar.value)\n        }\n    }\n    return hash & MODULUS\n}\n\n/* Хеширование с циклическим сдвигом */\nfunc rotHash(key: String) -> Int {\n    var hash = 0\n    let MODULUS = 1_000_000_007\n    for c in key {\n        for scalar in c.unicodeScalars {\n            hash = ((hash << 4) ^ (hash >> 28) ^ Int(scalar.value)) % MODULUS\n        }\n    }\n    return hash\n}\n
        simple_hash.js
        /* Аддитивное хеширование */\nfunction addHash(key) {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = (hash + c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n\n/* Мультипликативное хеширование */\nfunction mulHash(key) {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = (31 * hash + c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n\n/* XOR-хеширование */\nfunction xorHash(key) {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash ^= c.charCodeAt(0);\n    }\n    return hash % MODULUS;\n}\n\n/* Хеширование с циклическим сдвигом */\nfunction rotHash(key) {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n
        simple_hash.ts
        /* Аддитивное хеширование */\nfunction addHash(key: string): number {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = (hash + c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n\n/* Мультипликативное хеширование */\nfunction mulHash(key: string): number {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = (31 * hash + c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n\n/* XOR-хеширование */\nfunction xorHash(key: string): number {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash ^= c.charCodeAt(0);\n    }\n    return hash % MODULUS;\n}\n\n/* Хеширование с циклическим сдвигом */\nfunction rotHash(key: string): number {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n
        simple_hash.dart
        /* Аддитивное хеширование */\nint addHash(String key) {\n  int hash = 0;\n  final int MODULUS = 1000000007;\n  for (int i = 0; i < key.length; i++) {\n    hash = (hash + key.codeUnitAt(i)) % MODULUS;\n  }\n  return hash;\n}\n\n/* Мультипликативное хеширование */\nint mulHash(String key) {\n  int hash = 0;\n  final int MODULUS = 1000000007;\n  for (int i = 0; i < key.length; i++) {\n    hash = (31 * hash + key.codeUnitAt(i)) % MODULUS;\n  }\n  return hash;\n}\n\n/* XOR-хеширование */\nint xorHash(String key) {\n  int hash = 0;\n  final int MODULUS = 1000000007;\n  for (int i = 0; i < key.length; i++) {\n    hash ^= key.codeUnitAt(i);\n  }\n  return hash & MODULUS;\n}\n\n/* Хеширование с циклическим сдвигом */\nint rotHash(String key) {\n  int hash = 0;\n  final int MODULUS = 1000000007;\n  for (int i = 0; i < key.length; i++) {\n    hash = ((hash << 4) ^ (hash >> 28) ^ key.codeUnitAt(i)) % MODULUS;\n  }\n  return hash;\n}\n
        simple_hash.rs
        /* Аддитивное хеширование */\nfn add_hash(key: &str) -> i32 {\n    let mut hash = 0_i64;\n    const MODULUS: i64 = 1000000007;\n\n    for c in key.chars() {\n        hash = (hash + c as i64) % MODULUS;\n    }\n\n    hash as i32\n}\n\n/* Мультипликативное хеширование */\nfn mul_hash(key: &str) -> i32 {\n    let mut hash = 0_i64;\n    const MODULUS: i64 = 1000000007;\n\n    for c in key.chars() {\n        hash = (31 * hash + c as i64) % MODULUS;\n    }\n\n    hash as i32\n}\n\n/* XOR-хеширование */\nfn xor_hash(key: &str) -> i32 {\n    let mut hash = 0_i64;\n    const MODULUS: i64 = 1000000007;\n\n    for c in key.chars() {\n        hash ^= c as i64;\n    }\n\n    (hash & MODULUS) as i32\n}\n\n/* Хеширование с циклическим сдвигом */\nfn rot_hash(key: &str) -> i32 {\n    let mut hash = 0_i64;\n    const MODULUS: i64 = 1000000007;\n\n    for c in key.chars() {\n        hash = ((hash << 4) ^ (hash >> 28) ^ c as i64) % MODULUS;\n    }\n\n    hash as i32\n}\n
        simple_hash.c
        /* Аддитивное хеширование */\nint addHash(char *key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (int i = 0; i < strlen(key); i++) {\n        hash = (hash + (unsigned char)key[i]) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* Мультипликативное хеширование */\nint mulHash(char *key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (int i = 0; i < strlen(key); i++) {\n        hash = (31 * hash + (unsigned char)key[i]) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* XOR-хеширование */\nint xorHash(char *key) {\n    int hash = 0;\n    const int MODULUS = 1000000007;\n\n    for (int i = 0; i < strlen(key); i++) {\n        hash ^= (unsigned char)key[i];\n    }\n    return hash & MODULUS;\n}\n\n/* Хеширование с циклическим сдвигом */\nint rotHash(char *key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (int i = 0; i < strlen(key); i++) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ (unsigned char)key[i]) % MODULUS;\n    }\n\n    return (int)hash;\n}\n
        simple_hash.kt
        /* Аддитивное хеширование */\nfun addHash(key: String): Int {\n    var hash = 0L\n    val MODULUS = 1000000007\n    for (c in key.toCharArray()) {\n        hash = (hash + c.code) % MODULUS\n    }\n    return hash.toInt()\n}\n\n/* Мультипликативное хеширование */\nfun mulHash(key: String): Int {\n    var hash = 0L\n    val MODULUS = 1000000007\n    for (c in key.toCharArray()) {\n        hash = (31 * hash + c.code) % MODULUS\n    }\n    return hash.toInt()\n}\n\n/* XOR-хеширование */\nfun xorHash(key: String): Int {\n    var hash = 0\n    val MODULUS = 1000000007\n    for (c in key.toCharArray()) {\n        hash = hash xor c.code\n    }\n    return hash and MODULUS\n}\n\n/* Хеширование с циклическим сдвигом */\nfun rotHash(key: String): Int {\n    var hash = 0L\n    val MODULUS = 1000000007\n    for (c in key.toCharArray()) {\n        hash = ((hash shl 4) xor (hash shr 28) xor c.code.toLong()) % MODULUS\n    }\n    return hash.toInt()\n}\n
        simple_hash.rb
        ### Аддитивное хеширование ###\ndef add_hash(key)\n  hash = 0\n  modulus = 1_000_000_007\n\n  key.each_char { |c| hash += c.ord }\n\n  hash % modulus\nend\n\n### Мультипликативное хеширование ###\ndef mul_hash(key)\n  hash = 0\n  modulus = 1_000_000_007\n\n  key.each_char { |c| hash = 31 * hash + c.ord }\n\n  hash % modulus\nend\n\n### XOR-хеширование ###\ndef xor_hash(key)\n  hash = 0\n  modulus = 1_000_000_007\n\n  key.each_char { |c| hash ^= c.ord }\n\n  hash % modulus\nend\n\n### Хеширование с циклическим сдвигом ###\ndef rot_hash(key)\n  hash = 0\n  modulus = 1_000_000_007\n\n  key.each_char { |c| hash = (hash << 4) ^ (hash >> 28) ^ c.ord }\n\n  hash % modulus\nend\n
        Визуализация кода

        Во весь экран >

        Нетрудно заметить, что последний шаг каждого из этих хеш-алгоритмов - взятие по модулю большого простого числа \\(1000000007\\) , чтобы гарантировать, что хеш-значение остается в разумных границах. Стоит задуматься: почему подчеркивается именно взятие по модулю простого числа, и какие недостатки возникают при использовании составного модуля? Это интересный вопрос.

        Сначала дадим вывод: использование большого простого числа в качестве модуля позволяет в максимальной степени обеспечивать равномерное распределение хеш-значений. Поскольку простое число не имеет общих делителей с другими числами, это помогает уменьшить периодические закономерности, возникающие из-за операции взятия остатка, и тем самым снизить число хеш-коллизий.

        Рассмотрим пример. Предположим, мы выбрали составное число \\(9\\) в качестве модуля. Оно делится на \\(3\\) , поэтому все key , которые делятся на \\(3\\) , будут отображаться только в три хеш-значения: \\(0\\) , \\(3\\) , \\(6\\) .

        \\[ \\begin{aligned} \\text{modulus} & = 9 \\newline \\text{key} & = \\{ 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, \\dots \\} \\newline \\text{hash} & = \\{ 0, 3, 6, 0, 3, 6, 0, 3, 6, 0, 3, 6,\\dots \\} \\end{aligned} \\]

        Если входные key как раз удовлетворяют такому распределению в виде арифметической прогрессии, то хеш-значения начнут скучиваться, а это усугубит хеш-коллизии. Теперь предположим, что мы заменили modulus на простое число \\(13\\). Поскольку между key и modulus нет общих делителей, равномерность распределения хеш-значений заметно улучшится.

        \\[ \\begin{aligned} \\text{modulus} & = 13 \\newline \\text{key} & = \\{ 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, \\dots \\} \\newline \\text{hash} & = \\{ 0, 3, 6, 9, 12, 2, 5, 8, 11, 1, 4, 7, \\dots \\} \\end{aligned} \\]

        Следует отметить: если можно гарантировать, что key распределены случайно и равномерно, то выбор простого или составного числа в качестве модуля не так важен - оба варианта способны дать равномерное распределение хеш-значений. Но если в распределении key присутствует периодичность, то взятие по модулю составного числа гораздо легче приводит к кластеризации.

        Итак, на практике мы обычно выбираем простое число в качестве модуля, причем это простое число желательно брать достаточно большим, чтобы по возможности убрать периодические закономерности и повысить устойчивость хеш-алгоритма.

        ","path":["Глава 6. Хеш-таблицы","6.3   Алгоритмы хеширования"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#633-","level":2,"title":"6.3.3   Распространенные хеш-алгоритмы","text":"

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

        На практике мы обычно используем стандартные хеш-алгоритмы, такие как MD5, SHA-1, SHA-2 и SHA-3. Они могут отображать входные данные произвольной длины в хеш-значения фиксированной длины.

        На протяжении почти ста лет хеш-алгоритмы непрерывно развивались и оптимизировались. Одни исследователи старались повысить их производительность, а другие исследователи и хакеры сосредоточивались на поиске уязвимостей в их безопасности. В таблице 6-2 приведены распространенные хеш-алгоритмы, которые часто встречаются в реальных приложениях.

        • MD5 и SHA-1 уже многократно были успешно атакованы, поэтому они выведены из большинства сценариев, где требуется безопасность.
        • SHA-256 из семейства SHA-2 является одним из самых надежных хеш-алгоритмов. На сегодняшний день не известно успешных практических атак, поэтому он широко используется в самых разных протоколах и системах безопасности.
        • SHA-3 по сравнению с SHA-2 требует меньших затрат на реализацию и обеспечивает более высокую вычислительную эффективность, но на данный момент распространен слабее, чем семейство SHA-2.

        Таблица 6-2   Распространенные хеш-алгоритмы

        MD5 SHA-1 SHA-2 SHA-3 Год появления 1992 1995 2002 2008 Длина вывода 128 bit 160 bit 256/512 bit 224/256/384/512 bit Хеш-коллизии Частые Частые Редкие Редкие Уровень безопасности Низкий, успешно атакован Низкий, успешно атакован Высокий Высокий Применение Устарел, но еще используется для проверки целостности данных Устарел Проверка криптовалютных транзакций, цифровые подписи и т. д. Может использоваться как замена SHA-2","path":["Глава 6. Хеш-таблицы","6.3   Алгоритмы хеширования"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#634-","level":2,"title":"6.3.4   Хеш-значения структур данных","text":"

        Мы знаем, что key в хеш-таблице могут быть целыми числами, вещественными числами, строками и другими типами данных. Языки программирования обычно предоставляют встроенные хеш-алгоритмы для этих типов, чтобы вычислять индексы бакетов в хеш-таблице. Возьмем Python: в нем можно вызвать функцию hash() , чтобы вычислить хеш-значения для различных типов данных.

        • Хеш-значение целого числа и булева значения совпадает с самим значением.
        • Вычисление хеш-значений для вещественных чисел и строк устроено сложнее. Интересующиеся читатели могут изучить это самостоятельно.
        • Хеш-значение кортежа получается путем хеширования каждого элемента, а затем объединения этих хеш-значений в одно итоговое значение.
        • Хеш-значение объекта обычно строится на основе его адреса в памяти. Если переопределить метод хеширования объекта, можно реализовать вычисление хеша по содержимому.

        Tip

        Обрати внимание: определения и способы вычисления встроенных хеш-значений в разных языках программирования отличаются.

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby built_in_hash.py
        num = 3\nhash_num = hash(num)\n# Хеш-значение целого числа 3 равно 3\n\nbol = True\nhash_bol = hash(bol)\n# Хеш-значение булевого значения True равно 1\n\ndec = 3.14159\nhash_dec = hash(dec)\n# Хеш-значение числа 3.14159 равно 326484311674566659\n\nstr = \"Hello Algo\"\nhash_str = hash(str)\n# Хеш-значение строки \"Hello Algo\" равно 4617003410720528961\n\ntup = (12836, \"Сяо Ха\")\nhash_tup = hash(tup)\n# Хеш-значение кортежа (12836, \"Сяо Ха\") равно 1029005403108185979\n\nobj = ListNode(0)\nhash_obj = hash(obj)\n# Хеш-значение объекта узла <ListNode object at 0x1058fd810> равно 274267521\n
        built_in_hash.cpp
        int num = 3;\nsize_t hashNum = hash<int>()(num);\n// Хеш-значение целого числа 3 равно 3\n\nbool bol = true;\nsize_t hashBol = hash<bool>()(bol);\n// Хеш-значение булевого значения 1 равно 1\n\ndouble dec = 3.14159;\nsize_t hashDec = hash<double>()(dec);\n// Хеш-значение числа 3.14159 равно 4614256650576692846\n\nstring str = \"Hello Algo\";\nsize_t hashStr = hash<string>()(str);\n// Хеш-значение строки \"Hello Algo\" равно 15466937326284535026\n\n// В C++ встроенный std::hash() предоставляет вычисление хеша только для базовых типов данных\n// Для массивов и объектов хеш-значение обычно приходится реализовывать самостоятельно\n
        built_in_hash.java
        int num = 3;\nint hashNum = Integer.hashCode(num);\n// Хеш-значение целого числа 3 равно 3\n\nboolean bol = true;\nint hashBol = Boolean.hashCode(bol);\n// Хеш-значение булевого значения true равно 1231\n\ndouble dec = 3.14159;\nint hashDec = Double.hashCode(dec);\n// Хеш-значение числа 3.14159 равно -1340954729\n\nString str = \"Hello Algo\";\nint hashStr = str.hashCode();\n// Хеш-значение строки \"Hello Algo\" равно -727081396\n\nObject[] arr = { 12836, \"Сяо Ха\" };\nint hashTup = Arrays.hashCode(arr);\n// Хеш-значение массива [12836, Сяо Ха] равно 1151158\n\nListNode obj = new ListNode(0);\nint hashObj = obj.hashCode();\n// Хеш-значение объекта узла utils.ListNode@7dc5e7b4 равно 2110121908\n
        built_in_hash.cs
        int num = 3;\nint hashNum = num.GetHashCode();\n// Хеш-значение целого числа 3 равно 3;\n\nbool bol = true;\nint hashBol = bol.GetHashCode();\n// Хеш-значение булевого значения true равно 1;\n\ndouble dec = 3.14159;\nint hashDec = dec.GetHashCode();\n// Хеш-значение числа 3.14159 равно -1340954729;\n\nstring str = \"Hello Algo\";\nint hashStr = str.GetHashCode();\n// Хеш-значение строки \"Hello Algo\" равно -586107568;\n\nobject[] arr = [12836, \"Сяо Ха\"];\nint hashTup = arr.GetHashCode();\n// Хеш-значение массива [12836, Сяо Ха] равно 42931033;\n\nListNode obj = new(0);\nint hashObj = obj.GetHashCode();\n// Хеш-значение объекта узла 0 равно 39053774;\n
        built_in_hash.go
        // В Go нет встроенной функции hash code\n
        built_in_hash.swift
        let num = 3\nlet hashNum = num.hashValue\n// Хеш-значение целого числа 3 равно 9047044699613009734\n\nlet bol = true\nlet hashBol = bol.hashValue\n// Хеш-значение булевого значения true равно -4431640247352757451\n\nlet dec = 3.14159\nlet hashDec = dec.hashValue\n// Хеш-значение числа 3.14159 равно -2465384235396674631\n\nlet str = \"Hello Algo\"\nlet hashStr = str.hashValue\n// Хеш-значение строки \"Hello Algo\" равно -7850626797806988787\n\nlet arr = [AnyHashable(12836), AnyHashable(\"Сяо Ха\")]\nlet hashTup = arr.hashValue\n// Хеш-значение массива [AnyHashable(12836), AnyHashable(\"Сяо Ха\")] равно -2308633508154532996\n\nlet obj = ListNode(x: 0)\nlet hashObj = obj.hashValue\n// Хеш-значение объекта узла utils.ListNode равно -2434780518035996159\n
        built_in_hash.js
        // В JavaScript нет встроенной функции hash code\n
        built_in_hash.ts
        // В TypeScript нет встроенной функции hash code\n
        built_in_hash.dart
        int num = 3;\nint hashNum = num.hashCode;\n// Хеш-значение целого числа 3 равно 34803\n\nbool bol = true;\nint hashBol = bol.hashCode;\n// Хеш-значение булевого значения true равно 1231\n\ndouble dec = 3.14159;\nint hashDec = dec.hashCode;\n// Хеш-значение числа 3.14159 равно 2570631074981783\n\nString str = \"Hello Algo\";\nint hashStr = str.hashCode;\n// Хеш-значение строки \"Hello Algo\" равно 468167534\n\nList arr = [12836, \"Сяо Ха\"];\nint hashArr = arr.hashCode;\n// Хеш-значение массива [12836, Сяо Ха] равно 976512528\n\nListNode obj = new ListNode(0);\nint hashObj = obj.hashCode;\n// Хеш-значение объекта Instance of 'ListNode' равно 1033450432\n
        built_in_hash.rs
        use std::collections::hash_map::DefaultHasher;\nuse std::hash::{Hash, Hasher};\n\nlet num = 3;\nlet mut num_hasher = DefaultHasher::new();\nnum.hash(&mut num_hasher);\nlet hash_num = num_hasher.finish();\n// Хеш-значение целого числа 3 равно 568126464209439262\n\nlet bol = true;\nlet mut bol_hasher = DefaultHasher::new();\nbol.hash(&mut bol_hasher);\nlet hash_bol = bol_hasher.finish();\n// Хеш-значение булевого значения true равно 4952851536318644461\n\nlet dec: f32 = 3.14159;\nlet mut dec_hasher = DefaultHasher::new();\ndec.to_bits().hash(&mut dec_hasher);\nlet hash_dec = dec_hasher.finish();\n// Хеш-значение числа 3.14159 равно 2566941990314602357\n\nlet str = \"Hello Algo\";\nlet mut str_hasher = DefaultHasher::new();\nstr.hash(&mut str_hasher);\nlet hash_str = str_hasher.finish();\n// Хеш-значение строки \"Hello Algo\" равно 16092673739211250988\n\nlet arr = (&12836, &\"Сяо Ха\");\nlet mut tup_hasher = DefaultHasher::new();\narr.hash(&mut tup_hasher);\nlet hash_tup = tup_hasher.finish();\n// Хеш-значение кортежа (12836, \"Сяо Ха\") равно 1885128010422702749\n\nlet node = ListNode::new(42);\nlet mut hasher = DefaultHasher::new();\nnode.borrow().val.hash(&mut hasher);\nlet hash = hasher.finish();\n// Хеш-значение объекта RefCell { value: ListNode { val: 42, next: None } } равно 15387811073369036852\n
        built_in_hash.c
        // В C нет встроенной функции hash code\n
        built_in_hash.kt
        val num = 3\nval hashNum = num.hashCode()\n// Хеш-значение целого числа 3 равно 3\n\nval bol = true\nval hashBol = bol.hashCode()\n// Хеш-значение булевого значения true равно 1231\n\nval dec = 3.14159\nval hashDec = dec.hashCode()\n// Хеш-значение числа 3.14159 равно -1340954729\n\nval str = \"Hello Algo\"\nval hashStr = str.hashCode()\n// Хеш-значение строки \"Hello Algo\" равно -727081396\n\nval arr = arrayOf<Any>(12836, \"Сяо Ха\")\nval hashTup = arr.hashCode()\n// Хеш-значение массива [12836, Сяо Ха] равно 189568618\n\nval obj = ListNode(0)\nval hashObj = obj.hashCode()\n// Хеш-значение объекта узла utils.ListNode@1d81eb93 равно 495053715\n
        built_in_hash.rb
        num = 3\nhash_num = num.hash\n# Хеш-значение целого числа 3 равно -4385856518450339636\n\nbol = true\nhash_bol = bol.hash\n# Хеш-значение булевого значения true равно -1617938112149317027\n\ndec = 3.14159\nhash_dec = dec.hash\n# Хеш-значение числа 3.14159 равно -1479186995943067893\n\nstr = \"Hello Algo\"\nhash_str = str.hash\n# Хеш-значение строки \"Hello Algo\" равно -4075943250025831763\n\ntup = [12836, 'Сяо Ха']\nhash_tup = tup.hash\n# Хеш-значение кортежа (12836, 'Сяо Ха') равно 1999544809202288822\n\nobj = ListNode.new(0)\nhash_obj = obj.hash\n# Хеш-значение объекта #<ListNode:0x000078133140ab70> равно 4302940560806366381\n
        Визуализация выполнения

        https://pythontutor.com/render.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%D1%81%D0%B2%D1%8F%D0%B7%D0%BD%D1%8B%D0%B9%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%D1%83%D0%B7%D0%B5%D0%BB%D0%BA%D0%BB%D0%B0%D1%81%D1%81%22%22%22%0A%20%20%20%20def%20__init__%28self%2C%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%D0%97%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%D1%83%D0%B7%D0%BB%D0%B0%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%D0%A1%D1%81%D1%8B%D0%BB%D0%BA%D0%B0%20%D0%BD%D0%B0%20%D1%81%D0%BB%D0%B5%D0%B4%D1%83%D1%8E%D1%89%D0%B8%D0%B9%20%D1%83%D0%B7%D0%B5%D0%BB%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20num%20%3D%203%0A%20%20%20%20hash_num%20%3D%20hash%28num%29%0A%20%20%20%20%23%20%D0%A5%D0%B5%D1%88-%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%D1%86%D0%B5%D0%BB%D0%BE%D0%B3%D0%BE%20%D1%87%D0%B8%D1%81%D0%BB%D0%B0%203%20%D1%80%D0%B0%D0%B2%D0%BD%D0%BE%203%0A%0A%20%20%20%20bol%20%3D%20True%0A%20%20%20%20hash_bol%20%3D%20hash%28bol%29%0A%20%20%20%20%23%20%D0%A5%D0%B5%D1%88-%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%D0%B1%D1%83%D0%BB%D0%B5%D0%B2%D0%B0%20%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D1%8F%20True%20%D1%80%D0%B0%D0%B2%D0%BD%D0%BE%201%0A%0A%20%20%20%20dec%20%3D%203.14159%0A%20%20%20%20hash_dec%20%3D%20hash%28dec%29%0A%20%20%20%20%23%20%D0%A5%D0%B5%D1%88-%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%D1%87%D0%B8%D1%81%D0%BB%D0%B0%203.14159%20%D1%80%D0%B0%D0%B2%D0%BD%D0%BE%20326484311674566659%0A%0A%20%20%20%20str%20%3D%20%22Hello%20Algo%22%0A%20%20%20%20hash_str%20%3D%20hash%28str%29%0A%20%20%20%20%23%20%D0%A5%D0%B5%D1%88-%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%D1%81%D1%82%D1%80%D0%BE%D0%BA%D0%B8%20%22Hello%20Algo%22%20%D1%80%D0%B0%D0%B2%D0%BD%D0%BE%204617003410720528961%0A%0A%20%20%20%20tup%20%3D%20%2812836%2C%20%22%D0%A1%D1%8F%D0%BE%20%D0%A5%D0%B0%22%29%0A%20%20%20%20hash_tup%20%3D%20hash%28tup%29%0A%20%20%20%20%23%20%D0%A5%D0%B5%D1%88-%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%D0%BA%D0%BE%D1%80%D1%82%D0%B5%D0%B6%D0%B0%20%2812836%2C%20%27%D0%A1%D1%8F%D0%BE%20%D0%A5%D0%B0%27%29%20%D1%80%D0%B0%D0%B2%D0%BD%D0%BE%201029005403108185979%0A%0A%20%20%20%20obj%20%3D%20ListNode%280%29%0A%20%20%20%20hash_obj%20%3D%20hash%28obj%29%0A%20%20%20%20%23%20%D0%A5%D0%B5%D1%88-%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%D0%BE%D0%B1%D1%8A%D0%B5%D0%BA%D1%82%D0%B0%20%D1%83%D0%B7%D0%BB%D0%B0%20%3CListNode%20object%20at%200x1058fd810%3E%20%D1%80%D0%B0%D0%B2%D0%BD%D0%BE%20274267521&cumulative=false&curInstr=19&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

        Во многих языках программирования в качестве key хеш-таблицы можно использовать только неизменяемые объекты . Если, например, использовать список (динамический массив) как key , то после изменения содержимого списка изменится и его хеш-значение, из-за чего мы уже не сможем найти прежнее value в хеш-таблице.

        Хотя у пользовательских объектов (например, у узла связного списка) поля являются изменяемыми, сам объект все же может быть хешируемым. Причина в том, что хеш-значение объекта обычно строится на основе адреса в памяти : даже если содержимое объекта меняется, его адрес памяти остается прежним, а значит, и хеш-значение не меняется.

        Внимательный читатель мог заметить, что при запуске программы в разных консолях выводимые хеш-значения отличаются. Это связано с тем, что интерпретатор Python при каждом запуске добавляет в хеш-функцию строк случайную соль (salt). Такой подход эффективно защищает от атак типа HashDoS и повышает безопасность хеш-алгоритма.

        ","path":["Глава 6. Хеш-таблицы","6.3   Алгоритмы хеширования"],"tags":[]},{"location":"chapter_hashing/hash_collision/","level":1,"title":"6.2   Хеш-коллизии","text":"

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

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

        1. Улучшить структуру данных хеш-таблицы, чтобы она могла корректно работать даже при возникновении хеш-коллизий.
        2. Выполнять расширение только тогда, когда это действительно необходимо, то есть когда хеш-коллизии становятся достаточно серьезными.

        Основные способы улучшения структуры хеш-таблицы включают метод цепочек и открытую адресацию.

        ","path":["Глава 6. Хеш-таблицы","6.2   Хеш-коллизии"],"tags":[]},{"location":"chapter_hashing/hash_collision/#621","level":2,"title":"6.2.1   Метод цепочек","text":"

        В исходной хеш-таблице каждый бакет может хранить только одну пару ключ-значение. Метод цепочек (separate chaining) превращает отдельный элемент в связный список: пары ключ-значение становятся узлами списка, и все конфликтующие пары ключ-значение хранятся в одном и том же списке. На рисунке 6-5 показан пример хеш-таблицы, реализованной методом цепочек.

        Рисунок 6-5   Хеш-таблица с методом цепочек

        Методы работы с хеш-таблицей, построенной на основе метода цепочек, меняются следующим образом.

        • Поиск элемента: передаем key , по хеш-функции получаем индекс бакета, после чего обращаемся к голове списка и обходим список, сравнивая key , пока не найдем целевую пару ключ-значение.
        • Добавление элемента: сначала через хеш-функцию получаем голову списка, затем добавляем узел (пару ключ-значение) в этот список.
        • Удаление элемента: по результату хеш-функции обращаемся к голове списка, затем обходим список, находим целевой узел и удаляем его.

        Метод цепочек имеет следующие ограничения.

        • Рост потребления памяти: связный список содержит указатели на узлы, поэтому по сравнению с массивом он требует больше памяти.
        • Снижение эффективности поиска: для нахождения нужного элемента нужно линейно обходить связный список.

        Ниже приведена простая реализация хеш-таблицы методом цепочек. Следует обратить внимание на два момента.

        • Для упрощения кода вместо связного списка используется список (динамический массив). В этой реализации хеш-таблица (массив) содержит несколько бакетов, и каждый бакет представляет собой список.
        • Ниже включен метод расширения хеш-таблицы. Когда коэффициент загрузки превышает \\(\\frac{2}{3}\\) , мы расширяем хеш-таблицу до \\(2\\) раз от прежней емкости.
        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map_chaining.py
        class HashMapChaining:\n    \"\"\"Хеш-таблица с цепочками\"\"\"\n\n    def __init__(self):\n        \"\"\"Конструктор\"\"\"\n        self.size = 0  # Число пар ключ-значение\n        self.capacity = 4  # Вместимость хеш-таблицы\n        self.load_thres = 2.0 / 3.0  # Порог коэффициента загрузки для запуска расширения\n        self.extend_ratio = 2  # Коэффициент расширения\n        self.buckets = [[] for _ in range(self.capacity)]  # Массив корзин\n\n    def hash_func(self, key: int) -> int:\n        \"\"\"Хеш-функция\"\"\"\n        return key % self.capacity\n\n    def load_factor(self) -> float:\n        \"\"\"Коэффициент загрузки\"\"\"\n        return self.size / self.capacity\n\n    def get(self, key: int) -> str | None:\n        \"\"\"Операция поиска\"\"\"\n        index = self.hash_func(key)\n        bucket = self.buckets[index]\n        # Обойти корзину; если найден key, вернуть соответствующее val\n        for pair in bucket:\n            if pair.key == key:\n                return pair.val\n        # Если key не найден, вернуть None\n        return None\n\n    def put(self, key: int, val: str):\n        \"\"\"Операция добавления\"\"\"\n        # Когда коэффициент загрузки превышает порог, выполнить расширение\n        if self.load_factor() > self.load_thres:\n            self.extend()\n        index = self.hash_func(key)\n        bucket = self.buckets[index]\n        # Обойти корзину; если встретился указанный key, обновить соответствующее val и вернуть\n        for pair in bucket:\n            if pair.key == key:\n                pair.val = val\n                return\n        # Если такого key нет, добавить пару ключ-значение в конец\n        pair = Pair(key, val)\n        bucket.append(pair)\n        self.size += 1\n\n    def remove(self, key: int):\n        \"\"\"Операция удаления\"\"\"\n        index = self.hash_func(key)\n        bucket = self.buckets[index]\n        # Обойти корзину и удалить из нее пару ключ-значение\n        for pair in bucket:\n            if pair.key == key:\n                bucket.remove(pair)\n                self.size -= 1\n                break\n\n    def extend(self):\n        \"\"\"Расширить хеш-таблицу\"\"\"\n        # Временно сохранить исходную хеш-таблицу\n        buckets = self.buckets\n        # Инициализация новой хеш-таблицы после расширения\n        self.capacity *= self.extend_ratio\n        self.buckets = [[] for _ in range(self.capacity)]\n        self.size = 0\n        # Перенести пары ключ-значение из исходной хеш-таблицы в новую\n        for bucket in buckets:\n            for pair in bucket:\n                self.put(pair.key, pair.val)\n\n    def print(self):\n        \"\"\"Вывести хеш-таблицу\"\"\"\n        for bucket in self.buckets:\n            res = []\n            for pair in bucket:\n                res.append(str(pair.key) + \" -> \" + pair.val)\n            print(res)\n
        hash_map_chaining.cpp
        /* Хеш-таблица с цепочками */\nclass HashMapChaining {\n  private:\n    int size;                       // Число пар ключ-значение\n    int capacity;                   // Вместимость хеш-таблицы\n    double loadThres;               // Порог коэффициента загрузки для запуска расширения\n    int extendRatio;                // Коэффициент расширения\n    vector<vector<Pair *>> buckets; // Массив корзин\n\n  public:\n    /* Конструктор */\n    HashMapChaining() : size(0), capacity(4), loadThres(2.0 / 3.0), extendRatio(2) {\n        buckets.resize(capacity);\n    }\n\n    /* Метод-деструктор */\n    ~HashMapChaining() {\n        for (auto &bucket : buckets) {\n            for (Pair *pair : bucket) {\n                // Освободить память\n                delete pair;\n            }\n        }\n    }\n\n    /* Хеш-функция */\n    int hashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* Коэффициент загрузки */\n    double loadFactor() {\n        return (double)size / (double)capacity;\n    }\n\n    /* Операция поиска */\n    string get(int key) {\n        int index = hashFunc(key);\n        // Обойти корзину; если найден key, вернуть соответствующее val\n        for (Pair *pair : buckets[index]) {\n            if (pair->key == key) {\n                return pair->val;\n            }\n        }\n        // Если key не найден, вернуть пустую строку\n        return \"\";\n    }\n\n    /* Операция добавления */\n    void put(int key, string val) {\n        // Когда коэффициент загрузки превышает порог, выполнить расширение\n        if (loadFactor() > loadThres) {\n            extend();\n        }\n        int index = hashFunc(key);\n        // Обойти корзину; если встретился указанный key, обновить соответствующее val и вернуть\n        for (Pair *pair : buckets[index]) {\n            if (pair->key == key) {\n                pair->val = val;\n                return;\n            }\n        }\n        // Если такого key нет, добавить пару ключ-значение в конец\n        buckets[index].push_back(new Pair(key, val));\n        size++;\n    }\n\n    /* Операция удаления */\n    void remove(int key) {\n        int index = hashFunc(key);\n        auto &bucket = buckets[index];\n        // Обойти корзину и удалить из нее пару ключ-значение\n        for (int i = 0; i < bucket.size(); i++) {\n            if (bucket[i]->key == key) {\n                Pair *tmp = bucket[i];\n                bucket.erase(bucket.begin() + i); // Удалить из него пару ключ-значение\n                delete tmp;                       // Освободить память\n                size--;\n                return;\n            }\n        }\n    }\n\n    /* Расширить хеш-таблицу */\n    void extend() {\n        // Временно сохранить исходную хеш-таблицу\n        vector<vector<Pair *>> bucketsTmp = buckets;\n        // Инициализация новой хеш-таблицы после расширения\n        capacity *= extendRatio;\n        buckets.clear();\n        buckets.resize(capacity);\n        size = 0;\n        // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n        for (auto &bucket : bucketsTmp) {\n            for (Pair *pair : bucket) {\n                put(pair->key, pair->val);\n                // Освободить память\n                delete pair;\n            }\n        }\n    }\n\n    /* Вывести хеш-таблицу */\n    void print() {\n        for (auto &bucket : buckets) {\n            cout << \"[\";\n            for (Pair *pair : bucket) {\n                cout << pair->key << \" -> \" << pair->val << \", \";\n            }\n            cout << \"]\\n\";\n        }\n    }\n};\n
        hash_map_chaining.java
        /* Хеш-таблица с цепочками */\nclass HashMapChaining {\n    int size; // Число пар ключ-значение\n    int capacity; // Вместимость хеш-таблицы\n    double loadThres; // Порог коэффициента загрузки для запуска расширения\n    int extendRatio; // Коэффициент расширения\n    List<List<Pair>> buckets; // Массив корзин\n\n    /* Конструктор */\n    public HashMapChaining() {\n        size = 0;\n        capacity = 4;\n        loadThres = 2.0 / 3.0;\n        extendRatio = 2;\n        buckets = new ArrayList<>(capacity);\n        for (int i = 0; i < capacity; i++) {\n            buckets.add(new ArrayList<>());\n        }\n    }\n\n    /* Хеш-функция */\n    int hashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* Коэффициент загрузки */\n    double loadFactor() {\n        return (double) size / capacity;\n    }\n\n    /* Операция поиска */\n    String get(int key) {\n        int index = hashFunc(key);\n        List<Pair> bucket = buckets.get(index);\n        // Обойти корзину; если найден key, вернуть соответствующее val\n        for (Pair pair : bucket) {\n            if (pair.key == key) {\n                return pair.val;\n            }\n        }\n        // Если key не найден, вернуть null\n        return null;\n    }\n\n    /* Операция добавления */\n    void put(int key, String val) {\n        // Когда коэффициент загрузки превышает порог, выполнить расширение\n        if (loadFactor() > loadThres) {\n            extend();\n        }\n        int index = hashFunc(key);\n        List<Pair> bucket = buckets.get(index);\n        // Обойти корзину; если встретился указанный key, обновить соответствующее val и вернуть\n        for (Pair pair : bucket) {\n            if (pair.key == key) {\n                pair.val = val;\n                return;\n            }\n        }\n        // Если такого key нет, добавить пару ключ-значение в конец\n        Pair pair = new Pair(key, val);\n        bucket.add(pair);\n        size++;\n    }\n\n    /* Операция удаления */\n    void remove(int key) {\n        int index = hashFunc(key);\n        List<Pair> bucket = buckets.get(index);\n        // Обойти корзину и удалить из нее пару ключ-значение\n        for (Pair pair : bucket) {\n            if (pair.key == key) {\n                bucket.remove(pair);\n                size--;\n                break;\n            }\n        }\n    }\n\n    /* Расширить хеш-таблицу */\n    void extend() {\n        // Временно сохранить исходную хеш-таблицу\n        List<List<Pair>> bucketsTmp = buckets;\n        // Инициализация новой хеш-таблицы после расширения\n        capacity *= extendRatio;\n        buckets = new ArrayList<>(capacity);\n        for (int i = 0; i < capacity; i++) {\n            buckets.add(new ArrayList<>());\n        }\n        size = 0;\n        // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n        for (List<Pair> bucket : bucketsTmp) {\n            for (Pair pair : bucket) {\n                put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Вывести хеш-таблицу */\n    void print() {\n        for (List<Pair> bucket : buckets) {\n            List<String> res = new ArrayList<>();\n            for (Pair pair : bucket) {\n                res.add(pair.key + \" -> \" + pair.val);\n            }\n            System.out.println(res);\n        }\n    }\n}\n
        hash_map_chaining.cs
        /* Хеш-таблица с цепочками */\nclass HashMapChaining {\n    int size; // Число пар ключ-значение\n    int capacity; // Вместимость хеш-таблицы\n    double loadThres; // Порог коэффициента загрузки для запуска расширения\n    int extendRatio; // Коэффициент расширения\n    List<List<Pair>> buckets; // Массив корзин\n\n    /* Конструктор */\n    public HashMapChaining() {\n        size = 0;\n        capacity = 4;\n        loadThres = 2.0 / 3.0;\n        extendRatio = 2;\n        buckets = new List<List<Pair>>(capacity);\n        for (int i = 0; i < capacity; i++) {\n            buckets.Add([]);\n        }\n    }\n\n    /* Хеш-функция */\n    int HashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* Коэффициент загрузки */\n    double LoadFactor() {\n        return (double)size / capacity;\n    }\n\n    /* Операция поиска */\n    public string? Get(int key) {\n        int index = HashFunc(key);\n        // Обойти корзину; если найден key, вернуть соответствующее val\n        foreach (Pair pair in buckets[index]) {\n            if (pair.key == key) {\n                return pair.val;\n            }\n        }\n        // Если key не найден, вернуть null\n        return null;\n    }\n\n    /* Операция добавления */\n    public void Put(int key, string val) {\n        // Когда коэффициент загрузки превышает порог, выполнить расширение\n        if (LoadFactor() > loadThres) {\n            Extend();\n        }\n        int index = HashFunc(key);\n        // Обойти корзину; если встретился указанный key, обновить соответствующее val и вернуть\n        foreach (Pair pair in buckets[index]) {\n            if (pair.key == key) {\n                pair.val = val;\n                return;\n            }\n        }\n        // Если такого key нет, добавить пару ключ-значение в конец\n        buckets[index].Add(new Pair(key, val));\n        size++;\n    }\n\n    /* Операция удаления */\n    public void Remove(int key) {\n        int index = HashFunc(key);\n        // Обойти корзину и удалить из нее пару ключ-значение\n        foreach (Pair pair in buckets[index].ToList()) {\n            if (pair.key == key) {\n                buckets[index].Remove(pair);\n                size--;\n                break;\n            }\n        }\n    }\n\n    /* Расширить хеш-таблицу */\n    void Extend() {\n        // Временно сохранить исходную хеш-таблицу\n        List<List<Pair>> bucketsTmp = buckets;\n        // Инициализация новой хеш-таблицы после расширения\n        capacity *= extendRatio;\n        buckets = new List<List<Pair>>(capacity);\n        for (int i = 0; i < capacity; i++) {\n            buckets.Add([]);\n        }\n        size = 0;\n        // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n        foreach (List<Pair> bucket in bucketsTmp) {\n            foreach (Pair pair in bucket) {\n                Put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Вывести хеш-таблицу */\n    public void Print() {\n        foreach (List<Pair> bucket in buckets) {\n            List<string> res = [];\n            foreach (Pair pair in bucket) {\n                res.Add(pair.key + \" -> \" + pair.val);\n            }\n            foreach (string kv in res) {\n                Console.WriteLine(kv);\n            }\n        }\n    }\n}\n
        hash_map_chaining.go
        /* Хеш-таблица с цепочками */\ntype hashMapChaining struct {\n    size        int      // Число пар ключ-значение\n    capacity    int      // Вместимость хеш-таблицы\n    loadThres   float64  // Порог коэффициента загрузки для запуска расширения\n    extendRatio int      // Коэффициент расширения\n    buckets     [][]pair // Массив корзин\n}\n\n/* Конструктор */\nfunc newHashMapChaining() *hashMapChaining {\n    buckets := make([][]pair, 4)\n    for i := 0; i < 4; i++ {\n        buckets[i] = make([]pair, 0)\n    }\n    return &hashMapChaining{\n        size:        0,\n        capacity:    4,\n        loadThres:   2.0 / 3.0,\n        extendRatio: 2,\n        buckets:     buckets,\n    }\n}\n\n/* Хеш-функция */\nfunc (m *hashMapChaining) hashFunc(key int) int {\n    return key % m.capacity\n}\n\n/* Коэффициент загрузки */\nfunc (m *hashMapChaining) loadFactor() float64 {\n    return float64(m.size) / float64(m.capacity)\n}\n\n/* Операция поиска */\nfunc (m *hashMapChaining) get(key int) string {\n    idx := m.hashFunc(key)\n    bucket := m.buckets[idx]\n    // Обойти корзину; если найден key, вернуть соответствующее val\n    for _, p := range bucket {\n        if p.key == key {\n            return p.val\n        }\n    }\n    // Если key не найден, вернуть пустую строку\n    return \"\"\n}\n\n/* Операция добавления */\nfunc (m *hashMapChaining) put(key int, val string) {\n    // Когда коэффициент загрузки превышает порог, выполнить расширение\n    if m.loadFactor() > m.loadThres {\n        m.extend()\n    }\n    idx := m.hashFunc(key)\n    // Обойти корзину; если встретился указанный key, обновить соответствующее val и вернуть\n    for i := range m.buckets[idx] {\n        if m.buckets[idx][i].key == key {\n            m.buckets[idx][i].val = val\n            return\n        }\n    }\n    // Если такого key нет, добавить пару ключ-значение в конец\n    p := pair{\n        key: key,\n        val: val,\n    }\n    m.buckets[idx] = append(m.buckets[idx], p)\n    m.size += 1\n}\n\n/* Операция удаления */\nfunc (m *hashMapChaining) remove(key int) {\n    idx := m.hashFunc(key)\n    // Обойти корзину и удалить из нее пару ключ-значение\n    for i, p := range m.buckets[idx] {\n        if p.key == key {\n            // Удаление из среза\n            m.buckets[idx] = append(m.buckets[idx][:i], m.buckets[idx][i+1:]...)\n            m.size -= 1\n            break\n        }\n    }\n}\n\n/* Расширить хеш-таблицу */\nfunc (m *hashMapChaining) extend() {\n    // Временно сохранить исходную хеш-таблицу\n    tmpBuckets := make([][]pair, len(m.buckets))\n    for i := 0; i < len(m.buckets); i++ {\n        tmpBuckets[i] = make([]pair, len(m.buckets[i]))\n        copy(tmpBuckets[i], m.buckets[i])\n    }\n    // Инициализация новой хеш-таблицы после расширения\n    m.capacity *= m.extendRatio\n    m.buckets = make([][]pair, m.capacity)\n    for i := 0; i < m.capacity; i++ {\n        m.buckets[i] = make([]pair, 0)\n    }\n    m.size = 0\n    // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n    for _, bucket := range tmpBuckets {\n        for _, p := range bucket {\n            m.put(p.key, p.val)\n        }\n    }\n}\n\n/* Вывести хеш-таблицу */\nfunc (m *hashMapChaining) print() {\n    var builder strings.Builder\n\n    for _, bucket := range m.buckets {\n        builder.WriteString(\"[\")\n        for _, p := range bucket {\n            builder.WriteString(strconv.Itoa(p.key) + \" -> \" + p.val + \" \")\n        }\n        builder.WriteString(\"]\")\n        fmt.Println(builder.String())\n        builder.Reset()\n    }\n}\n
        hash_map_chaining.swift
        /* Хеш-таблица с цепочками */\nclass HashMapChaining {\n    var size: Int // Число пар ключ-значение\n    var capacity: Int // Вместимость хеш-таблицы\n    var loadThres: Double // Порог коэффициента загрузки для запуска расширения\n    var extendRatio: Int // Коэффициент расширения\n    var buckets: [[Pair]] // Массив корзин\n\n    /* Конструктор */\n    init() {\n        size = 0\n        capacity = 4\n        loadThres = 2.0 / 3.0\n        extendRatio = 2\n        buckets = Array(repeating: [], count: capacity)\n    }\n\n    /* Хеш-функция */\n    func hashFunc(key: Int) -> Int {\n        key % capacity\n    }\n\n    /* Коэффициент загрузки */\n    func loadFactor() -> Double {\n        Double(size) / Double(capacity)\n    }\n\n    /* Операция поиска */\n    func get(key: Int) -> String? {\n        let index = hashFunc(key: key)\n        let bucket = buckets[index]\n        // Обойти корзину; если найден key, вернуть соответствующее val\n        for pair in bucket {\n            if pair.key == key {\n                return pair.val\n            }\n        }\n        // Если key не найден, вернуть nil\n        return nil\n    }\n\n    /* Операция добавления */\n    func put(key: Int, val: String) {\n        // Когда коэффициент загрузки превышает порог, выполнить расширение\n        if loadFactor() > loadThres {\n            extend()\n        }\n        let index = hashFunc(key: key)\n        let bucket = buckets[index]\n        // Обойти корзину; если встретился указанный key, обновить соответствующее val и вернуть\n        for pair in bucket {\n            if pair.key == key {\n                pair.val = val\n                return\n            }\n        }\n        // Если такого key нет, добавить пару ключ-значение в конец\n        let pair = Pair(key: key, val: val)\n        buckets[index].append(pair)\n        size += 1\n    }\n\n    /* Операция удаления */\n    func remove(key: Int) {\n        let index = hashFunc(key: key)\n        let bucket = buckets[index]\n        // Обойти корзину и удалить из нее пару ключ-значение\n        for (pairIndex, pair) in bucket.enumerated() {\n            if pair.key == key {\n                buckets[index].remove(at: pairIndex)\n                size -= 1\n                break\n            }\n        }\n    }\n\n    /* Расширить хеш-таблицу */\n    func extend() {\n        // Временно сохранить исходную хеш-таблицу\n        let bucketsTmp = buckets\n        // Инициализация новой хеш-таблицы после расширения\n        capacity *= extendRatio\n        buckets = Array(repeating: [], count: capacity)\n        size = 0\n        // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n        for bucket in bucketsTmp {\n            for pair in bucket {\n                put(key: pair.key, val: pair.val)\n            }\n        }\n    }\n\n    /* Вывести хеш-таблицу */\n    func print() {\n        for bucket in buckets {\n            let res = bucket.map { \"\\($0.key) -> \\($0.val)\" }\n            Swift.print(res)\n        }\n    }\n}\n
        hash_map_chaining.js
        /* Хеш-таблица с цепочками */\nclass HashMapChaining {\n    #size; // Число пар ключ-значение\n    #capacity; // Вместимость хеш-таблицы\n    #loadThres; // Порог коэффициента загрузки для запуска расширения\n    #extendRatio; // Коэффициент расширения\n    #buckets; // Массив корзин\n\n    /* Конструктор */\n    constructor() {\n        this.#size = 0;\n        this.#capacity = 4;\n        this.#loadThres = 2.0 / 3.0;\n        this.#extendRatio = 2;\n        this.#buckets = new Array(this.#capacity).fill(null).map((x) => []);\n    }\n\n    /* Хеш-функция */\n    #hashFunc(key) {\n        return key % this.#capacity;\n    }\n\n    /* Коэффициент загрузки */\n    #loadFactor() {\n        return this.#size / this.#capacity;\n    }\n\n    /* Операция поиска */\n    get(key) {\n        const index = this.#hashFunc(key);\n        const bucket = this.#buckets[index];\n        // Обойти корзину; если найден key, вернуть соответствующее val\n        for (const pair of bucket) {\n            if (pair.key === key) {\n                return pair.val;\n            }\n        }\n        // Если key не найден, вернуть null\n        return null;\n    }\n\n    /* Операция добавления */\n    put(key, val) {\n        // Когда коэффициент загрузки превышает порог, выполнить расширение\n        if (this.#loadFactor() > this.#loadThres) {\n            this.#extend();\n        }\n        const index = this.#hashFunc(key);\n        const bucket = this.#buckets[index];\n        // Обойти корзину; если встретился указанный key, обновить соответствующее val и вернуть\n        for (const pair of bucket) {\n            if (pair.key === key) {\n                pair.val = val;\n                return;\n            }\n        }\n        // Если такого key нет, добавить пару ключ-значение в конец\n        const pair = new Pair(key, val);\n        bucket.push(pair);\n        this.#size++;\n    }\n\n    /* Операция удаления */\n    remove(key) {\n        const index = this.#hashFunc(key);\n        let bucket = this.#buckets[index];\n        // Обойти корзину и удалить из нее пару ключ-значение\n        for (let i = 0; i < bucket.length; i++) {\n            if (bucket[i].key === key) {\n                bucket.splice(i, 1);\n                this.#size--;\n                break;\n            }\n        }\n    }\n\n    /* Расширить хеш-таблицу */\n    #extend() {\n        // Временно сохранить исходную хеш-таблицу\n        const bucketsTmp = this.#buckets;\n        // Инициализация новой хеш-таблицы после расширения\n        this.#capacity *= this.#extendRatio;\n        this.#buckets = new Array(this.#capacity).fill(null).map((x) => []);\n        this.#size = 0;\n        // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n        for (const bucket of bucketsTmp) {\n            for (const pair of bucket) {\n                this.put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Вывести хеш-таблицу */\n    print() {\n        for (const bucket of this.#buckets) {\n            let res = [];\n            for (const pair of bucket) {\n                res.push(pair.key + ' -> ' + pair.val);\n            }\n            console.log(res);\n        }\n    }\n}\n
        hash_map_chaining.ts
        /* Хеш-таблица с цепочками */\nclass HashMapChaining {\n    #size: number; // Число пар ключ-значение\n    #capacity: number; // Вместимость хеш-таблицы\n    #loadThres: number; // Порог коэффициента загрузки для запуска расширения\n    #extendRatio: number; // Коэффициент расширения\n    #buckets: Pair[][]; // Массив корзин\n\n    /* Конструктор */\n    constructor() {\n        this.#size = 0;\n        this.#capacity = 4;\n        this.#loadThres = 2.0 / 3.0;\n        this.#extendRatio = 2;\n        this.#buckets = new Array(this.#capacity).fill(null).map((x) => []);\n    }\n\n    /* Хеш-функция */\n    #hashFunc(key: number): number {\n        return key % this.#capacity;\n    }\n\n    /* Коэффициент загрузки */\n    #loadFactor(): number {\n        return this.#size / this.#capacity;\n    }\n\n    /* Операция поиска */\n    get(key: number): string | null {\n        const index = this.#hashFunc(key);\n        const bucket = this.#buckets[index];\n        // Обойти корзину; если найден key, вернуть соответствующее val\n        for (const pair of bucket) {\n            if (pair.key === key) {\n                return pair.val;\n            }\n        }\n        // Если key не найден, вернуть null\n        return null;\n    }\n\n    /* Операция добавления */\n    put(key: number, val: string): void {\n        // Когда коэффициент загрузки превышает порог, выполнить расширение\n        if (this.#loadFactor() > this.#loadThres) {\n            this.#extend();\n        }\n        const index = this.#hashFunc(key);\n        const bucket = this.#buckets[index];\n        // Обойти корзину; если встретился указанный key, обновить соответствующее val и вернуть\n        for (const pair of bucket) {\n            if (pair.key === key) {\n                pair.val = val;\n                return;\n            }\n        }\n        // Если такого key нет, добавить пару ключ-значение в конец\n        const pair = new Pair(key, val);\n        bucket.push(pair);\n        this.#size++;\n    }\n\n    /* Операция удаления */\n    remove(key: number): void {\n        const index = this.#hashFunc(key);\n        let bucket = this.#buckets[index];\n        // Обойти корзину и удалить из нее пару ключ-значение\n        for (let i = 0; i < bucket.length; i++) {\n            if (bucket[i].key === key) {\n                bucket.splice(i, 1);\n                this.#size--;\n                break;\n            }\n        }\n    }\n\n    /* Расширить хеш-таблицу */\n    #extend(): void {\n        // Временно сохранить исходную хеш-таблицу\n        const bucketsTmp = this.#buckets;\n        // Инициализация новой хеш-таблицы после расширения\n        this.#capacity *= this.#extendRatio;\n        this.#buckets = new Array(this.#capacity).fill(null).map((x) => []);\n        this.#size = 0;\n        // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n        for (const bucket of bucketsTmp) {\n            for (const pair of bucket) {\n                this.put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Вывести хеш-таблицу */\n    print(): void {\n        for (const bucket of this.#buckets) {\n            let res = [];\n            for (const pair of bucket) {\n                res.push(pair.key + ' -> ' + pair.val);\n            }\n            console.log(res);\n        }\n    }\n}\n
        hash_map_chaining.dart
        /* Хеш-таблица с цепочками */\nclass HashMapChaining {\n  late int size; // Число пар ключ-значение\n  late int capacity; // Вместимость хеш-таблицы\n  late double loadThres; // Порог коэффициента загрузки для запуска расширения\n  late int extendRatio; // Коэффициент расширения\n  late List<List<Pair>> buckets; // Массив корзин\n\n  /* Конструктор */\n  HashMapChaining() {\n    size = 0;\n    capacity = 4;\n    loadThres = 2.0 / 3.0;\n    extendRatio = 2;\n    buckets = List.generate(capacity, (_) => []);\n  }\n\n  /* Хеш-функция */\n  int hashFunc(int key) {\n    return key % capacity;\n  }\n\n  /* Коэффициент загрузки */\n  double loadFactor() {\n    return size / capacity;\n  }\n\n  /* Операция поиска */\n  String? get(int key) {\n    int index = hashFunc(key);\n    List<Pair> bucket = buckets[index];\n    // Обойти корзину; если найден key, вернуть соответствующее val\n    for (Pair pair in bucket) {\n      if (pair.key == key) {\n        return pair.val;\n      }\n    }\n    // Если key не найден, вернуть null\n    return null;\n  }\n\n  /* Операция добавления */\n  void put(int key, String val) {\n    // Когда коэффициент загрузки превышает порог, выполнить расширение\n    if (loadFactor() > loadThres) {\n      extend();\n    }\n    int index = hashFunc(key);\n    List<Pair> bucket = buckets[index];\n    // Обойти корзину; если встретился указанный key, обновить соответствующее val и вернуть\n    for (Pair pair in bucket) {\n      if (pair.key == key) {\n        pair.val = val;\n        return;\n      }\n    }\n    // Если такого key нет, добавить пару ключ-значение в конец\n    Pair pair = Pair(key, val);\n    bucket.add(pair);\n    size++;\n  }\n\n  /* Операция удаления */\n  void remove(int key) {\n    int index = hashFunc(key);\n    List<Pair> bucket = buckets[index];\n    // Обойти корзину и удалить из нее пару ключ-значение\n    for (Pair pair in bucket) {\n      if (pair.key == key) {\n        bucket.remove(pair);\n        size--;\n        break;\n      }\n    }\n  }\n\n  /* Расширить хеш-таблицу */\n  void extend() {\n    // Временно сохранить исходную хеш-таблицу\n    List<List<Pair>> bucketsTmp = buckets;\n    // Инициализация новой хеш-таблицы после расширения\n    capacity *= extendRatio;\n    buckets = List.generate(capacity, (_) => []);\n    size = 0;\n    // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n    for (List<Pair> bucket in bucketsTmp) {\n      for (Pair pair in bucket) {\n        put(pair.key, pair.val);\n      }\n    }\n  }\n\n  /* Вывести хеш-таблицу */\n  void printHashMap() {\n    for (List<Pair> bucket in buckets) {\n      List<String> res = [];\n      for (Pair pair in bucket) {\n        res.add(\"${pair.key} -> ${pair.val}\");\n      }\n      print(res);\n    }\n  }\n}\n
        hash_map_chaining.rs
        /* Хеш-таблица с цепочками */\nstruct HashMapChaining {\n    size: usize,\n    capacity: usize,\n    load_thres: f32,\n    extend_ratio: usize,\n    buckets: Vec<Vec<Pair>>,\n}\n\nimpl HashMapChaining {\n    /* Конструктор */\n    fn new() -> Self {\n        Self {\n            size: 0,\n            capacity: 4,\n            load_thres: 2.0 / 3.0,\n            extend_ratio: 2,\n            buckets: vec![vec![]; 4],\n        }\n    }\n\n    /* Хеш-функция */\n    fn hash_func(&self, key: i32) -> usize {\n        key as usize % self.capacity\n    }\n\n    /* Коэффициент загрузки */\n    fn load_factor(&self) -> f32 {\n        self.size as f32 / self.capacity as f32\n    }\n\n    /* Операция удаления */\n    fn remove(&mut self, key: i32) -> Option<String> {\n        let index = self.hash_func(key);\n\n        // Обойти корзину и удалить из нее пару ключ-значение\n        for (i, p) in self.buckets[index].iter_mut().enumerate() {\n            if p.key == key {\n                let pair = self.buckets[index].remove(i);\n                self.size -= 1;\n                return Some(pair.val);\n            }\n        }\n\n        // Если key не найден, вернуть None\n        None\n    }\n\n    /* Расширить хеш-таблицу */\n    fn extend(&mut self) {\n        // Временно сохранить исходную хеш-таблицу\n        let buckets_tmp = std::mem::take(&mut self.buckets);\n\n        // Инициализация новой хеш-таблицы после расширения\n        self.capacity *= self.extend_ratio;\n        self.buckets = vec![Vec::new(); self.capacity as usize];\n        self.size = 0;\n\n        // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n        for bucket in buckets_tmp {\n            for pair in bucket {\n                self.put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Вывести хеш-таблицу */\n    fn print(&self) {\n        for bucket in &self.buckets {\n            let mut res = Vec::new();\n            for pair in bucket {\n                res.push(format!(\"{} -> {}\", pair.key, pair.val));\n            }\n            println!(\"{:?}\", res);\n        }\n    }\n\n    /* Операция добавления */\n    fn put(&mut self, key: i32, val: String) {\n        // Когда коэффициент загрузки превышает порог, выполнить расширение\n        if self.load_factor() > self.load_thres {\n            self.extend();\n        }\n\n        let index = self.hash_func(key);\n\n        // Обойти корзину; если встретился указанный key, обновить соответствующее val и вернуть\n        for pair in self.buckets[index].iter_mut() {\n            if pair.key == key {\n                pair.val = val;\n                return;\n            }\n        }\n\n        // Если такого key нет, добавить пару ключ-значение в конец\n        let pair = Pair { key, val };\n        self.buckets[index].push(pair);\n        self.size += 1;\n    }\n\n    /* Операция поиска */\n    fn get(&self, key: i32) -> Option<&str> {\n        let index = self.hash_func(key);\n\n        // Обойти корзину; если найден key, вернуть соответствующее val\n        for pair in self.buckets[index].iter() {\n            if pair.key == key {\n                return Some(&pair.val);\n            }\n        }\n\n        // Если key не найден, вернуть None\n        None\n    }\n}\n
        hash_map_chaining.c
        /* Узел связного списка */\ntypedef struct Node {\n    Pair *pair;\n    struct Node *next;\n} Node;\n\n/* Хеш-таблица с цепочками */\ntypedef struct {\n    int size;         // Число пар ключ-значение\n    int capacity;     // Вместимость хеш-таблицы\n    double loadThres; // Порог коэффициента загрузки для запуска расширения\n    int extendRatio;  // Коэффициент расширения\n    Node **buckets;   // Массив корзин\n} HashMapChaining;\n\n/* Конструктор */\nHashMapChaining *newHashMapChaining() {\n    HashMapChaining *hashMap = (HashMapChaining *)malloc(sizeof(HashMapChaining));\n    hashMap->size = 0;\n    hashMap->capacity = 4;\n    hashMap->loadThres = 2.0 / 3.0;\n    hashMap->extendRatio = 2;\n    hashMap->buckets = (Node **)malloc(hashMap->capacity * sizeof(Node *));\n    for (int i = 0; i < hashMap->capacity; i++) {\n        hashMap->buckets[i] = NULL;\n    }\n    return hashMap;\n}\n\n/* Деструктор */\nvoid delHashMapChaining(HashMapChaining *hashMap) {\n    for (int i = 0; i < hashMap->capacity; i++) {\n        Node *cur = hashMap->buckets[i];\n        while (cur) {\n            Node *tmp = cur;\n            cur = cur->next;\n            free(tmp->pair);\n            free(tmp);\n        }\n    }\n    free(hashMap->buckets);\n    free(hashMap);\n}\n\n/* Хеш-функция */\nint hashFunc(HashMapChaining *hashMap, int key) {\n    return key % hashMap->capacity;\n}\n\n/* Коэффициент загрузки */\ndouble loadFactor(HashMapChaining *hashMap) {\n    return (double)hashMap->size / (double)hashMap->capacity;\n}\n\n/* Операция поиска */\nchar *get(HashMapChaining *hashMap, int key) {\n    int index = hashFunc(hashMap, key);\n    // Обойти корзину; если найден key, вернуть соответствующее val\n    Node *cur = hashMap->buckets[index];\n    while (cur) {\n        if (cur->pair->key == key) {\n            return cur->pair->val;\n        }\n        cur = cur->next;\n    }\n    return \"\"; // Если key не найден, вернуть пустую строку\n}\n\n/* Операция добавления */\nvoid put(HashMapChaining *hashMap, int key, const char *val) {\n    // Когда коэффициент загрузки превышает порог, выполнить расширение\n    if (loadFactor(hashMap) > hashMap->loadThres) {\n        extend(hashMap);\n    }\n    int index = hashFunc(hashMap, key);\n    // Обойти корзину; если встретился указанный key, обновить соответствующее val и вернуть\n    Node *cur = hashMap->buckets[index];\n    while (cur) {\n        if (cur->pair->key == key) {\n            strcpy(cur->pair->val, val); // Если встретился указанный key, обновить соответствующий val и вернуть\n            return;\n        }\n        cur = cur->next;\n    }\n    // Если такого key нет, добавить пару ключ-значение в голову связного списка\n    Pair *newPair = (Pair *)malloc(sizeof(Pair));\n    newPair->key = key;\n    strcpy(newPair->val, val);\n    Node *newNode = (Node *)malloc(sizeof(Node));\n    newNode->pair = newPair;\n    newNode->next = hashMap->buckets[index];\n    hashMap->buckets[index] = newNode;\n    hashMap->size++;\n}\n\n/* Расширить хеш-таблицу */\nvoid extend(HashMapChaining *hashMap) {\n    // Временно сохранить исходную хеш-таблицу\n    int oldCapacity = hashMap->capacity;\n    Node **oldBuckets = hashMap->buckets;\n    // Инициализация новой хеш-таблицы после расширения\n    hashMap->capacity *= hashMap->extendRatio;\n    hashMap->buckets = (Node **)malloc(hashMap->capacity * sizeof(Node *));\n    for (int i = 0; i < hashMap->capacity; i++) {\n        hashMap->buckets[i] = NULL;\n    }\n    hashMap->size = 0;\n    // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n    for (int i = 0; i < oldCapacity; i++) {\n        Node *cur = oldBuckets[i];\n        while (cur) {\n            put(hashMap, cur->pair->key, cur->pair->val);\n            Node *temp = cur;\n            cur = cur->next;\n            // Освободить память\n            free(temp->pair);\n            free(temp);\n        }\n    }\n\n    free(oldBuckets);\n}\n\n/* Операция удаления */\nvoid removeItem(HashMapChaining *hashMap, int key) {\n    int index = hashFunc(hashMap, key);\n    Node *cur = hashMap->buckets[index];\n    Node *pre = NULL;\n    while (cur) {\n        if (cur->pair->key == key) {\n            // Удалить из него пару ключ-значение\n            if (pre) {\n                pre->next = cur->next;\n            } else {\n                hashMap->buckets[index] = cur->next;\n            }\n            // Освободить память\n            free(cur->pair);\n            free(cur);\n            hashMap->size--;\n            return;\n        }\n        pre = cur;\n        cur = cur->next;\n    }\n}\n\n/* Вывести хеш-таблицу */\nvoid print(HashMapChaining *hashMap) {\n    for (int i = 0; i < hashMap->capacity; i++) {\n        Node *cur = hashMap->buckets[i];\n        printf(\"[\");\n        while (cur) {\n            printf(\"%d -> %s, \", cur->pair->key, cur->pair->val);\n            cur = cur->next;\n        }\n        printf(\"]\\n\");\n    }\n}\n
        hash_map_chaining.kt
        /* Хеш-таблица с цепочками */\nclass HashMapChaining {\n    var size: Int // Число пар ключ-значение\n    var capacity: Int // Вместимость хеш-таблицы\n    val loadThres: Double // Порог коэффициента загрузки для запуска расширения\n    val extendRatio: Int // Коэффициент расширения\n    var buckets: MutableList<MutableList<Pair>> // Массив корзин\n\n    /* Конструктор */\n    init {\n        size = 0\n        capacity = 4\n        loadThres = 2.0 / 3.0\n        extendRatio = 2\n        buckets = mutableListOf()\n        for (i in 0..<capacity) {\n            buckets.add(mutableListOf())\n        }\n    }\n\n    /* Хеш-функция */\n    fun hashFunc(key: Int): Int {\n        return key % capacity\n    }\n\n    /* Коэффициент загрузки */\n    fun loadFactor(): Double {\n        return (size / capacity).toDouble()\n    }\n\n    /* Операция поиска */\n    fun get(key: Int): String? {\n        val index = hashFunc(key)\n        val bucket = buckets[index]\n        // Обойти корзину; если найден key, вернуть соответствующее val\n        for (pair in bucket) {\n            if (pair.key == key) return pair._val\n        }\n        // Если key не найден, вернуть null\n        return null\n    }\n\n    /* Операция добавления */\n    fun put(key: Int, _val: String) {\n        // Когда коэффициент загрузки превышает порог, выполнить расширение\n        if (loadFactor() > loadThres) {\n            extend()\n        }\n        val index = hashFunc(key)\n        val bucket = buckets[index]\n        // Обойти корзину; если встретился указанный key, обновить соответствующее val и вернуть\n        for (pair in bucket) {\n            if (pair.key == key) {\n                pair._val = _val\n                return\n            }\n        }\n        // Если такого key нет, добавить пару ключ-значение в конец\n        val pair = Pair(key, _val)\n        bucket.add(pair)\n        size++\n    }\n\n    /* Операция удаления */\n    fun remove(key: Int) {\n        val index = hashFunc(key)\n        val bucket = buckets[index]\n        // Обойти корзину и удалить из нее пару ключ-значение\n        for (pair in bucket) {\n            if (pair.key == key) {\n                bucket.remove(pair)\n                size--\n                break\n            }\n        }\n    }\n\n    /* Расширить хеш-таблицу */\n    fun extend() {\n        // Временно сохранить исходную хеш-таблицу\n        val bucketsTmp = buckets\n        // Инициализация новой хеш-таблицы после расширения\n        capacity *= extendRatio\n        // mutablelist не имеет фиксированного размера\n        buckets = mutableListOf()\n        for (i in 0..<capacity) {\n            buckets.add(mutableListOf())\n        }\n        size = 0\n        // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n        for (bucket in bucketsTmp) {\n            for (pair in bucket) {\n                put(pair.key, pair._val)\n            }\n        }\n    }\n\n    /* Вывести хеш-таблицу */\n    fun print() {\n        for (bucket in buckets) {\n            val res = mutableListOf<String>()\n            for (pair in bucket) {\n                val k = pair.key\n                val v = pair._val\n                res.add(\"$k -> $v\")\n            }\n            println(res)\n        }\n    }\n}\n
        hash_map_chaining.rb
        ### Хеш-таблица с цепочками ###\nclass HashMapChaining\n  ### Конструктор ###\n  def initialize\n    @size = 0 # Число пар ключ-значение\n    @capacity = 4 # Вместимость хеш-таблицы\n    @load_thres = 2.0 / 3.0 # Порог коэффициента загрузки для запуска расширения\n    @extend_ratio = 2 # Коэффициент расширения\n    @buckets = Array.new(@capacity) { [] } # Массив корзин\n  end\n\n  ### Хеш-функция ###\n  def hash_func(key)\n    key % @capacity\n  end\n\n  ### Коэффициент загрузки ###\n  def load_factor\n    @size / @capacity\n  end\n\n  ### Операция поиска ###\n  def get(key)\n    index = hash_func(key)\n    bucket = @buckets[index]\n    # Обойти корзину; если найден key, вернуть соответствующее val\n    for pair in bucket\n      return pair.val if pair.key == key\n    end\n    # Если key не найден, вернуть nil\n    nil\n  end\n\n  ### Операция добавления ###\n  def put(key, val)\n    # Когда коэффициент загрузки превышает порог, выполнить расширение\n    extend if load_factor > @load_thres\n    index = hash_func(key)\n    bucket = @buckets[index]\n    # Обойти корзину; если встретился указанный key, обновить соответствующее val и вернуть\n    for pair in bucket\n      if pair.key == key\n        pair.val = val\n        return\n      end\n    end\n    # Если такого key нет, добавить пару ключ-значение в конец\n    pair = Pair.new(key, val)\n    bucket << pair\n    @size += 1\n  end\n\n  ### Операция удаления ###\n  def remove(key)\n    index = hash_func(key)\n    bucket = @buckets[index]\n    # Обойти корзину и удалить из нее пару ключ-значение\n    for pair in bucket\n      if pair.key == key\n        bucket.delete(pair)\n        @size -= 1\n        break\n      end\n    end\n  end\n\n  ### Расширение хеш-таблицы ###\n  def extend\n    # Временно сохранить исходную хеш-таблицу\n    buckets = @buckets\n    # Инициализация новой хеш-таблицы после расширения\n    @capacity *= @extend_ratio\n    @buckets = Array.new(@capacity) { [] }\n    @size = 0\n    # Перенести пары ключ-значение из исходной хеш-таблицы в новую\n    for bucket in buckets\n      for pair in bucket\n        put(pair.key, pair.val)\n      end\n    end\n  end\n\n  ### Вывести хеш-таблицу ###\n  def print\n    for bucket in @buckets\n      res = []\n      for pair in bucket\n        res << \"#{pair.key} -> #{pair.val}\"\n      end\n      pp res\n    end\n  end\nend\n
        Визуализация кода

        Во весь экран >

        Следует отметить, что когда связный список становится очень длинным, эффективность поиска \\(O(n)\\) оказывается низкой. В этом случае список можно преобразовать в AVL-дерево или красно-черное дерево , чтобы оптимизировать временную сложность поиска до \\(O(\\log n)\\) .

        ","path":["Глава 6. Хеш-таблицы","6.2   Хеш-коллизии"],"tags":[]},{"location":"chapter_hashing/hash_collision/#622","level":2,"title":"6.2.2   Открытая адресация","text":"

        Открытая адресация (open addressing) не вводит дополнительных структур данных, а обрабатывает хеш-коллизии с помощью многократного пробирования. Основные варианты пробирования включают линейное пробирование, квадратичное пробирование и повторное хеширование.

        Ниже на примере линейного пробирования рассмотрим механизм работы хеш-таблицы с открытой адресацией.

        ","path":["Глава 6. Хеш-таблицы","6.2   Хеш-коллизии"],"tags":[]},{"location":"chapter_hashing/hash_collision/#1","level":3,"title":"1.   Линейное пробирование","text":"

        Линейное пробирование использует линейный поиск с фиксированным шагом. Его методы работы отличаются от обычной хеш-таблицы.

        • Вставка элемента: по хеш-функции вычисляется индекс бакета. Если бакет уже занят, то от места конфликта выполняется линейный обход вперед (шаг обычно равен \\(1\\) ), пока не будет найден пустой бакет, после чего элемент вставляется туда.
        • Поиск элемента: если возник конфликт, то с тем же шагом продолжается линейный обход вперед, пока не будет найден целевой элемент и возвращено value. Если встречается пустой бакет, это означает, что искомого элемента в хеш-таблице нет, и возвращается None .

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

        Рисунок 6-6   Распределение пар ключ-значение в хеш-таблице с открытой адресацией (линейное пробирование)

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

        Стоит заметить, что мы не можем напрямую удалять элементы из хеш-таблицы с открытой адресацией. Причина в том, что удаление создаст внутри массива пустой бакет None , а при поиске элемента линейное пробирование остановится на этом пустом бакете и вернет результат, из-за чего элементы ниже этого бакета уже не смогут быть найдены, и программа может ошибочно посчитать, что их не существует, как показано на рисунке 6-7.

        Рисунок 6-7   Проблема поиска после удаления элемента в открытой адресации

        Чтобы решить эту проблему, можно использовать механизм ленивого удаления (lazy deletion): он не удаляет элемент из хеш-таблицы напрямую, **а помечает этот бакет специальной константой TOMBSTONE **. В этом механизме и None , и TOMBSTONE означают пустой бакет, и оба могут быть использованы для размещения пары ключ-значение. Но есть важное различие: при линейном пробировании, встретив TOMBSTONE , нужно продолжать обход, потому что ниже него все еще могут существовать пары ключ-значение.

        Однако ленивое удаление может ускорять деградацию производительности хеш-таблицы. Это связано с тем, что каждая операция удаления создает новую метку удаления. По мере роста числа TOMBSTONE время поиска тоже увеличивается, потому что линейное пробирование может быть вынуждено перескакивать через множество TOMBSTONE , прежде чем найдет целевой элемент.

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

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

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map_open_addressing.py
        class HashMapOpenAddressing:\n    \"\"\"Хеш-таблица с открытой адресацией\"\"\"\n\n    def __init__(self):\n        \"\"\"Конструктор\"\"\"\n        self.size = 0  # Число пар ключ-значение\n        self.capacity = 4  # Вместимость хеш-таблицы\n        self.load_thres = 2.0 / 3.0  # Порог коэффициента загрузки для запуска расширения\n        self.extend_ratio = 2  # Коэффициент расширения\n        self.buckets: list[Pair | None] = [None] * self.capacity  # Массив корзин\n        self.TOMBSTONE = Pair(-1, \"-1\")  # Удалить метку\n\n    def hash_func(self, key: int) -> int:\n        \"\"\"Хеш-функция\"\"\"\n        return key % self.capacity\n\n    def load_factor(self) -> float:\n        \"\"\"Коэффициент загрузки\"\"\"\n        return self.size / self.capacity\n\n    def find_bucket(self, key: int) -> int:\n        \"\"\"Найти индекс корзины, соответствующий key\"\"\"\n        index = self.hash_func(key)\n        first_tombstone = -1\n        # Выполнять линейное пробирование и завершить при встрече с пустой корзиной\n        while self.buckets[index] is not None:\n            # Если встретился key, вернуть соответствующий индекс корзины\n            if self.buckets[index].key == key:\n                # Если ранее встретилась метка удаления, переместить пару ключ-значение на этот индекс\n                if first_tombstone != -1:\n                    self.buckets[first_tombstone] = self.buckets[index]\n                    self.buckets[index] = self.TOMBSTONE\n                    return first_tombstone  # Вернуть индекс корзины после перемещения\n                return index  # Вернуть индекс корзины\n            # Записать первую встретившуюся метку удаления\n            if first_tombstone == -1 and self.buckets[index] is self.TOMBSTONE:\n                first_tombstone = index\n            # Вычислить индекс корзины; при выходе за конец вернуться к началу\n            index = (index + 1) % self.capacity\n        # Если key не существует, вернуть индекс точки добавления\n        return index if first_tombstone == -1 else first_tombstone\n\n    def get(self, key: int) -> str:\n        \"\"\"Операция поиска\"\"\"\n        # Найти индекс корзины, соответствующий key\n        index = self.find_bucket(key)\n        # Если пара ключ-значение найдена, вернуть соответствующее val\n        if self.buckets[index] not in [None, self.TOMBSTONE]:\n            return self.buckets[index].val\n        # Если пара ключ-значение не существует, вернуть None\n        return None\n\n    def put(self, key: int, val: str):\n        \"\"\"Операция добавления\"\"\"\n        # Когда коэффициент загрузки превышает порог, выполнить расширение\n        if self.load_factor() > self.load_thres:\n            self.extend()\n        # Найти индекс корзины, соответствующий key\n        index = self.find_bucket(key)\n        # Если пара ключ-значение найдена, перезаписать val и вернуть\n        if self.buckets[index] not in [None, self.TOMBSTONE]:\n            self.buckets[index].val = val\n            return\n        # Если пары ключ-значение нет, добавить ее\n        self.buckets[index] = Pair(key, val)\n        self.size += 1\n\n    def remove(self, key: int):\n        \"\"\"Операция удаления\"\"\"\n        # Найти индекс корзины, соответствующий key\n        index = self.find_bucket(key)\n        # Если пара ключ-значение найдена, заменить ее меткой удаления\n        if self.buckets[index] not in [None, self.TOMBSTONE]:\n            self.buckets[index] = self.TOMBSTONE\n            self.size -= 1\n\n    def extend(self):\n        \"\"\"Расширить хеш-таблицу\"\"\"\n        # Временно сохранить исходную хеш-таблицу\n        buckets_tmp = self.buckets\n        # Инициализация новой хеш-таблицы после расширения\n        self.capacity *= self.extend_ratio\n        self.buckets = [None] * self.capacity\n        self.size = 0\n        # Перенести пары ключ-значение из исходной хеш-таблицы в новую\n        for pair in buckets_tmp:\n            if pair not in [None, self.TOMBSTONE]:\n                self.put(pair.key, pair.val)\n\n    def print(self):\n        \"\"\"Вывести хеш-таблицу\"\"\"\n        for pair in self.buckets:\n            if pair is None:\n                print(\"None\")\n            elif pair is self.TOMBSTONE:\n                print(\"TOMBSTONE\")\n            else:\n                print(pair.key, \"->\", pair.val)\n
        hash_map_open_addressing.cpp
        /* Хеш-таблица с открытой адресацией */\nclass HashMapOpenAddressing {\n  private:\n    int size;                             // Число пар ключ-значение\n    int capacity = 4;                     // Вместимость хеш-таблицы\n    const double loadThres = 2.0 / 3.0;     // Порог коэффициента загрузки для запуска расширения\n    const int extendRatio = 2;            // Коэффициент расширения\n    vector<Pair *> buckets;               // Массив корзин\n    Pair *TOMBSTONE = new Pair(-1, \"-1\"); // Удалить метку\n\n  public:\n    /* Конструктор */\n    HashMapOpenAddressing() : size(0), buckets(capacity, nullptr) {\n    }\n\n    /* Метод-деструктор */\n    ~HashMapOpenAddressing() {\n        for (Pair *pair : buckets) {\n            if (pair != nullptr && pair != TOMBSTONE) {\n                delete pair;\n            }\n        }\n        delete TOMBSTONE;\n    }\n\n    /* Хеш-функция */\n    int hashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* Коэффициент загрузки */\n    double loadFactor() {\n        return (double)size / capacity;\n    }\n\n    /* Найти индекс корзины, соответствующий key */\n    int findBucket(int key) {\n        int index = hashFunc(key);\n        int firstTombstone = -1;\n        // Выполнять линейное пробирование и завершить при встрече с пустой корзиной\n        while (buckets[index] != nullptr) {\n            // Если встретился key, вернуть соответствующий индекс корзины\n            if (buckets[index]->key == key) {\n                // Если ранее встретилась метка удаления, переместить пару ключ-значение на этот индекс\n                if (firstTombstone != -1) {\n                    buckets[firstTombstone] = buckets[index];\n                    buckets[index] = TOMBSTONE;\n                    return firstTombstone; // Вернуть индекс корзины после перемещения\n                }\n                return index; // Вернуть индекс корзины\n            }\n            // Записать первую встретившуюся метку удаления\n            if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {\n                firstTombstone = index;\n            }\n            // Вычислить индекс корзины; при выходе за конец вернуться к началу\n            index = (index + 1) % capacity;\n        }\n        // Если key не существует, вернуть индекс точки добавления\n        return firstTombstone == -1 ? index : firstTombstone;\n    }\n\n    /* Операция поиска */\n    string get(int key) {\n        // Найти индекс корзины, соответствующий key\n        int index = findBucket(key);\n        // Если пара ключ-значение найдена, вернуть соответствующее val\n        if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {\n            return buckets[index]->val;\n        }\n        // Если пары ключ-значение не существует, вернуть пустую строку\n        return \"\";\n    }\n\n    /* Операция добавления */\n    void put(int key, string val) {\n        // Когда коэффициент загрузки превышает порог, выполнить расширение\n        if (loadFactor() > loadThres) {\n            extend();\n        }\n        // Найти индекс корзины, соответствующий key\n        int index = findBucket(key);\n        // Если пара ключ-значение найдена, перезаписать val и вернуть\n        if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {\n            buckets[index]->val = val;\n            return;\n        }\n        // Если пары ключ-значение нет, добавить ее\n        buckets[index] = new Pair(key, val);\n        size++;\n    }\n\n    /* Операция удаления */\n    void remove(int key) {\n        // Найти индекс корзины, соответствующий key\n        int index = findBucket(key);\n        // Если пара ключ-значение найдена, заменить ее меткой удаления\n        if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {\n            delete buckets[index];\n            buckets[index] = TOMBSTONE;\n            size--;\n        }\n    }\n\n    /* Расширить хеш-таблицу */\n    void extend() {\n        // Временно сохранить исходную хеш-таблицу\n        vector<Pair *> bucketsTmp = buckets;\n        // Инициализация новой хеш-таблицы после расширения\n        capacity *= extendRatio;\n        buckets = vector<Pair *>(capacity, nullptr);\n        size = 0;\n        // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n        for (Pair *pair : bucketsTmp) {\n            if (pair != nullptr && pair != TOMBSTONE) {\n                put(pair->key, pair->val);\n                delete pair;\n            }\n        }\n    }\n\n    /* Вывести хеш-таблицу */\n    void print() {\n        for (Pair *pair : buckets) {\n            if (pair == nullptr) {\n                cout << \"nullptr\" << endl;\n            } else if (pair == TOMBSTONE) {\n                cout << \"TOMBSTONE\" << endl;\n            } else {\n                cout << pair->key << \" -> \" << pair->val << endl;\n            }\n        }\n    }\n};\n
        hash_map_open_addressing.java
        /* Хеш-таблица с открытой адресацией */\nclass HashMapOpenAddressing {\n    private int size; // Число пар ключ-значение\n    private int capacity = 4; // Вместимость хеш-таблицы\n    private final double loadThres = 2.0 / 3.0; // Порог коэффициента загрузки для запуска расширения\n    private final int extendRatio = 2; // Коэффициент расширения\n    private Pair[] buckets; // Массив корзин\n    private final Pair TOMBSTONE = new Pair(-1, \"-1\"); // Удалить метку\n\n    /* Конструктор */\n    public HashMapOpenAddressing() {\n        size = 0;\n        buckets = new Pair[capacity];\n    }\n\n    /* Хеш-функция */\n    private int hashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* Коэффициент загрузки */\n    private double loadFactor() {\n        return (double) size / capacity;\n    }\n\n    /* Найти индекс корзины, соответствующий key */\n    private int findBucket(int key) {\n        int index = hashFunc(key);\n        int firstTombstone = -1;\n        // Выполнять линейное пробирование и завершить при встрече с пустой корзиной\n        while (buckets[index] != null) {\n            // Если встретился key, вернуть соответствующий индекс корзины\n            if (buckets[index].key == key) {\n                // Если ранее встретилась метка удаления, переместить пару ключ-значение на этот индекс\n                if (firstTombstone != -1) {\n                    buckets[firstTombstone] = buckets[index];\n                    buckets[index] = TOMBSTONE;\n                    return firstTombstone; // Вернуть индекс корзины после перемещения\n                }\n                return index; // Вернуть индекс корзины\n            }\n            // Записать первую встретившуюся метку удаления\n            if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {\n                firstTombstone = index;\n            }\n            // Вычислить индекс корзины; при выходе за конец вернуться к началу\n            index = (index + 1) % capacity;\n        }\n        // Если key не существует, вернуть индекс точки добавления\n        return firstTombstone == -1 ? index : firstTombstone;\n    }\n\n    /* Операция поиска */\n    public String get(int key) {\n        // Найти индекс корзины, соответствующий key\n        int index = findBucket(key);\n        // Если пара ключ-значение найдена, вернуть соответствующее val\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            return buckets[index].val;\n        }\n        // Если пары ключ-значение не существует, вернуть null\n        return null;\n    }\n\n    /* Операция добавления */\n    public void put(int key, String val) {\n        // Когда коэффициент загрузки превышает порог, выполнить расширение\n        if (loadFactor() > loadThres) {\n            extend();\n        }\n        // Найти индекс корзины, соответствующий key\n        int index = findBucket(key);\n        // Если пара ключ-значение найдена, перезаписать val и вернуть\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index].val = val;\n            return;\n        }\n        // Если пары ключ-значение нет, добавить ее\n        buckets[index] = new Pair(key, val);\n        size++;\n    }\n\n    /* Операция удаления */\n    public void remove(int key) {\n        // Найти индекс корзины, соответствующий key\n        int index = findBucket(key);\n        // Если пара ключ-значение найдена, заменить ее меткой удаления\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index] = TOMBSTONE;\n            size--;\n        }\n    }\n\n    /* Расширить хеш-таблицу */\n    private void extend() {\n        // Временно сохранить исходную хеш-таблицу\n        Pair[] bucketsTmp = buckets;\n        // Инициализация новой хеш-таблицы после расширения\n        capacity *= extendRatio;\n        buckets = new Pair[capacity];\n        size = 0;\n        // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n        for (Pair pair : bucketsTmp) {\n            if (pair != null && pair != TOMBSTONE) {\n                put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Вывести хеш-таблицу */\n    public void print() {\n        for (Pair pair : buckets) {\n            if (pair == null) {\n                System.out.println(\"null\");\n            } else if (pair == TOMBSTONE) {\n                System.out.println(\"TOMBSTONE\");\n            } else {\n                System.out.println(pair.key + \" -> \" + pair.val);\n            }\n        }\n    }\n}\n
        hash_map_open_addressing.cs
        /* Хеш-таблица с открытой адресацией */\nclass HashMapOpenAddressing {\n    int size; // Число пар ключ-значение\n    int capacity = 4; // Вместимость хеш-таблицы\n    double loadThres = 2.0 / 3.0; // Порог коэффициента загрузки для запуска расширения\n    int extendRatio = 2; // Коэффициент расширения\n    Pair[] buckets; // Массив корзин\n    Pair TOMBSTONE = new(-1, \"-1\"); // Удалить метку\n\n    /* Конструктор */\n    public HashMapOpenAddressing() {\n        size = 0;\n        buckets = new Pair[capacity];\n    }\n\n    /* Хеш-функция */\n    int HashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* Коэффициент загрузки */\n    double LoadFactor() {\n        return (double)size / capacity;\n    }\n\n    /* Найти индекс корзины, соответствующий key */\n    int FindBucket(int key) {\n        int index = HashFunc(key);\n        int firstTombstone = -1;\n        // Выполнять линейное пробирование и завершить при встрече с пустой корзиной\n        while (buckets[index] != null) {\n            // Если встретился key, вернуть соответствующий индекс корзины\n            if (buckets[index].key == key) {\n                // Если ранее встретилась метка удаления, переместить пару ключ-значение на этот индекс\n                if (firstTombstone != -1) {\n                    buckets[firstTombstone] = buckets[index];\n                    buckets[index] = TOMBSTONE;\n                    return firstTombstone; // Вернуть индекс корзины после перемещения\n                }\n                return index; // Вернуть индекс корзины\n            }\n            // Записать первую встретившуюся метку удаления\n            if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {\n                firstTombstone = index;\n            }\n            // Вычислить индекс корзины; при выходе за конец вернуться к началу\n            index = (index + 1) % capacity;\n        }\n        // Если key не существует, вернуть индекс точки добавления\n        return firstTombstone == -1 ? index : firstTombstone;\n    }\n\n    /* Операция поиска */\n    public string? Get(int key) {\n        // Найти индекс корзины, соответствующий key\n        int index = FindBucket(key);\n        // Если пара ключ-значение найдена, вернуть соответствующее val\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            return buckets[index].val;\n        }\n        // Если пары ключ-значение не существует, вернуть null\n        return null;\n    }\n\n    /* Операция добавления */\n    public void Put(int key, string val) {\n        // Когда коэффициент загрузки превышает порог, выполнить расширение\n        if (LoadFactor() > loadThres) {\n            Extend();\n        }\n        // Найти индекс корзины, соответствующий key\n        int index = FindBucket(key);\n        // Если пара ключ-значение найдена, перезаписать val и вернуть\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index].val = val;\n            return;\n        }\n        // Если пары ключ-значение нет, добавить ее\n        buckets[index] = new Pair(key, val);\n        size++;\n    }\n\n    /* Операция удаления */\n    public void Remove(int key) {\n        // Найти индекс корзины, соответствующий key\n        int index = FindBucket(key);\n        // Если пара ключ-значение найдена, заменить ее меткой удаления\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index] = TOMBSTONE;\n            size--;\n        }\n    }\n\n    /* Расширить хеш-таблицу */\n    void Extend() {\n        // Временно сохранить исходную хеш-таблицу\n        Pair[] bucketsTmp = buckets;\n        // Инициализация новой хеш-таблицы после расширения\n        capacity *= extendRatio;\n        buckets = new Pair[capacity];\n        size = 0;\n        // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n        foreach (Pair pair in bucketsTmp) {\n            if (pair != null && pair != TOMBSTONE) {\n                Put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Вывести хеш-таблицу */\n    public void Print() {\n        foreach (Pair pair in buckets) {\n            if (pair == null) {\n                Console.WriteLine(\"null\");\n            } else if (pair == TOMBSTONE) {\n                Console.WriteLine(\"TOMBSTONE\");\n            } else {\n                Console.WriteLine(pair.key + \" -> \" + pair.val);\n            }\n        }\n    }\n}\n
        hash_map_open_addressing.go
        /* Хеш-таблица с открытой адресацией */\ntype hashMapOpenAddressing struct {\n    size        int     // Число пар ключ-значение\n    capacity    int     // Вместимость хеш-таблицы\n    loadThres   float64 // Порог коэффициента загрузки для запуска расширения\n    extendRatio int     // Коэффициент расширения\n    buckets     []*pair // Массив корзин\n    TOMBSTONE   *pair   // Удалить метку\n}\n\n/* Конструктор */\nfunc newHashMapOpenAddressing() *hashMapOpenAddressing {\n    return &hashMapOpenAddressing{\n        size:        0,\n        capacity:    4,\n        loadThres:   2.0 / 3.0,\n        extendRatio: 2,\n        buckets:     make([]*pair, 4),\n        TOMBSTONE:   &pair{-1, \"-1\"},\n    }\n}\n\n/* Хеш-функция */\nfunc (h *hashMapOpenAddressing) hashFunc(key int) int {\n    return key % h.capacity // Вычислить хеш-значение по ключу\n}\n\n/* Коэффициент загрузки */\nfunc (h *hashMapOpenAddressing) loadFactor() float64 {\n    return float64(h.size) / float64(h.capacity) // Вычислить текущий коэффициент загрузки\n}\n\n/* Найти индекс корзины, соответствующий key */\nfunc (h *hashMapOpenAddressing) findBucket(key int) int {\n    index := h.hashFunc(key) // Получить начальный индекс\n    firstTombstone := -1     // Запомнить положение первого TOMBSTONE\n    for h.buckets[index] != nil {\n        if h.buckets[index].key == key {\n            if firstTombstone != -1 {\n                // Если ранее встретилась метка удаления, переместить пару ключ-значение на этот индекс\n                h.buckets[firstTombstone] = h.buckets[index]\n                h.buckets[index] = h.TOMBSTONE\n                return firstTombstone // Вернуть индекс корзины после перемещения\n            }\n            return index // Вернуть найденный индекс\n        }\n        if firstTombstone == -1 && h.buckets[index] == h.TOMBSTONE {\n            firstTombstone = index // Запомнить положение первой метки удаления\n        }\n        index = (index + 1) % h.capacity // Линейное пробирование: при выходе за хвост вернуться к началу\n    }\n    // Если key не существует, вернуть индекс точки добавления\n    if firstTombstone != -1 {\n        return firstTombstone\n    }\n    return index\n}\n\n/* Операция поиска */\nfunc (h *hashMapOpenAddressing) get(key int) string {\n    index := h.findBucket(key) // Найти индекс корзины, соответствующий key\n    if h.buckets[index] != nil && h.buckets[index] != h.TOMBSTONE {\n        return h.buckets[index].val // Если пара ключ-значение найдена, вернуть соответствующее val\n    }\n    return \"\" // Если пара ключ-значение не существует, вернуть \"\"\n}\n\n/* Операция добавления */\nfunc (h *hashMapOpenAddressing) put(key int, val string) {\n    if h.loadFactor() > h.loadThres {\n        h.extend() // Когда коэффициент загрузки превышает порог, выполнить расширение\n    }\n    index := h.findBucket(key) // Найти индекс корзины, соответствующий key\n    if h.buckets[index] == nil || h.buckets[index] == h.TOMBSTONE {\n        h.buckets[index] = &pair{key, val} // Если пары ключ-значение нет, добавить ее\n        h.size++\n    } else {\n        h.buckets[index].val = val // Если пара ключ-значение найдена, перезаписать val\n    }\n}\n\n/* Операция удаления */\nfunc (h *hashMapOpenAddressing) remove(key int) {\n    index := h.findBucket(key) // Найти индекс корзины, соответствующий key\n    if h.buckets[index] != nil && h.buckets[index] != h.TOMBSTONE {\n        h.buckets[index] = h.TOMBSTONE // Если пара ключ-значение найдена, заменить ее меткой удаления\n        h.size--\n    }\n}\n\n/* Расширить хеш-таблицу */\nfunc (h *hashMapOpenAddressing) extend() {\n    oldBuckets := h.buckets               // Временно сохранить исходную хеш-таблицу\n    h.capacity *= h.extendRatio           // Обновить емкость\n    h.buckets = make([]*pair, h.capacity) // Инициализация новой хеш-таблицы после расширения\n    h.size = 0                            // Сбросить размер\n    // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n    for _, pair := range oldBuckets {\n        if pair != nil && pair != h.TOMBSTONE {\n            h.put(pair.key, pair.val)\n        }\n    }\n}\n\n/* Вывести хеш-таблицу */\nfunc (h *hashMapOpenAddressing) print() {\n    for _, pair := range h.buckets {\n        if pair == nil {\n            fmt.Println(\"nil\")\n        } else if pair == h.TOMBSTONE {\n            fmt.Println(\"TOMBSTONE\")\n        } else {\n            fmt.Printf(\"%d -> %s\\n\", pair.key, pair.val)\n        }\n    }\n}\n
        hash_map_open_addressing.swift
        /* Хеш-таблица с открытой адресацией */\nclass HashMapOpenAddressing {\n    var size: Int // Число пар ключ-значение\n    var capacity: Int // Вместимость хеш-таблицы\n    var loadThres: Double // Порог коэффициента загрузки для запуска расширения\n    var extendRatio: Int // Коэффициент расширения\n    var buckets: [Pair?] // Массив корзин\n    var TOMBSTONE: Pair // Удалить метку\n\n    /* Конструктор */\n    init() {\n        size = 0\n        capacity = 4\n        loadThres = 2.0 / 3.0\n        extendRatio = 2\n        buckets = Array(repeating: nil, count: capacity)\n        TOMBSTONE = Pair(key: -1, val: \"-1\")\n    }\n\n    /* Хеш-функция */\n    func hashFunc(key: Int) -> Int {\n        key % capacity\n    }\n\n    /* Коэффициент загрузки */\n    func loadFactor() -> Double {\n        Double(size) / Double(capacity)\n    }\n\n    /* Найти индекс корзины, соответствующий key */\n    func findBucket(key: Int) -> Int {\n        var index = hashFunc(key: key)\n        var firstTombstone = -1\n        // Выполнять линейное пробирование и завершить при встрече с пустой корзиной\n        while buckets[index] != nil {\n            // Если встретился key, вернуть соответствующий индекс корзины\n            if buckets[index]!.key == key {\n                // Если ранее встретилась метка удаления, переместить пару ключ-значение на этот индекс\n                if firstTombstone != -1 {\n                    buckets[firstTombstone] = buckets[index]\n                    buckets[index] = TOMBSTONE\n                    return firstTombstone // Вернуть индекс корзины после перемещения\n                }\n                return index // Вернуть индекс корзины\n            }\n            // Записать первую встретившуюся метку удаления\n            if firstTombstone == -1 && buckets[index] == TOMBSTONE {\n                firstTombstone = index\n            }\n            // Вычислить индекс корзины; при выходе за конец вернуться к началу\n            index = (index + 1) % capacity\n        }\n        // Если key не существует, вернуть индекс точки добавления\n        return firstTombstone == -1 ? index : firstTombstone\n    }\n\n    /* Операция поиска */\n    func get(key: Int) -> String? {\n        // Найти индекс корзины, соответствующий key\n        let index = findBucket(key: key)\n        // Если пара ключ-значение найдена, вернуть соответствующее val\n        if buckets[index] != nil, buckets[index] != TOMBSTONE {\n            return buckets[index]!.val\n        }\n        // Если пары ключ-значение не существует, вернуть null\n        return nil\n    }\n\n    /* Операция добавления */\n    func put(key: Int, val: String) {\n        // Когда коэффициент загрузки превышает порог, выполнить расширение\n        if loadFactor() > loadThres {\n            extend()\n        }\n        // Найти индекс корзины, соответствующий key\n        let index = findBucket(key: key)\n        // Если пара ключ-значение найдена, перезаписать val и вернуть\n        if buckets[index] != nil, buckets[index] != TOMBSTONE {\n            buckets[index]!.val = val\n            return\n        }\n        // Если пары ключ-значение нет, добавить ее\n        buckets[index] = Pair(key: key, val: val)\n        size += 1\n    }\n\n    /* Операция удаления */\n    func remove(key: Int) {\n        // Найти индекс корзины, соответствующий key\n        let index = findBucket(key: key)\n        // Если пара ключ-значение найдена, заменить ее меткой удаления\n        if buckets[index] != nil, buckets[index] != TOMBSTONE {\n            buckets[index] = TOMBSTONE\n            size -= 1\n        }\n    }\n\n    /* Расширить хеш-таблицу */\n    func extend() {\n        // Временно сохранить исходную хеш-таблицу\n        let bucketsTmp = buckets\n        // Инициализация новой хеш-таблицы после расширения\n        capacity *= extendRatio\n        buckets = Array(repeating: nil, count: capacity)\n        size = 0\n        // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n        for pair in bucketsTmp {\n            if let pair, pair != TOMBSTONE {\n                put(key: pair.key, val: pair.val)\n            }\n        }\n    }\n\n    /* Вывести хеш-таблицу */\n    func print() {\n        for pair in buckets {\n            if pair == nil {\n                Swift.print(\"null\")\n            } else if pair == TOMBSTONE {\n                Swift.print(\"TOMBSTONE\")\n            } else {\n                Swift.print(\"\\(pair!.key) -> \\(pair!.val)\")\n            }\n        }\n    }\n}\n
        hash_map_open_addressing.js
        /* Хеш-таблица с открытой адресацией */\nclass HashMapOpenAddressing {\n    #size; // Число пар ключ-значение\n    #capacity; // Вместимость хеш-таблицы\n    #loadThres; // Порог коэффициента загрузки для запуска расширения\n    #extendRatio; // Коэффициент расширения\n    #buckets; // Массив корзин\n    #TOMBSTONE; // Удалить метку\n\n    /* Конструктор */\n    constructor() {\n        this.#size = 0; // Число пар ключ-значение\n        this.#capacity = 4; // Вместимость хеш-таблицы\n        this.#loadThres = 2.0 / 3.0; // Порог коэффициента загрузки для запуска расширения\n        this.#extendRatio = 2; // Коэффициент расширения\n        this.#buckets = Array(this.#capacity).fill(null); // Массив корзин\n        this.#TOMBSTONE = new Pair(-1, '-1'); // Удалить метку\n    }\n\n    /* Хеш-функция */\n    #hashFunc(key) {\n        return key % this.#capacity;\n    }\n\n    /* Коэффициент загрузки */\n    #loadFactor() {\n        return this.#size / this.#capacity;\n    }\n\n    /* Найти индекс корзины, соответствующий key */\n    #findBucket(key) {\n        let index = this.#hashFunc(key);\n        let firstTombstone = -1;\n        // Выполнять линейное пробирование и завершить при встрече с пустой корзиной\n        while (this.#buckets[index] !== null) {\n            // Если встретился key, вернуть соответствующий индекс корзины\n            if (this.#buckets[index].key === key) {\n                // Если ранее встретилась метка удаления, переместить пару ключ-значение на этот индекс\n                if (firstTombstone !== -1) {\n                    this.#buckets[firstTombstone] = this.#buckets[index];\n                    this.#buckets[index] = this.#TOMBSTONE;\n                    return firstTombstone; // Вернуть индекс корзины после перемещения\n                }\n                return index; // Вернуть индекс корзины\n            }\n            // Записать первую встретившуюся метку удаления\n            if (\n                firstTombstone === -1 &&\n                this.#buckets[index] === this.#TOMBSTONE\n            ) {\n                firstTombstone = index;\n            }\n            // Вычислить индекс корзины; при выходе за конец вернуться к началу\n            index = (index + 1) % this.#capacity;\n        }\n        // Если key не существует, вернуть индекс точки добавления\n        return firstTombstone === -1 ? index : firstTombstone;\n    }\n\n    /* Операция поиска */\n    get(key) {\n        // Найти индекс корзины, соответствующий key\n        const index = this.#findBucket(key);\n        // Если пара ключ-значение найдена, вернуть соответствующее val\n        if (\n            this.#buckets[index] !== null &&\n            this.#buckets[index] !== this.#TOMBSTONE\n        ) {\n            return this.#buckets[index].val;\n        }\n        // Если пары ключ-значение не существует, вернуть null\n        return null;\n    }\n\n    /* Операция добавления */\n    put(key, val) {\n        // Когда коэффициент загрузки превышает порог, выполнить расширение\n        if (this.#loadFactor() > this.#loadThres) {\n            this.#extend();\n        }\n        // Найти индекс корзины, соответствующий key\n        const index = this.#findBucket(key);\n        // Если пара ключ-значение найдена, перезаписать val и вернуть\n        if (\n            this.#buckets[index] !== null &&\n            this.#buckets[index] !== this.#TOMBSTONE\n        ) {\n            this.#buckets[index].val = val;\n            return;\n        }\n        // Если пары ключ-значение нет, добавить ее\n        this.#buckets[index] = new Pair(key, val);\n        this.#size++;\n    }\n\n    /* Операция удаления */\n    remove(key) {\n        // Найти индекс корзины, соответствующий key\n        const index = this.#findBucket(key);\n        // Если пара ключ-значение найдена, заменить ее меткой удаления\n        if (\n            this.#buckets[index] !== null &&\n            this.#buckets[index] !== this.#TOMBSTONE\n        ) {\n            this.#buckets[index] = this.#TOMBSTONE;\n            this.#size--;\n        }\n    }\n\n    /* Расширить хеш-таблицу */\n    #extend() {\n        // Временно сохранить исходную хеш-таблицу\n        const bucketsTmp = this.#buckets;\n        // Инициализация новой хеш-таблицы после расширения\n        this.#capacity *= this.#extendRatio;\n        this.#buckets = Array(this.#capacity).fill(null);\n        this.#size = 0;\n        // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n        for (const pair of bucketsTmp) {\n            if (pair !== null && pair !== this.#TOMBSTONE) {\n                this.put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Вывести хеш-таблицу */\n    print() {\n        for (const pair of this.#buckets) {\n            if (pair === null) {\n                console.log('null');\n            } else if (pair === this.#TOMBSTONE) {\n                console.log('TOMBSTONE');\n            } else {\n                console.log(pair.key + ' -> ' + pair.val);\n            }\n        }\n    }\n}\n
        hash_map_open_addressing.ts
        /* Хеш-таблица с открытой адресацией */\nclass HashMapOpenAddressing {\n    private size: number; // Число пар ключ-значение\n    private capacity: number; // Вместимость хеш-таблицы\n    private loadThres: number; // Порог коэффициента загрузки для запуска расширения\n    private extendRatio: number; // Коэффициент расширения\n    private buckets: Array<Pair | null>; // Массив корзин\n    private TOMBSTONE: Pair; // Удалить метку\n\n    /* Конструктор */\n    constructor() {\n        this.size = 0; // Число пар ключ-значение\n        this.capacity = 4; // Вместимость хеш-таблицы\n        this.loadThres = 2.0 / 3.0; // Порог коэффициента загрузки для запуска расширения\n        this.extendRatio = 2; // Коэффициент расширения\n        this.buckets = Array(this.capacity).fill(null); // Массив корзин\n        this.TOMBSTONE = new Pair(-1, '-1'); // Удалить метку\n    }\n\n    /* Хеш-функция */\n    private hashFunc(key: number): number {\n        return key % this.capacity;\n    }\n\n    /* Коэффициент загрузки */\n    private loadFactor(): number {\n        return this.size / this.capacity;\n    }\n\n    /* Найти индекс корзины, соответствующий key */\n    private findBucket(key: number): number {\n        let index = this.hashFunc(key);\n        let firstTombstone = -1;\n        // Выполнять линейное пробирование и завершить при встрече с пустой корзиной\n        while (this.buckets[index] !== null) {\n            // Если встретился key, вернуть соответствующий индекс корзины\n            if (this.buckets[index]!.key === key) {\n                // Если ранее встретилась метка удаления, переместить пару ключ-значение на этот индекс\n                if (firstTombstone !== -1) {\n                    this.buckets[firstTombstone] = this.buckets[index];\n                    this.buckets[index] = this.TOMBSTONE;\n                    return firstTombstone; // Вернуть индекс корзины после перемещения\n                }\n                return index; // Вернуть индекс корзины\n            }\n            // Записать первую встретившуюся метку удаления\n            if (\n                firstTombstone === -1 &&\n                this.buckets[index] === this.TOMBSTONE\n            ) {\n                firstTombstone = index;\n            }\n            // Вычислить индекс корзины; при выходе за конец вернуться к началу\n            index = (index + 1) % this.capacity;\n        }\n        // Если key не существует, вернуть индекс точки добавления\n        return firstTombstone === -1 ? index : firstTombstone;\n    }\n\n    /* Операция поиска */\n    get(key: number): string | null {\n        // Найти индекс корзины, соответствующий key\n        const index = this.findBucket(key);\n        // Если пара ключ-значение найдена, вернуть соответствующее val\n        if (\n            this.buckets[index] !== null &&\n            this.buckets[index] !== this.TOMBSTONE\n        ) {\n            return this.buckets[index]!.val;\n        }\n        // Если пары ключ-значение не существует, вернуть null\n        return null;\n    }\n\n    /* Операция добавления */\n    put(key: number, val: string): void {\n        // Когда коэффициент загрузки превышает порог, выполнить расширение\n        if (this.loadFactor() > this.loadThres) {\n            this.extend();\n        }\n        // Найти индекс корзины, соответствующий key\n        const index = this.findBucket(key);\n        // Если пара ключ-значение найдена, перезаписать val и вернуть\n        if (\n            this.buckets[index] !== null &&\n            this.buckets[index] !== this.TOMBSTONE\n        ) {\n            this.buckets[index]!.val = val;\n            return;\n        }\n        // Если пары ключ-значение нет, добавить ее\n        this.buckets[index] = new Pair(key, val);\n        this.size++;\n    }\n\n    /* Операция удаления */\n    remove(key: number): void {\n        // Найти индекс корзины, соответствующий key\n        const index = this.findBucket(key);\n        // Если пара ключ-значение найдена, заменить ее меткой удаления\n        if (\n            this.buckets[index] !== null &&\n            this.buckets[index] !== this.TOMBSTONE\n        ) {\n            this.buckets[index] = this.TOMBSTONE;\n            this.size--;\n        }\n    }\n\n    /* Расширить хеш-таблицу */\n    private extend(): void {\n        // Временно сохранить исходную хеш-таблицу\n        const bucketsTmp = this.buckets;\n        // Инициализация новой хеш-таблицы после расширения\n        this.capacity *= this.extendRatio;\n        this.buckets = Array(this.capacity).fill(null);\n        this.size = 0;\n        // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n        for (const pair of bucketsTmp) {\n            if (pair !== null && pair !== this.TOMBSTONE) {\n                this.put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Вывести хеш-таблицу */\n    print(): void {\n        for (const pair of this.buckets) {\n            if (pair === null) {\n                console.log('null');\n            } else if (pair === this.TOMBSTONE) {\n                console.log('TOMBSTONE');\n            } else {\n                console.log(pair.key + ' -> ' + pair.val);\n            }\n        }\n    }\n}\n
        hash_map_open_addressing.dart
        /* Хеш-таблица с открытой адресацией */\nclass HashMapOpenAddressing {\n  late int _size; // Число пар ключ-значение\n  int _capacity = 4; // Вместимость хеш-таблицы\n  double _loadThres = 2.0 / 3.0; // Порог коэффициента загрузки для запуска расширения\n  int _extendRatio = 2; // Коэффициент расширения\n  late List<Pair?> _buckets; // Массив корзин\n  Pair _TOMBSTONE = Pair(-1, \"-1\"); // Удалить метку\n\n  /* Конструктор */\n  HashMapOpenAddressing() {\n    _size = 0;\n    _buckets = List.generate(_capacity, (index) => null);\n  }\n\n  /* Хеш-функция */\n  int hashFunc(int key) {\n    return key % _capacity;\n  }\n\n  /* Коэффициент загрузки */\n  double loadFactor() {\n    return _size / _capacity;\n  }\n\n  /* Найти индекс корзины, соответствующий key */\n  int findBucket(int key) {\n    int index = hashFunc(key);\n    int firstTombstone = -1;\n    // Выполнять линейное пробирование и завершить при встрече с пустой корзиной\n    while (_buckets[index] != null) {\n      // Если встретился key, вернуть соответствующий индекс корзины\n      if (_buckets[index]!.key == key) {\n        // Если ранее встретилась метка удаления, переместить пару ключ-значение на этот индекс\n        if (firstTombstone != -1) {\n          _buckets[firstTombstone] = _buckets[index];\n          _buckets[index] = _TOMBSTONE;\n          return firstTombstone; // Вернуть индекс корзины после перемещения\n        }\n        return index; // Вернуть индекс корзины\n      }\n      // Записать первую встретившуюся метку удаления\n      if (firstTombstone == -1 && _buckets[index] == _TOMBSTONE) {\n        firstTombstone = index;\n      }\n      // Вычислить индекс корзины; при выходе за конец вернуться к началу\n      index = (index + 1) % _capacity;\n    }\n    // Если key не существует, вернуть индекс точки добавления\n    return firstTombstone == -1 ? index : firstTombstone;\n  }\n\n  /* Операция поиска */\n  String? get(int key) {\n    // Найти индекс корзины, соответствующий key\n    int index = findBucket(key);\n    // Если пара ключ-значение найдена, вернуть соответствующее val\n    if (_buckets[index] != null && _buckets[index] != _TOMBSTONE) {\n      return _buckets[index]!.val;\n    }\n    // Если пары ключ-значение не существует, вернуть null\n    return null;\n  }\n\n  /* Операция добавления */\n  void put(int key, String val) {\n    // Когда коэффициент загрузки превышает порог, выполнить расширение\n    if (loadFactor() > _loadThres) {\n      extend();\n    }\n    // Найти индекс корзины, соответствующий key\n    int index = findBucket(key);\n    // Если пара ключ-значение найдена, перезаписать val и вернуть\n    if (_buckets[index] != null && _buckets[index] != _TOMBSTONE) {\n      _buckets[index]!.val = val;\n      return;\n    }\n    // Если пары ключ-значение нет, добавить ее\n    _buckets[index] = new Pair(key, val);\n    _size++;\n  }\n\n  /* Операция удаления */\n  void remove(int key) {\n    // Найти индекс корзины, соответствующий key\n    int index = findBucket(key);\n    // Если пара ключ-значение найдена, заменить ее меткой удаления\n    if (_buckets[index] != null && _buckets[index] != _TOMBSTONE) {\n      _buckets[index] = _TOMBSTONE;\n      _size--;\n    }\n  }\n\n  /* Расширить хеш-таблицу */\n  void extend() {\n    // Временно сохранить исходную хеш-таблицу\n    List<Pair?> bucketsTmp = _buckets;\n    // Инициализация новой хеш-таблицы после расширения\n    _capacity *= _extendRatio;\n    _buckets = List.generate(_capacity, (index) => null);\n    _size = 0;\n    // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n    for (Pair? pair in bucketsTmp) {\n      if (pair != null && pair != _TOMBSTONE) {\n        put(pair.key, pair.val);\n      }\n    }\n  }\n\n  /* Вывести хеш-таблицу */\n  void printHashMap() {\n    for (Pair? pair in _buckets) {\n      if (pair == null) {\n        print(\"null\");\n      } else if (pair == _TOMBSTONE) {\n        print(\"TOMBSTONE\");\n      } else {\n        print(\"${pair.key} -> ${pair.val}\");\n      }\n    }\n  }\n}\n
        hash_map_open_addressing.rs
        /* Хеш-таблица с открытой адресацией */\nstruct HashMapOpenAddressing {\n    size: usize,                // Число пар ключ-значение\n    capacity: usize,            // Вместимость хеш-таблицы\n    load_thres: f64,            // Порог коэффициента загрузки для запуска расширения\n    extend_ratio: usize,        // Коэффициент расширения\n    buckets: Vec<Option<Pair>>, // Массив корзин\n    TOMBSTONE: Option<Pair>,    // Удалить метку\n}\n\nimpl HashMapOpenAddressing {\n    /* Конструктор */\n    fn new() -> Self {\n        Self {\n            size: 0,\n            capacity: 4,\n            load_thres: 2.0 / 3.0,\n            extend_ratio: 2,\n            buckets: vec![None; 4],\n            TOMBSTONE: Some(Pair {\n                key: -1,\n                val: \"-1\".to_string(),\n            }),\n        }\n    }\n\n    /* Хеш-функция */\n    fn hash_func(&self, key: i32) -> usize {\n        (key % self.capacity as i32) as usize\n    }\n\n    /* Коэффициент загрузки */\n    fn load_factor(&self) -> f64 {\n        self.size as f64 / self.capacity as f64\n    }\n\n    /* Найти индекс корзины, соответствующий key */\n    fn find_bucket(&mut self, key: i32) -> usize {\n        let mut index = self.hash_func(key);\n        let mut first_tombstone = -1;\n        // Выполнять линейное пробирование и завершить при встрече с пустой корзиной\n        while self.buckets[index].is_some() {\n            // Если встретился key, вернуть соответствующий индекс корзины\n            if self.buckets[index].as_ref().unwrap().key == key {\n                // Если ранее встретилась метка удаления, переместить пару ключ-значение в этот индекс\n                if first_tombstone != -1 {\n                    self.buckets[first_tombstone as usize] = self.buckets[index].take();\n                    self.buckets[index] = self.TOMBSTONE.clone();\n                    return first_tombstone as usize; // Вернуть индекс корзины после перемещения\n                }\n                return index; // Вернуть индекс корзины\n            }\n            // Записать первую встретившуюся метку удаления\n            if first_tombstone == -1 && self.buckets[index] == self.TOMBSTONE {\n                first_tombstone = index as i32;\n            }\n            // Вычислить индекс корзины; при выходе за конец вернуться к началу\n            index = (index + 1) % self.capacity;\n        }\n        // Если key не существует, вернуть индекс точки добавления\n        if first_tombstone == -1 {\n            index\n        } else {\n            first_tombstone as usize\n        }\n    }\n\n    /* Операция поиска */\n    fn get(&mut self, key: i32) -> Option<&str> {\n        // Найти индекс корзины, соответствующий key\n        let index = self.find_bucket(key);\n        // Если пара ключ-значение найдена, вернуть соответствующее val\n        if self.buckets[index].is_some() && self.buckets[index] != self.TOMBSTONE {\n            return self.buckets[index].as_ref().map(|pair| &pair.val as &str);\n        }\n        // Если пары ключ-значение не существует, вернуть null\n        None\n    }\n\n    /* Операция добавления */\n    fn put(&mut self, key: i32, val: String) {\n        // Когда коэффициент загрузки превышает порог, выполнить расширение\n        if self.load_factor() > self.load_thres {\n            self.extend();\n        }\n        // Найти индекс корзины, соответствующий key\n        let index = self.find_bucket(key);\n        // Если пара ключ-значение найдена, перезаписать val и вернуть\n        if self.buckets[index].is_some() && self.buckets[index] != self.TOMBSTONE {\n            self.buckets[index].as_mut().unwrap().val = val;\n            return;\n        }\n        // Если пары ключ-значение нет, добавить ее\n        self.buckets[index] = Some(Pair { key, val });\n        self.size += 1;\n    }\n\n    /* Операция удаления */\n    fn remove(&mut self, key: i32) {\n        // Найти индекс корзины, соответствующий key\n        let index = self.find_bucket(key);\n        // Если пара ключ-значение найдена, заменить ее меткой удаления\n        if self.buckets[index].is_some() && self.buckets[index] != self.TOMBSTONE {\n            self.buckets[index] = self.TOMBSTONE.clone();\n            self.size -= 1;\n        }\n    }\n\n    /* Расширить хеш-таблицу */\n    fn extend(&mut self) {\n        // Временно сохранить исходную хеш-таблицу\n        let buckets_tmp = self.buckets.clone();\n        // Инициализация новой хеш-таблицы после расширения\n        self.capacity *= self.extend_ratio;\n        self.buckets = vec![None; self.capacity];\n        self.size = 0;\n\n        // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n        for pair in buckets_tmp {\n            if pair.is_none() || pair == self.TOMBSTONE {\n                continue;\n            }\n            let pair = pair.unwrap();\n\n            self.put(pair.key, pair.val);\n        }\n    }\n    /* Вывести хеш-таблицу */\n    fn print(&self) {\n        for pair in &self.buckets {\n            if pair.is_none() {\n                println!(\"null\");\n            } else if pair == &self.TOMBSTONE {\n                println!(\"TOMBSTONE\");\n            } else {\n                let pair = pair.as_ref().unwrap();\n                println!(\"{} -> {}\", pair.key, pair.val);\n            }\n        }\n    }\n}\n
        hash_map_open_addressing.c
        /* Хеш-таблица с открытой адресацией */\ntypedef struct {\n    int size;         // Число пар ключ-значение\n    int capacity;     // Вместимость хеш-таблицы\n    double loadThres; // Порог коэффициента загрузки для запуска расширения\n    int extendRatio;  // Коэффициент расширения\n    Pair **buckets;   // Массив корзин\n    Pair *TOMBSTONE;  // Удалить метку\n} HashMapOpenAddressing;\n\n/* Конструктор */\nHashMapOpenAddressing *newHashMapOpenAddressing() {\n    HashMapOpenAddressing *hashMap = (HashMapOpenAddressing *)malloc(sizeof(HashMapOpenAddressing));\n    hashMap->size = 0;\n    hashMap->capacity = 4;\n    hashMap->loadThres = 2.0 / 3.0;\n    hashMap->extendRatio = 2;\n    hashMap->buckets = (Pair **)calloc(hashMap->capacity, sizeof(Pair *));\n    hashMap->TOMBSTONE = (Pair *)malloc(sizeof(Pair));\n    hashMap->TOMBSTONE->key = -1;\n    hashMap->TOMBSTONE->val = \"-1\";\n\n    return hashMap;\n}\n\n/* Деструктор */\nvoid delHashMapOpenAddressing(HashMapOpenAddressing *hashMap) {\n    for (int i = 0; i < hashMap->capacity; i++) {\n        Pair *pair = hashMap->buckets[i];\n        if (pair != NULL && pair != hashMap->TOMBSTONE) {\n            free(pair->val);\n            free(pair);\n        }\n    }\n    free(hashMap->buckets);\n    free(hashMap->TOMBSTONE);\n    free(hashMap);\n}\n\n/* Хеш-функция */\nint hashFunc(HashMapOpenAddressing *hashMap, int key) {\n    return key % hashMap->capacity;\n}\n\n/* Коэффициент загрузки */\ndouble loadFactor(HashMapOpenAddressing *hashMap) {\n    return (double)hashMap->size / (double)hashMap->capacity;\n}\n\n/* Найти индекс корзины, соответствующий key */\nint findBucket(HashMapOpenAddressing *hashMap, int key) {\n    int index = hashFunc(hashMap, key);\n    int firstTombstone = -1;\n    // Выполнять линейное пробирование и завершить при встрече с пустой корзиной\n    while (hashMap->buckets[index] != NULL) {\n        // Если встретился key, вернуть соответствующий индекс корзины\n        if (hashMap->buckets[index]->key == key) {\n            // Если ранее встретилась метка удаления, переместить пару ключ-значение на этот индекс\n            if (firstTombstone != -1) {\n                hashMap->buckets[firstTombstone] = hashMap->buckets[index];\n                hashMap->buckets[index] = hashMap->TOMBSTONE;\n                return firstTombstone; // Вернуть индекс корзины после перемещения\n            }\n            return index; // Вернуть индекс корзины\n        }\n        // Записать первую встретившуюся метку удаления\n        if (firstTombstone == -1 && hashMap->buckets[index] == hashMap->TOMBSTONE) {\n            firstTombstone = index;\n        }\n        // Вычислить индекс корзины; при выходе за конец вернуться к началу\n        index = (index + 1) % hashMap->capacity;\n    }\n    // Если key не существует, вернуть индекс точки добавления\n    return firstTombstone == -1 ? index : firstTombstone;\n}\n\n/* Операция поиска */\nchar *get(HashMapOpenAddressing *hashMap, int key) {\n    // Найти индекс корзины, соответствующий key\n    int index = findBucket(hashMap, key);\n    // Если пара ключ-значение найдена, вернуть соответствующее val\n    if (hashMap->buckets[index] != NULL && hashMap->buckets[index] != hashMap->TOMBSTONE) {\n        return hashMap->buckets[index]->val;\n    }\n    // Если пары ключ-значение не существует, вернуть пустую строку\n    return \"\";\n}\n\n/* Операция добавления */\nvoid put(HashMapOpenAddressing *hashMap, int key, char *val) {\n    // Когда коэффициент загрузки превышает порог, выполнить расширение\n    if (loadFactor(hashMap) > hashMap->loadThres) {\n        extend(hashMap);\n    }\n    // Найти индекс корзины, соответствующий key\n    int index = findBucket(hashMap, key);\n    // Если пара ключ-значение найдена, перезаписать val и вернуть\n    if (hashMap->buckets[index] != NULL && hashMap->buckets[index] != hashMap->TOMBSTONE) {\n        free(hashMap->buckets[index]->val);\n        hashMap->buckets[index]->val = (char *)malloc(sizeof(strlen(val) + 1));\n        strcpy(hashMap->buckets[index]->val, val);\n        hashMap->buckets[index]->val[strlen(val)] = '\\0';\n        return;\n    }\n    // Если пары ключ-значение нет, добавить ее\n    Pair *pair = (Pair *)malloc(sizeof(Pair));\n    pair->key = key;\n    pair->val = (char *)malloc(sizeof(strlen(val) + 1));\n    strcpy(pair->val, val);\n    pair->val[strlen(val)] = '\\0';\n\n    hashMap->buckets[index] = pair;\n    hashMap->size++;\n}\n\n/* Операция удаления */\nvoid removeItem(HashMapOpenAddressing *hashMap, int key) {\n    // Найти индекс корзины, соответствующий key\n    int index = findBucket(hashMap, key);\n    // Если пара ключ-значение найдена, заменить ее меткой удаления\n    if (hashMap->buckets[index] != NULL && hashMap->buckets[index] != hashMap->TOMBSTONE) {\n        Pair *pair = hashMap->buckets[index];\n        free(pair->val);\n        free(pair);\n        hashMap->buckets[index] = hashMap->TOMBSTONE;\n        hashMap->size--;\n    }\n}\n\n/* Расширить хеш-таблицу */\nvoid extend(HashMapOpenAddressing *hashMap) {\n    // Временно сохранить исходную хеш-таблицу\n    Pair **bucketsTmp = hashMap->buckets;\n    int oldCapacity = hashMap->capacity;\n    // Инициализация новой хеш-таблицы после расширения\n    hashMap->capacity *= hashMap->extendRatio;\n    hashMap->buckets = (Pair **)calloc(hashMap->capacity, sizeof(Pair *));\n    hashMap->size = 0;\n    // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n    for (int i = 0; i < oldCapacity; i++) {\n        Pair *pair = bucketsTmp[i];\n        if (pair != NULL && pair != hashMap->TOMBSTONE) {\n            put(hashMap, pair->key, pair->val);\n            free(pair->val);\n            free(pair);\n        }\n    }\n    free(bucketsTmp);\n}\n\n/* Вывести хеш-таблицу */\nvoid print(HashMapOpenAddressing *hashMap) {\n    for (int i = 0; i < hashMap->capacity; i++) {\n        Pair *pair = hashMap->buckets[i];\n        if (pair == NULL) {\n            printf(\"NULL\\n\");\n        } else if (pair == hashMap->TOMBSTONE) {\n            printf(\"TOMBSTONE\\n\");\n        } else {\n            printf(\"%d -> %s\\n\", pair->key, pair->val);\n        }\n    }\n}\n
        hash_map_open_addressing.kt
        /* Хеш-таблица с открытой адресацией */\nclass HashMapOpenAddressing {\n    private var size: Int               // Число пар ключ-значение\n    private var capacity: Int           // Вместимость хеш-таблицы\n    private val loadThres: Double       // Порог коэффициента загрузки для запуска расширения\n    private val extendRatio: Int        // Коэффициент расширения\n    private var buckets: Array<Pair?>   // Массив корзин\n    private val TOMBSTONE: Pair         // Удалить метку\n\n    /* Конструктор */\n    init {\n        size = 0\n        capacity = 4\n        loadThres = 2.0 / 3.0\n        extendRatio = 2\n        buckets = arrayOfNulls(capacity)\n        TOMBSTONE = Pair(-1, \"-1\")\n    }\n\n    /* Хеш-функция */\n    fun hashFunc(key: Int): Int {\n        return key % capacity\n    }\n\n    /* Коэффициент загрузки */\n    fun loadFactor(): Double {\n        return (size / capacity).toDouble()\n    }\n\n    /* Найти индекс корзины, соответствующий key */\n    fun findBucket(key: Int): Int {\n        var index = hashFunc(key)\n        var firstTombstone = -1\n        // Выполнять линейное пробирование и завершить при встрече с пустой корзиной\n        while (buckets[index] != null) {\n            // Если встретился key, вернуть соответствующий индекс корзины\n            if (buckets[index]?.key == key) {\n                // Если ранее встретилась метка удаления, переместить пару ключ-значение на этот индекс\n                if (firstTombstone != -1) {\n                    buckets[firstTombstone] = buckets[index]\n                    buckets[index] = TOMBSTONE\n                    return firstTombstone // Вернуть индекс корзины после перемещения\n                }\n                return index // Вернуть индекс корзины\n            }\n            // Записать первую встретившуюся метку удаления\n            if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {\n                firstTombstone = index\n            }\n            // Вычислить индекс корзины; при выходе за конец вернуться к началу\n            index = (index + 1) % capacity\n        }\n        // Если key не существует, вернуть индекс точки добавления\n        return if (firstTombstone == -1) index else firstTombstone\n    }\n\n    /* Операция поиска */\n    fun get(key: Int): String? {\n        // Найти индекс корзины, соответствующий key\n        val index = findBucket(key)\n        // Если пара ключ-значение найдена, вернуть соответствующее val\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            return buckets[index]?._val\n        }\n        // Если пары ключ-значение не существует, вернуть null\n        return null\n    }\n\n    /* Операция добавления */\n    fun put(key: Int, _val: String) {\n        // Когда коэффициент загрузки превышает порог, выполнить расширение\n        if (loadFactor() > loadThres) {\n            extend()\n        }\n        // Найти индекс корзины, соответствующий key\n        val index = findBucket(key)\n        // Если пара ключ-значение найдена, перезаписать val и вернуть\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index]!!._val = _val\n            return\n        }\n        // Если пары ключ-значение нет, добавить ее\n        buckets[index] = Pair(key, _val)\n        size++\n    }\n\n    /* Операция удаления */\n    fun remove(key: Int) {\n        // Найти индекс корзины, соответствующий key\n        val index = findBucket(key)\n        // Если пара ключ-значение найдена, заменить ее меткой удаления\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index] = TOMBSTONE\n            size--\n        }\n    }\n\n    /* Расширить хеш-таблицу */\n    fun extend() {\n        // Временно сохранить исходную хеш-таблицу\n        val bucketsTmp = buckets\n        // Инициализация новой хеш-таблицы после расширения\n        capacity *= extendRatio\n        buckets = arrayOfNulls(capacity)\n        size = 0\n        // Перенести пары ключ-значение из исходной хеш-таблицы в новую\n        for (pair in bucketsTmp) {\n            if (pair != null && pair != TOMBSTONE) {\n                put(pair.key, pair._val)\n            }\n        }\n    }\n\n    /* Вывести хеш-таблицу */\n    fun print() {\n        for (pair in buckets) {\n            if (pair == null) {\n                println(\"null\")\n            } else if (pair == TOMBSTONE) {\n                println(\"TOMESTOME\")\n            } else {\n                println(\"${pair.key} -> ${pair._val}\")\n            }\n        }\n    }\n}\n
        hash_map_open_addressing.rb
        ### Хеш-таблица с открытой адресацией ###\nclass HashMapOpenAddressing\n  TOMBSTONE = Pair.new(-1, '-1') # Удалить метку\n\n  ### Конструктор ###\n  def initialize\n    @size = 0 # Число пар ключ-значение\n    @capacity = 4 # Вместимость хеш-таблицы\n    @load_thres = 2.0 / 3.0 # Порог коэффициента загрузки для запуска расширения\n    @extend_ratio = 2 # Коэффициент расширения\n    @buckets = Array.new(@capacity) # Массив корзин\n  end\n\n  ### Хеш-функция ###\n  def hash_func(key)\n    key % @capacity\n  end\n\n  ### Коэффициент загрузки ###\n  def load_factor\n    @size / @capacity\n  end\n\n  ### Найти индекс корзины, соответствующий key ###\n  def find_bucket(key)\n    index = hash_func(key)\n    first_tombstone = -1\n    # Выполнять линейное пробирование и завершить при встрече с пустой корзиной\n    while !@buckets[index].nil?\n      # Если встретился key, вернуть соответствующий индекс корзины\n      if @buckets[index].key == key\n        # Если ранее встретилась метка удаления, переместить пару ключ-значение на этот индекс\n        if first_tombstone != -1\n          @buckets[first_tombstone] = @buckets[index]\n          @buckets[index] = TOMBSTONE\n          return first_tombstone # Вернуть индекс корзины после перемещения\n        end\n        return index # Вернуть индекс корзины\n      end\n      # Записать первую встретившуюся метку удаления\n      first_tombstone = index if first_tombstone == -1 && @buckets[index] == TOMBSTONE\n      # Вычислить индекс корзины; при выходе за конец вернуться к началу\n      index = (index + 1) % @capacity\n    end\n    # Если key не существует, вернуть индекс точки добавления\n    first_tombstone == -1 ? index : first_tombstone\n  end\n\n  ### Операция поиска ###\n  def get(key)\n    # Найти индекс корзины, соответствующий key\n    index = find_bucket(key)\n    # Если пара ключ-значение найдена, вернуть соответствующее val\n    return @buckets[index].val unless [nil, TOMBSTONE].include?(@buckets[index])\n    # Если пара ключ-значение не существует, вернуть nil\n    nil\n  end\n\n  ### Операция добавления ###\n  def put(key, val)\n    # Когда коэффициент загрузки превышает порог, выполнить расширение\n    extend if load_factor > @load_thres\n    # Найти индекс корзины, соответствующий key\n    index = find_bucket(key)\n    # Если пара ключ-значение найдена, перезаписать val и вернуть\n    unless [nil, TOMBSTONE].include?(@buckets[index])\n      @buckets[index].val = val\n      return\n    end\n    # Если пары ключ-значение нет, добавить ее\n    @buckets[index] = Pair.new(key, val)\n    @size += 1\n  end\n\n  ### Операция удаления ###\n  def remove(key)\n    # Найти индекс корзины, соответствующий key\n    index = find_bucket(key)\n    # Если пара ключ-значение найдена, заменить ее меткой удаления\n    unless [nil, TOMBSTONE].include?(@buckets[index])\n      @buckets[index] = TOMBSTONE\n      @size -= 1\n    end\n  end\n\n  ### Расширение хеш-таблицы ###\n  def extend\n    # Временно сохранить исходную хеш-таблицу\n    buckets_tmp = @buckets\n    # Инициализация новой хеш-таблицы после расширения\n    @capacity *= @extend_ratio\n    @buckets = Array.new(@capacity)\n    @size = 0\n    # Перенести пары ключ-значение из исходной хеш-таблицы в новую\n    for pair in buckets_tmp\n      put(pair.key, pair.val) unless [nil, TOMBSTONE].include?(pair)\n    end\n  end\n\n  ### Вывести хеш-таблицу ###\n  def print\n    for pair in @buckets\n      if pair.nil?\n        puts \"Nil\"\n      elsif pair == TOMBSTONE\n        puts \"TOMBSTONE\"\n      else\n        puts \"#{pair.key} -> #{pair.val}\"\n      end\n    end\n  end\nend\n
        ","path":["Глава 6. Хеш-таблицы","6.2   Хеш-коллизии"],"tags":[]},{"location":"chapter_hashing/hash_collision/#2","level":3,"title":"2.   Квадратичное пробирование","text":"

        Квадратичное пробирование похоже на линейное пробирование и тоже является одной из распространенных стратегий открытой адресации. При возникновении конфликта оно не пропускает фиксированное число шагов, а переходит на расстояние, равное «квадрату числа попыток», то есть на \\(1, 4, 9, \\dots\\) шагов.

        Квадратичное пробирование имеет следующие основные преимущества.

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

        Однако квадратичное пробирование не является идеальным.

        • Кластеризация все равно существует: некоторые позиции по-прежнему занимают чаще других.
        • Из-за быстрого роста квадрата квадратичное пробирование может не охватить всю хеш-таблицу, а это означает, что даже при наличии пустых бакетов оно может так до них и не добраться.
        ","path":["Глава 6. Хеш-таблицы","6.2   Хеш-коллизии"],"tags":[]},{"location":"chapter_hashing/hash_collision/#3","level":3,"title":"3.   Повторное хеширование","text":"

        Как видно из названия, метод повторного хеширования использует для пробирования несколько хеш-функций \\(f_1(x)\\), \\(f_2(x)\\), \\(f_3(x)\\), \\(\\dots\\) .

        • Вставка элемента: если хеш-функция \\(f_1(x)\\) вызывает конфликт, то пробуем \\(f_2(x)\\) , и так далее, пока не будет найдено пустое место для вставки элемента.
        • Поиск элемента: поиск идет в том же порядке хеш-функций, пока не будет найден целевой элемент. Если встречается пустая позиция или уже были опробованы все хеш-функции, это означает, что элемента в хеш-таблице нет, и возвращается None .

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

        Tip

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

        ","path":["Глава 6. Хеш-таблицы","6.2   Хеш-коллизии"],"tags":[]},{"location":"chapter_hashing/hash_collision/#623","level":2,"title":"6.2.3   Выбор в языках программирования","text":"

        Разные языки программирования используют разные стратегии реализации хеш-таблиц. Ниже приведено несколько примеров.

        • Python использует открытую адресацию. В словаре dict для пробирования применяются псевдослучайные числа.
        • Java использует метод цепочек. Начиная с JDK 1.8, когда длина массива внутри HashMap достигает 64, а длина списка достигает 8, этот список преобразуется в красно-черное дерево для повышения производительности поиска.
        • Go использует метод цепочек. В Go установлено, что каждый бакет может хранить не более 8 пар ключ-значение. При переполнении подключается overflow-бакет, а когда таких бакетов становится слишком много, выполняется специальное расширение того же масштаба, чтобы сохранить производительность.
        ","path":["Глава 6. Хеш-таблицы","6.2   Хеш-коллизии"],"tags":[]},{"location":"chapter_hashing/hash_map/","level":1,"title":"6.1   Хеш-таблица","text":"

        Хеш-таблица (hash table), также называемая таблицей рассеяния, реализует эффективный поиск элементов за счет установления соответствия между ключом key и значением value . Иначе говоря, если передать в хеш-таблицу ключ key , то можно за \\(O(1)\\) времени получить соответствующее значение value .

        Как показано на рисунке 6-1, пусть есть \\(n\\) студентов, и у каждого из них есть два поля данных: имя и номер студенческого билета. Если мы хотим реализовать запрос вида «ввести номер студенческого билета и вернуть соответствующее имя», то для этого можно воспользоваться хеш-таблицей, изображенной на рисунке 6-1.

        Рисунок 6-1   Абстрактное представление хеш-таблицы

        Помимо хеш-таблицы, функцией поиска также обладают массив и связный список. Сравнение их эффективности приведено в таблице 6-1.

        • Добавление элемента: нужно лишь добавить элемент в конец массива (или списка), что занимает \\(O(1)\\) времени.
        • Поиск элемента: так как массив (или список) неупорядочен, приходится обходить все элементы, что занимает \\(O(n)\\) времени.
        • Удаление элемента: сначала нужно найти элемент, затем удалить его из массива (или списка), что занимает \\(O(n)\\) времени.

        Таблица 6-1   Сравнение эффективности поиска элементов

        Массив Связный список Хеш-таблица Поиск элемента \\(O(n)\\) \\(O(n)\\) \\(O(1)\\) Добавление элемента \\(O(1)\\) \\(O(1)\\) \\(O(1)\\) Удаление элемента \\(O(n)\\) \\(O(n)\\) \\(O(1)\\)

        Нетрудно заметить, что операции поиска, добавления и удаления в хеш-таблице имеют временную сложность \\(O(1)\\) , то есть выполняются очень эффективно.

        ","path":["Глава 6. Хеш-таблицы","6.1   Хеш-таблица"],"tags":[]},{"location":"chapter_hashing/hash_map/#611-","level":2,"title":"6.1.1   Основные операции с хеш-таблицей","text":"

        К базовым операциям хеш-таблицы относятся инициализация, поиск, добавление пар ключ-значение и удаление пар ключ-значение. Пример кода приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map.py
        # Инициализация хеш-таблицы\nhmap: dict = {}\n\n# Операция добавления\n# Добавить пару ключ-значение (key, value) в хеш-таблицу\nhmap[12836] = \"Сяо Ха\"\nhmap[15937] = \"Сяо Ло\"\nhmap[16750] = \"Сяо Суань\"\nhmap[13276] = \"Сяо Фа\"\nhmap[10583] = \"Сяо Я\"\n\n# Операция поиска\n# Передать в хеш-таблицу ключ key и получить значение value\nname: str = hmap[15937]\n\n# Операция удаления\n# Удалить пару ключ-значение (key, value) из хеш-таблицы\nhmap.pop(10583)\n
        hash_map.cpp
        /* Инициализация хеш-таблицы */\nunordered_map<int, string> map;\n\n/* Операция добавления */\n// Добавить пару ключ-значение (key, value) в хеш-таблицу\nmap[12836] = \"Сяо Ха\";\nmap[15937] = \"Сяо Ло\";\nmap[16750] = \"Сяо Суань\";\nmap[13276] = \"Сяо Фа\";\nmap[10583] = \"Сяо Я\";\n\n/* Операция поиска */\n// Передать в хеш-таблицу ключ key и получить значение value\nstring name = map[15937];\n\n/* Операция удаления */\n// Удалить пару ключ-значение (key, value) из хеш-таблицы\nmap.erase(10583);\n
        hash_map.java
        /* Инициализация хеш-таблицы */\nMap<Integer, String> map = new HashMap<>();\n\n/* Операция добавления */\n// Добавить пару ключ-значение (key, value) в хеш-таблицу\nmap.put(12836, \"Сяо Ха\");\nmap.put(15937, \"Сяо Ло\");\nmap.put(16750, \"Сяо Суань\");\nmap.put(13276, \"Сяо Фа\");\nmap.put(10583, \"Сяо Я\");\n\n/* Операция поиска */\n// Передать в хеш-таблицу ключ key и получить значение value\nString name = map.get(15937);\n\n/* Операция удаления */\n// Удалить пару ключ-значение (key, value) из хеш-таблицы\nmap.remove(10583);\n
        hash_map.cs
        /* Инициализация хеш-таблицы */\nDictionary<int, string> map = new() {\n    /* Операция добавления */\n    // Добавить пару ключ-значение (key, value) в хеш-таблицу\n    { 12836, \"Сяо Ха\" },\n    { 15937, \"Сяо Ло\" },\n    { 16750, \"Сяо Суань\" },\n    { 13276, \"Сяо Фа\" },\n    { 10583, \"Сяо Я\" }\n};\n\n/* Операция поиска */\n// Передать в хеш-таблицу ключ key и получить значение value\nstring name = map[15937];\n\n/* Операция удаления */\n// Удалить пару ключ-значение (key, value) из хеш-таблицы\nmap.Remove(10583);\n
        hash_map_test.go
        /* Инициализация хеш-таблицы */\nhmap := make(map[int]string)\n\n/* Операция добавления */\n// Добавить пару ключ-значение (key, value) в хеш-таблицу\nhmap[12836] = \"Сяо Ха\"\nhmap[15937] = \"Сяо Ло\"\nhmap[16750] = \"Сяо Суань\"\nhmap[13276] = \"Сяо Фа\"\nhmap[10583] = \"Сяо Я\"\n\n/* Операция поиска */\n// Передать в хеш-таблицу ключ key и получить значение value\nname := hmap[15937]\n\n/* Операция удаления */\n// Удалить пару ключ-значение (key, value) из хеш-таблицы\ndelete(hmap, 10583)\n
        hash_map.swift
        /* Инициализация хеш-таблицы */\nvar map: [Int: String] = [:]\n\n/* Операция добавления */\n// Добавить пару ключ-значение (key, value) в хеш-таблицу\nmap[12836] = \"Сяо Ха\"\nmap[15937] = \"Сяо Ло\"\nmap[16750] = \"Сяо Суань\"\nmap[13276] = \"Сяо Фа\"\nmap[10583] = \"Сяо Я\"\n\n/* Операция поиска */\n// Передать в хеш-таблицу ключ key и получить значение value\nlet name = map[15937]!\n\n/* Операция удаления */\n// Удалить пару ключ-значение (key, value) из хеш-таблицы\nmap.removeValue(forKey: 10583)\n
        hash_map.js
        /* Инициализация хеш-таблицы */\nconst map = new Map();\n/* Операция добавления */\n// Добавить пару ключ-значение (key, value) в хеш-таблицу\nmap.set(12836, 'Сяо Ха');\nmap.set(15937, 'Сяо Ло');\nmap.set(16750, 'Сяо Суань');\nmap.set(13276, 'Сяо Фа');\nmap.set(10583, 'Сяо Я');\n\n/* Операция поиска */\n// Передать в хеш-таблицу ключ key и получить значение value\nlet name = map.get(15937);\n\n/* Операция удаления */\n// Удалить пару ключ-значение (key, value) из хеш-таблицы\nmap.delete(10583);\n
        hash_map.ts
        /* Инициализация хеш-таблицы */\nconst map = new Map<number, string>();\n/* Операция добавления */\n// Добавить пару ключ-значение (key, value) в хеш-таблицу\nmap.set(12836, 'Сяо Ха');\nmap.set(15937, 'Сяо Ло');\nmap.set(16750, 'Сяо Суань');\nmap.set(13276, 'Сяо Фа');\nmap.set(10583, 'Сяо Я');\nconsole.info('\\nПосле добавления хеш-таблица имеет вид\\nKey -> Value');\nconsole.info(map);\n\n/* Операция поиска */\n// Передать в хеш-таблицу ключ key и получить значение value\nlet name = map.get(15937);\nconsole.info('\\nПо номеру 15937 найдено имя ' + name);\n\n/* Операция удаления */\n// Удалить пару ключ-значение (key, value) из хеш-таблицы\nmap.delete(10583);\nconsole.info('\\nПосле удаления 10583 хеш-таблица имеет вид\\nKey -> Value');\nconsole.info(map);\n
        hash_map.dart
        /* Инициализация хеш-таблицы */\nMap<int, String> map = {};\n\n/* Операция добавления */\n// Добавить пару ключ-значение (key, value) в хеш-таблицу\nmap[12836] = \"Сяо Ха\";\nmap[15937] = \"Сяо Ло\";\nmap[16750] = \"Сяо Суань\";\nmap[13276] = \"Сяо Фа\";\nmap[10583] = \"Сяо Я\";\n\n/* Операция поиска */\n// Передать в хеш-таблицу ключ key и получить значение value\nString name = map[15937];\n\n/* Операция удаления */\n// Удалить пару ключ-значение (key, value) из хеш-таблицы\nmap.remove(10583);\n
        hash_map.rs
        use std::collections::HashMap;\n\n/* Инициализация хеш-таблицы */\nlet mut map: HashMap<i32, String> = HashMap::new();\n\n/* Операция добавления */\n// Добавить пару ключ-значение (key, value) в хеш-таблицу\nmap.insert(12836, \"Сяо Ха\".to_string());\nmap.insert(15937, \"Сяо Ло\".to_string());\nmap.insert(16750, \"Сяо Суань\".to_string());\nmap.insert(13279, \"Сяо Фа\".to_string());\nmap.insert(10583, \"Сяо Я\".to_string());\n\n/* Операция поиска */\n// Передать в хеш-таблицу ключ key и получить значение value\nlet _name: Option<&String> = map.get(&15937);\n\n/* Операция удаления */\n// Удалить пару ключ-значение (key, value) из хеш-таблицы\nlet _removed_value: Option<String> = map.remove(&10583);\n
        hash_map.c
        // В C нет встроенной хеш-таблицы\n
        hash_map.kt
        /* Инициализация хеш-таблицы */\nval map = HashMap<Int,String>()\n\n/* Операция добавления */\n// Добавить пару ключ-значение (key, value) в хеш-таблицу\nmap[12836] = \"Сяо Ха\"\nmap[15937] = \"Сяо Ло\"\nmap[16750] = \"Сяо Суань\"\nmap[13276] = \"Сяо Фа\"\nmap[10583] = \"Сяо Я\"\n\n/* Операция поиска */\n// Передать в хеш-таблицу ключ key и получить значение value\nval name = map[15937]\n\n/* Операция удаления */\n// Удалить пару ключ-значение (key, value) из хеш-таблицы\nmap.remove(10583)\n
        hash_map.rb
        # Инициализация хеш-таблицы\nhmap = {}\n\n# Операция добавления\n# Добавить пару ключ-значение (key, value) в хеш-таблицу\nhmap[12836] = \"Сяо Ха\"\nhmap[15937] = \"Сяо Ло\"\nhmap[16750] = \"Сяо Суань\"\nhmap[13276] = \"Сяо Фа\"\nhmap[10583] = \"Сяо Я\"\n\n# Операция поиска\n# Передать в хеш-таблицу ключ key и получить значение value\nname = hmap[15937]\n\n# Операция удаления\n# Удалить пару ключ-значение (key, value) из хеш-таблицы\nhmap.delete(10583)\n
        Визуализация выполнения

        https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%85%D0%B5%D1%88-%D1%82%D0%B0%D0%B1%D0%BB%D0%B8%D1%86%D1%83%0A%20%20%20%20hmap%20%3D%20%7B%7D%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%9E%D0%BF%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D1%8F%20%D0%B4%D0%BE%D0%B1%D0%B0%D0%B2%D0%BB%D0%B5%D0%BD%D0%B8%D1%8F%0A%20%20%20%20%23%20%D0%94%D0%BE%D0%B1%D0%B0%D0%B2%D0%B8%D1%82%D1%8C%20%D0%B2%20%D1%85%D0%B5%D1%88-%D1%82%D0%B0%D0%B1%D0%BB%D0%B8%D1%86%D1%83%20%D0%BF%D0%B0%D1%80%D1%83%20%D0%BA%D0%BB%D1%8E%D1%87-%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%28key%2C%20value%29%0A%20%20%20%20hmap%5B12836%5D%20%3D%20%22%D0%A1%D1%8F%D0%BE%20%D0%A5%D0%B0%22%0A%20%20%20%20hmap%5B15937%5D%20%3D%20%22%D0%A1%D1%8F%D0%BE%20%D0%9B%D0%BE%22%0A%20%20%20%20hmap%5B16750%5D%20%3D%20%22%D0%A1%D1%8F%D0%BE%20%D0%A1%D1%83%D0%B0%D0%BD%D1%8C%22%0A%20%20%20%20hmap%5B13276%5D%20%3D%20%22%D0%A1%D1%8F%D0%BE%20%D0%A4%D0%B0%22%0A%20%20%20%20hmap%5B10583%5D%20%3D%20%22%D0%A3%D1%82%D0%B5%D0%BD%D0%BE%D0%BA%22%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%9E%D0%BF%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D1%8F%20%D0%BF%D0%BE%D0%B8%D1%81%D0%BA%D0%B0%0A%20%20%20%20%23%20%D0%9F%D0%B5%D1%80%D0%B5%D0%B4%D0%B0%D1%82%D1%8C%20%D0%BA%D0%BB%D1%8E%D1%87%20key%20%D0%B2%20%D1%85%D0%B5%D1%88-%D1%82%D0%B0%D0%B1%D0%BB%D0%B8%D1%86%D1%83%20%D0%B8%20%D0%BF%D0%BE%D0%BB%D1%83%D1%87%D0%B8%D1%82%D1%8C%20%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20value%0A%20%20%20%20name%20%3D%20hmap%5B15937%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%9E%D0%BF%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D1%8F%20%D1%83%D0%B4%D0%B0%D0%BB%D0%B5%D0%BD%D0%B8%D1%8F%0A%20%20%20%20%23%20%D0%A3%D0%B4%D0%B0%D0%BB%D0%B8%D1%82%D1%8C%20%D0%B8%D0%B7%20%D1%85%D0%B5%D1%88-%D1%82%D0%B0%D0%B1%D0%BB%D0%B8%D1%86%D1%8B%20%D0%BF%D0%B0%D1%80%D1%83%20%D0%BA%D0%BB%D1%8E%D1%87-%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%28key%2C%20value%29%0A%20%20%20%20hmap.pop%2810583%29&cumulative=false&curInstr=2&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

        Существует три распространенных способа обхода хеш-таблицы: обход пар ключ-значение, обход ключей и обход значений. Примеры кода приведены ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map.py
        # Обход хеш-таблицы\n# Обход пар ключ-значение key->value\nfor key, value in hmap.items():\n    print(key, \"->\", value)\n# Обход только ключей key\nfor key in hmap.keys():\n    print(key)\n# Обход только значений value\nfor value in hmap.values():\n    print(value)\n
        hash_map.cpp
        /* Обход хеш-таблицы */\n// Обход пар ключ-значение key->value\nfor (auto kv: map) {\n    cout << kv.first << \" -> \" << kv.second << endl;\n}\n// Обход key->value с помощью итератора\nfor (auto iter = map.begin(); iter != map.end(); iter++) {\n    cout << iter->first << \"->\" << iter->second << endl;\n}\n
        hash_map.java
        /* Обход хеш-таблицы */\n// Обход пар ключ-значение key->value\nfor (Map.Entry <Integer, String> kv: map.entrySet()) {\n    System.out.println(kv.getKey() + \" -> \" + kv.getValue());\n}\n// Обход только ключей key\nfor (int key: map.keySet()) {\n    System.out.println(key);\n}\n// Обход только значений value\nfor (String val: map.values()) {\n    System.out.println(val);\n}\n
        hash_map.cs
        /* Обход хеш-таблицы */\n// Обход пар ключ-значение Key->Value\nforeach (var kv in map) {\n    Console.WriteLine(kv.Key + \" -> \" + kv.Value);\n}\n// Обход только ключей key\nforeach (int key in map.Keys) {\n    Console.WriteLine(key);\n}\n// Обход только значений value\nforeach (string val in map.Values) {\n    Console.WriteLine(val);\n}\n
        hash_map_test.go
        /* Обход хеш-таблицы */\n// Обход пар ключ-значение key->value\nfor key, value := range hmap {\n    fmt.Println(key, \"->\", value)\n}\n// Обход только ключей key\nfor key := range hmap {\n    fmt.Println(key)\n}\n// Обход только значений value\nfor _, value := range hmap {\n    fmt.Println(value)\n}\n
        hash_map.swift
        /* Обход хеш-таблицы */\n// Обход пар ключ-значение Key->Value\nfor (key, value) in map {\n    print(\"\\(key) -> \\(value)\")\n}\n// Обход только ключей Key\nfor key in map.keys {\n    print(key)\n}\n// Обход только значений Value\nfor value in map.values {\n    print(value)\n}\n
        hash_map.js
        /* Обход хеш-таблицы */\nconsole.info('\\nОбход пар ключ-значение Key->Value');\nfor (const [k, v] of map.entries()) {\n    console.info(k + ' -> ' + v);\n}\nconsole.info('\\nОбход только ключей Key');\nfor (const k of map.keys()) {\n    console.info(k);\n}\nconsole.info('\\nОбход только значений Value');\nfor (const v of map.values()) {\n    console.info(v);\n}\n
        hash_map.ts
        /* Обход хеш-таблицы */\nconsole.info('\\nОбход пар ключ-значение Key->Value');\nfor (const [k, v] of map.entries()) {\n    console.info(k + ' -> ' + v);\n}\nconsole.info('\\nОбход только ключей Key');\nfor (const k of map.keys()) {\n    console.info(k);\n}\nconsole.info('\\nОбход только значений Value');\nfor (const v of map.values()) {\n    console.info(v);\n}\n
        hash_map.dart
        /* Обход хеш-таблицы */\n// Обход пар ключ-значение Key->Value\nmap.forEach((key, value) {\n  print('$key -> $value');\n});\n\n// Обход только ключей Key\nmap.keys.forEach((key) {\n  print(key);\n});\n\n// Обход только значений Value\nmap.values.forEach((value) {\n  print(value);\n});\n
        hash_map.rs
        /* Обход хеш-таблицы */\n// Обход пар ключ-значение Key->Value\nfor (key, value) in &map {\n    println!(\"{key} -> {value}\");\n}\n\n// Обход только ключей Key\nfor key in map.keys() {\n    println!(\"{key}\");\n}\n\n// Обход только значений Value\nfor value in map.values() {\n    println!(\"{value}\");\n}\n
        hash_map.c
        // В C нет встроенной хеш-таблицы\n
        hash_map.kt
        /* Обход хеш-таблицы */\n// Обход пар ключ-значение key->value\nfor ((key, value) in map) {\n    println(\"$key -> $value\")\n}\n// Обход только ключей key\nfor (key in map.keys) {\n    println(key)\n}\n// Обход только значений value\nfor (_val in map.values) {\n    println(_val)\n}\n
        hash_map.rb
        # Обход хеш-таблицы\n# Обход пар ключ-значение key->value\nhmap.entries.each { |key, value| puts \"#{key} -> #{value}\" }\n\n# Обход только ключей key\nhmap.keys.each { |key| puts key }\n\n# Обход только значений value\nhmap.values.each { |val| puts val }\n
        Визуализация выполнения

        https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%85%D0%B5%D1%88-%D1%82%D0%B0%D0%B1%D0%BB%D0%B8%D1%86%D1%83%0A%20%20%20%20hmap%20%3D%20%7B%7D%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%9E%D0%BF%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D1%8F%20%D0%B4%D0%BE%D0%B1%D0%B0%D0%B2%D0%BB%D0%B5%D0%BD%D0%B8%D1%8F%0A%20%20%20%20%23%20%D0%94%D0%BE%D0%B1%D0%B0%D0%B2%D0%B8%D1%82%D1%8C%20%D0%B2%20%D1%85%D0%B5%D1%88-%D1%82%D0%B0%D0%B1%D0%BB%D0%B8%D1%86%D1%83%20%D0%BF%D0%B0%D1%80%D1%83%20%D0%BA%D0%BB%D1%8E%D1%87-%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%28key%2C%20value%29%0A%20%20%20%20hmap%5B12836%5D%20%3D%20%22%D0%A1%D1%8F%D0%BE%20%D0%A5%D0%B0%22%0A%20%20%20%20hmap%5B15937%5D%20%3D%20%22%D0%A1%D1%8F%D0%BE%20%D0%9B%D0%BE%22%0A%20%20%20%20hmap%5B16750%5D%20%3D%20%22%D0%A1%D1%8F%D0%BE%20%D0%A1%D1%83%D0%B0%D0%BD%D1%8C%22%0A%20%20%20%20hmap%5B13276%5D%20%3D%20%22%D0%A1%D1%8F%D0%BE%20%D0%A4%D0%B0%22%0A%20%20%20%20hmap%5B10583%5D%20%3D%20%22%D0%A3%D1%82%D0%B5%D0%BD%D0%BE%D0%BA%22%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%9F%D0%B5%D1%80%D0%B5%D0%B1%D1%80%D0%B0%D1%82%D1%8C%20%D1%85%D0%B5%D1%88-%D1%82%D0%B0%D0%B1%D0%BB%D0%B8%D1%86%D1%83%0A%20%20%20%20%23%20%D0%9E%D0%B1%D0%BE%D0%B9%D1%82%D0%B8%D0%BF%D0%B0%D1%80%D0%B0%20%D0%BA%D0%BB%D1%8E%D1%87-%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20key-%3Evalue%0A%20%20%20%20for%20key%2C%20value%20in%20hmap.items%28%29%3A%0A%20%20%20%20%20%20%20%20print%28key%2C%20%22-%3E%22%2C%20value%29%0A%20%20%20%20%23%20%D0%BE%D1%82%D0%B4%D0%B5%D0%BB%D1%8C%D0%BD%D0%BE%D0%9E%D0%B1%D0%BE%D0%B9%D1%82%D0%B8%D0%BA%D0%BB%D1%8E%D1%87%20key%0A%20%20%20%20for%20key%20in%20hmap.keys%28%29%3A%0A%20%20%20%20%20%20%20%20print%28key%29%0A%20%20%20%20%23%20%D0%BE%D1%82%D0%B4%D0%B5%D0%BB%D1%8C%D0%BD%D0%BE%D0%9E%D0%B1%D0%BE%D0%B9%D1%82%D0%B8%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20value%0A%20%20%20%20for%20value%20in%20hmap.values%28%29%3A%0A%20%20%20%20%20%20%20%20print%28value%29&cumulative=false&curInstr=8&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

        ","path":["Глава 6. Хеш-таблицы","6.1   Хеш-таблица"],"tags":[]},{"location":"chapter_hashing/hash_map/#612-","level":2,"title":"6.1.2   Простая реализация хеш-таблицы","text":"

        Сначала рассмотрим самый простой случай: реализуем хеш-таблицу только с помощью одного массива. В хеш-таблице каждую пустую ячейку массива мы называем бакетом (bucket), и каждый бакет может хранить одну пару ключ-значение. Следовательно, операция поиска сводится к тому, чтобы найти бакет, соответствующий key , и получить из него value .

        Но как определить бакет, соответствующий заданному key ? Это делается с помощью хеш-функции (hash function). Назначение хеш-функции - отображать большое входное пространство в меньшее выходное пространство. В хеш-таблице входным пространством являются все key , а выходным - все бакеты, то есть индексы массива. Иначе говоря, передав key на вход, мы можем с помощью хеш-функции получить позицию хранения соответствующей пары ключ-значение в массиве.

        Процесс вычисления хеш-функции для одного key включает два шага.

        1. Сначала с помощью некоторого хеш-алгоритма hash() вычисляется хеш-значение.
        2. Затем хеш-значение берется по модулю числа бакетов (длины массива) capacity , чтобы получить бакет (индекс массива) index , соответствующий этому key .
        index = hash(key) % capacity\n

        После этого можно использовать index для доступа к соответствующему бакету в хеш-таблице и получения value .

        Пусть длина массива capacity = 100 , а хеш-алгоритм hash(key) = key . Тогда легко получить хеш-функцию key % 100 . На рисунке 6-2 на примере key «номер студенческого билета» и value «имя» показан принцип работы хеш-функции.

        Рисунок 6-2   Принцип работы хеш-функции

        Ниже приведен код простой реализации хеш-таблицы. В нем мы инкапсулируем key и value в класс Pair , чтобы представить пару ключ-значение.

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_hash_map.py
        class Pair:\n    \"\"\"Пара ключ-значение\"\"\"\n\n    def __init__(self, key: int, val: str):\n        self.key = key\n        self.val = val\n\nclass ArrayHashMap:\n    \"\"\"Хеш-таблица на основе массива\"\"\"\n\n    def __init__(self):\n        \"\"\"Конструктор\"\"\"\n        # Инициализировать массив, содержащий 100 корзин\n        self.buckets: list[Pair | None] = [None] * 100\n\n    def hash_func(self, key: int) -> int:\n        \"\"\"Хеш-функция\"\"\"\n        index = key % 100\n        return index\n\n    def get(self, key: int) -> str | None:\n        \"\"\"Операция поиска\"\"\"\n        index: int = self.hash_func(key)\n        pair: Pair = self.buckets[index]\n        if pair is None:\n            return None\n        return pair.val\n\n    def put(self, key: int, val: str):\n        \"\"\"Операции добавления и обновления\"\"\"\n        pair = Pair(key, val)\n        index: int = self.hash_func(key)\n        self.buckets[index] = pair\n\n    def remove(self, key: int):\n        \"\"\"Операция удаления\"\"\"\n        index: int = self.hash_func(key)\n        # Присвоить None, что означает удаление\n        self.buckets[index] = None\n\n    def entry_set(self) -> list[Pair]:\n        \"\"\"Получить все пары ключ-значение\"\"\"\n        result: list[Pair] = []\n        for pair in self.buckets:\n            if pair is not None:\n                result.append(pair)\n        return result\n\n    def key_set(self) -> list[int]:\n        \"\"\"Получить все ключи\"\"\"\n        result = []\n        for pair in self.buckets:\n            if pair is not None:\n                result.append(pair.key)\n        return result\n\n    def value_set(self) -> list[str]:\n        \"\"\"Получить все значения\"\"\"\n        result = []\n        for pair in self.buckets:\n            if pair is not None:\n                result.append(pair.val)\n        return result\n\n    def print(self):\n        \"\"\"Вывести хеш-таблицу\"\"\"\n        for pair in self.buckets:\n            if pair is not None:\n                print(pair.key, \"->\", pair.val)\n
        array_hash_map.cpp
        /* Пара ключ-значение */\nstruct Pair {\n  public:\n    int key;\n    string val;\n    Pair(int key, string val) {\n        this->key = key;\n        this->val = val;\n    }\n};\n\n/* Хеш-таблица на основе массива */\nclass ArrayHashMap {\n  private:\n    vector<Pair *> buckets;\n\n  public:\n    ArrayHashMap() {\n        // Инициализировать массив, содержащий 100 корзин\n        buckets = vector<Pair *>(100);\n    }\n\n    ~ArrayHashMap() {\n        // Освободить память\n        for (const auto &bucket : buckets) {\n            delete bucket;\n        }\n        buckets.clear();\n    }\n\n    /* Хеш-функция */\n    int hashFunc(int key) {\n        int index = key % 100;\n        return index;\n    }\n\n    /* Операция поиска */\n    string get(int key) {\n        int index = hashFunc(key);\n        Pair *pair = buckets[index];\n        if (pair == nullptr)\n            return \"\";\n        return pair->val;\n    }\n\n    /* Операция добавления */\n    void put(int key, string val) {\n        Pair *pair = new Pair(key, val);\n        int index = hashFunc(key);\n        buckets[index] = pair;\n    }\n\n    /* Операция удаления */\n    void remove(int key) {\n        int index = hashFunc(key);\n        // Освободить память и присвоить nullptr\n        delete buckets[index];\n        buckets[index] = nullptr;\n    }\n\n    /* Получить все пары ключ-значение */\n    vector<Pair *> pairSet() {\n        vector<Pair *> pairSet;\n        for (Pair *pair : buckets) {\n            if (pair != nullptr) {\n                pairSet.push_back(pair);\n            }\n        }\n        return pairSet;\n    }\n\n    /* Получить все ключи */\n    vector<int> keySet() {\n        vector<int> keySet;\n        for (Pair *pair : buckets) {\n            if (pair != nullptr) {\n                keySet.push_back(pair->key);\n            }\n        }\n        return keySet;\n    }\n\n    /* Получить все значения */\n    vector<string> valueSet() {\n        vector<string> valueSet;\n        for (Pair *pair : buckets) {\n            if (pair != nullptr) {\n                valueSet.push_back(pair->val);\n            }\n        }\n        return valueSet;\n    }\n\n    /* Вывести хеш-таблицу */\n    void print() {\n        for (Pair *kv : pairSet()) {\n            cout << kv->key << \" -> \" << kv->val << endl;\n        }\n    }\n};\n
        array_hash_map.java
        /* Пара ключ-значение */\nclass Pair {\n    public int key;\n    public String val;\n\n    public Pair(int key, String val) {\n        this.key = key;\n        this.val = val;\n    }\n}\n\n/* Хеш-таблица на основе массива */\nclass ArrayHashMap {\n    private List<Pair> buckets;\n\n    public ArrayHashMap() {\n        // Инициализировать массив, содержащий 100 корзин\n        buckets = new ArrayList<>();\n        for (int i = 0; i < 100; i++) {\n            buckets.add(null);\n        }\n    }\n\n    /* Хеш-функция */\n    private int hashFunc(int key) {\n        int index = key % 100;\n        return index;\n    }\n\n    /* Операция поиска */\n    public String get(int key) {\n        int index = hashFunc(key);\n        Pair pair = buckets.get(index);\n        if (pair == null)\n            return null;\n        return pair.val;\n    }\n\n    /* Операция добавления */\n    public void put(int key, String val) {\n        Pair pair = new Pair(key, val);\n        int index = hashFunc(key);\n        buckets.set(index, pair);\n    }\n\n    /* Операция удаления */\n    public void remove(int key) {\n        int index = hashFunc(key);\n        // Присвоить null, что означает удаление\n        buckets.set(index, null);\n    }\n\n    /* Получить все пары ключ-значение */\n    public List<Pair> pairSet() {\n        List<Pair> pairSet = new ArrayList<>();\n        for (Pair pair : buckets) {\n            if (pair != null)\n                pairSet.add(pair);\n        }\n        return pairSet;\n    }\n\n    /* Получить все ключи */\n    public List<Integer> keySet() {\n        List<Integer> keySet = new ArrayList<>();\n        for (Pair pair : buckets) {\n            if (pair != null)\n                keySet.add(pair.key);\n        }\n        return keySet;\n    }\n\n    /* Получить все значения */\n    public List<String> valueSet() {\n        List<String> valueSet = new ArrayList<>();\n        for (Pair pair : buckets) {\n            if (pair != null)\n                valueSet.add(pair.val);\n        }\n        return valueSet;\n    }\n\n    /* Вывести хеш-таблицу */\n    public void print() {\n        for (Pair kv : pairSet()) {\n            System.out.println(kv.key + \" -> \" + kv.val);\n        }\n    }\n}\n
        array_hash_map.cs
        /* Пара ключ-значение int->string */\nclass Pair(int key, string val) {\n    public int key = key;\n    public string val = val;\n}\n\n/* Хеш-таблица на основе массива */\nclass ArrayHashMap {\n    List<Pair?> buckets;\n    public ArrayHashMap() {\n        // Инициализировать массив, содержащий 100 корзин\n        buckets = [];\n        for (int i = 0; i < 100; i++) {\n            buckets.Add(null);\n        }\n    }\n\n    /* Хеш-функция */\n    int HashFunc(int key) {\n        int index = key % 100;\n        return index;\n    }\n\n    /* Операция поиска */\n    public string? Get(int key) {\n        int index = HashFunc(key);\n        Pair? pair = buckets[index];\n        if (pair == null) return null;\n        return pair.val;\n    }\n\n    /* Операция добавления */\n    public void Put(int key, string val) {\n        Pair pair = new(key, val);\n        int index = HashFunc(key);\n        buckets[index] = pair;\n    }\n\n    /* Операция удаления */\n    public void Remove(int key) {\n        int index = HashFunc(key);\n        // Присвоить null, что означает удаление\n        buckets[index] = null;\n    }\n\n    /* Получить все пары ключ-значение */\n    public List<Pair> PairSet() {\n        List<Pair> pairSet = [];\n        foreach (Pair? pair in buckets) {\n            if (pair != null)\n                pairSet.Add(pair);\n        }\n        return pairSet;\n    }\n\n    /* Получить все ключи */\n    public List<int> KeySet() {\n        List<int> keySet = [];\n        foreach (Pair? pair in buckets) {\n            if (pair != null)\n                keySet.Add(pair.key);\n        }\n        return keySet;\n    }\n\n    /* Получить все значения */\n    public List<string> ValueSet() {\n        List<string> valueSet = [];\n        foreach (Pair? pair in buckets) {\n            if (pair != null)\n                valueSet.Add(pair.val);\n        }\n        return valueSet;\n    }\n\n    /* Вывести хеш-таблицу */\n    public void Print() {\n        foreach (Pair kv in PairSet()) {\n            Console.WriteLine(kv.key + \" -> \" + kv.val);\n        }\n    }\n}\n
        array_hash_map.go
        /* Пара ключ-значение */\ntype pair struct {\n    key int\n    val string\n}\n\n/* Хеш-таблица на основе массива */\ntype arrayHashMap struct {\n    buckets []*pair\n}\n\n/* Инициализация хеш-таблицы */\nfunc newArrayHashMap() *arrayHashMap {\n    // Инициализировать массив, содержащий 100 корзин\n    buckets := make([]*pair, 100)\n    return &arrayHashMap{buckets: buckets}\n}\n\n/* Хеш-функция */\nfunc (a *arrayHashMap) hashFunc(key int) int {\n    index := key % 100\n    return index\n}\n\n/* Операция поиска */\nfunc (a *arrayHashMap) get(key int) string {\n    index := a.hashFunc(key)\n    pair := a.buckets[index]\n    if pair == nil {\n        return \"Not Found\"\n    }\n    return pair.val\n}\n\n/* Операция добавления */\nfunc (a *arrayHashMap) put(key int, val string) {\n    pair := &pair{key: key, val: val}\n    index := a.hashFunc(key)\n    a.buckets[index] = pair\n}\n\n/* Операция удаления */\nfunc (a *arrayHashMap) remove(key int) {\n    index := a.hashFunc(key)\n    // Присвоить nil, что означает удаление\n    a.buckets[index] = nil\n}\n\n/* Получить все ключи */\nfunc (a *arrayHashMap) pairSet() []*pair {\n    var pairs []*pair\n    for _, pair := range a.buckets {\n        if pair != nil {\n            pairs = append(pairs, pair)\n        }\n    }\n    return pairs\n}\n\n/* Получить все ключи */\nfunc (a *arrayHashMap) keySet() []int {\n    var keys []int\n    for _, pair := range a.buckets {\n        if pair != nil {\n            keys = append(keys, pair.key)\n        }\n    }\n    return keys\n}\n\n/* Получить все значения */\nfunc (a *arrayHashMap) valueSet() []string {\n    var values []string\n    for _, pair := range a.buckets {\n        if pair != nil {\n            values = append(values, pair.val)\n        }\n    }\n    return values\n}\n\n/* Вывести хеш-таблицу */\nfunc (a *arrayHashMap) print() {\n    for _, pair := range a.buckets {\n        if pair != nil {\n            fmt.Println(pair.key, \"->\", pair.val)\n        }\n    }\n}\n
        array_hash_map.swift
        /* Пара ключ-значение */\nclass Pair: Equatable {\n    public var key: Int\n    public var val: String\n\n    public init(key: Int, val: String) {\n        self.key = key\n        self.val = val\n    }\n\n    public static func == (lhs: Pair, rhs: Pair) -> Bool {\n        lhs.key == rhs.key && lhs.val == rhs.val\n    }\n}\n\n/* Хеш-таблица на основе массива */\nclass ArrayHashMap {\n    private var buckets: [Pair?]\n\n    init() {\n        // Инициализировать массив, содержащий 100 корзин\n        buckets = Array(repeating: nil, count: 100)\n    }\n\n    /* Хеш-функция */\n    private func hashFunc(key: Int) -> Int {\n        let index = key % 100\n        return index\n    }\n\n    /* Операция поиска */\n    func get(key: Int) -> String? {\n        let index = hashFunc(key: key)\n        let pair = buckets[index]\n        return pair?.val\n    }\n\n    /* Операция добавления */\n    func put(key: Int, val: String) {\n        let pair = Pair(key: key, val: val)\n        let index = hashFunc(key: key)\n        buckets[index] = pair\n    }\n\n    /* Операция удаления */\n    func remove(key: Int) {\n        let index = hashFunc(key: key)\n        // Присвоить nil, что означает удаление\n        buckets[index] = nil\n    }\n\n    /* Получить все пары ключ-значение */\n    func pairSet() -> [Pair] {\n        buckets.compactMap { $0 }\n    }\n\n    /* Получить все ключи */\n    func keySet() -> [Int] {\n        buckets.compactMap { $0?.key }\n    }\n\n    /* Получить все значения */\n    func valueSet() -> [String] {\n        buckets.compactMap { $0?.val }\n    }\n\n    /* Вывести хеш-таблицу */\n    func print() {\n        for pair in pairSet() {\n            Swift.print(\"\\(pair.key) -> \\(pair.val)\")\n        }\n    }\n}\n
        array_hash_map.js
        /* Пара ключ-значение Number -> String */\nclass Pair {\n    constructor(key, val) {\n        this.key = key;\n        this.val = val;\n    }\n}\n\n/* Хеш-таблица на основе массива */\nclass ArrayHashMap {\n    #buckets;\n    constructor() {\n        // Инициализировать массив, содержащий 100 корзин\n        this.#buckets = new Array(100).fill(null);\n    }\n\n    /* Хеш-функция */\n    #hashFunc(key) {\n        return key % 100;\n    }\n\n    /* Операция поиска */\n    get(key) {\n        let index = this.#hashFunc(key);\n        let pair = this.#buckets[index];\n        if (pair === null) return null;\n        return pair.val;\n    }\n\n    /* Операция добавления */\n    set(key, val) {\n        let index = this.#hashFunc(key);\n        this.#buckets[index] = new Pair(key, val);\n    }\n\n    /* Операция удаления */\n    delete(key) {\n        let index = this.#hashFunc(key);\n        // Присвоить null, что означает удаление\n        this.#buckets[index] = null;\n    }\n\n    /* Получить все пары ключ-значение */\n    entries() {\n        let arr = [];\n        for (let i = 0; i < this.#buckets.length; i++) {\n            if (this.#buckets[i]) {\n                arr.push(this.#buckets[i]);\n            }\n        }\n        return arr;\n    }\n\n    /* Получить все ключи */\n    keys() {\n        let arr = [];\n        for (let i = 0; i < this.#buckets.length; i++) {\n            if (this.#buckets[i]) {\n                arr.push(this.#buckets[i].key);\n            }\n        }\n        return arr;\n    }\n\n    /* Получить все значения */\n    values() {\n        let arr = [];\n        for (let i = 0; i < this.#buckets.length; i++) {\n            if (this.#buckets[i]) {\n                arr.push(this.#buckets[i].val);\n            }\n        }\n        return arr;\n    }\n\n    /* Вывести хеш-таблицу */\n    print() {\n        let pairSet = this.entries();\n        for (const pair of pairSet) {\n            console.info(`${pair.key} -> ${pair.val}`);\n        }\n    }\n}\n
        array_hash_map.ts
        /* Пара ключ-значение Number -> String */\nclass Pair {\n    public key: number;\n    public val: string;\n\n    constructor(key: number, val: string) {\n        this.key = key;\n        this.val = val;\n    }\n}\n\n/* Хеш-таблица на основе массива */\nclass ArrayHashMap {\n    private readonly buckets: (Pair | null)[];\n\n    constructor() {\n        // Инициализировать массив, содержащий 100 корзин\n        this.buckets = new Array(100).fill(null);\n    }\n\n    /* Хеш-функция */\n    private hashFunc(key: number): number {\n        return key % 100;\n    }\n\n    /* Операция поиска */\n    public get(key: number): string | null {\n        let index = this.hashFunc(key);\n        let pair = this.buckets[index];\n        if (pair === null) return null;\n        return pair.val;\n    }\n\n    /* Операция добавления */\n    public set(key: number, val: string) {\n        let index = this.hashFunc(key);\n        this.buckets[index] = new Pair(key, val);\n    }\n\n    /* Операция удаления */\n    public delete(key: number) {\n        let index = this.hashFunc(key);\n        // Присвоить null, что означает удаление\n        this.buckets[index] = null;\n    }\n\n    /* Получить все пары ключ-значение */\n    public entries(): (Pair | null)[] {\n        let arr: (Pair | null)[] = [];\n        for (let i = 0; i < this.buckets.length; i++) {\n            if (this.buckets[i]) {\n                arr.push(this.buckets[i]);\n            }\n        }\n        return arr;\n    }\n\n    /* Получить все ключи */\n    public keys(): (number | undefined)[] {\n        let arr: (number | undefined)[] = [];\n        for (let i = 0; i < this.buckets.length; i++) {\n            if (this.buckets[i]) {\n                arr.push(this.buckets[i].key);\n            }\n        }\n        return arr;\n    }\n\n    /* Получить все значения */\n    public values(): (string | undefined)[] {\n        let arr: (string | undefined)[] = [];\n        for (let i = 0; i < this.buckets.length; i++) {\n            if (this.buckets[i]) {\n                arr.push(this.buckets[i].val);\n            }\n        }\n        return arr;\n    }\n\n    /* Вывести хеш-таблицу */\n    public print() {\n        let pairSet = this.entries();\n        for (const pair of pairSet) {\n            console.info(`${pair.key} -> ${pair.val}`);\n        }\n    }\n}\n
        array_hash_map.dart
        /* Пара ключ-значение */\nclass Pair {\n  int key;\n  String val;\n  Pair(this.key, this.val);\n}\n\n/* Хеш-таблица на основе массива */\nclass ArrayHashMap {\n  late List<Pair?> _buckets;\n\n  ArrayHashMap() {\n    // Инициализировать массив, содержащий 100 корзин\n    _buckets = List.filled(100, null);\n  }\n\n  /* Хеш-функция */\n  int _hashFunc(int key) {\n    final int index = key % 100;\n    return index;\n  }\n\n  /* Операция поиска */\n  String? get(int key) {\n    final int index = _hashFunc(key);\n    final Pair? pair = _buckets[index];\n    if (pair == null) {\n      return null;\n    }\n    return pair.val;\n  }\n\n  /* Операция добавления */\n  void put(int key, String val) {\n    final Pair pair = Pair(key, val);\n    final int index = _hashFunc(key);\n    _buckets[index] = pair;\n  }\n\n  /* Операция удаления */\n  void remove(int key) {\n    final int index = _hashFunc(key);\n    _buckets[index] = null;\n  }\n\n  /* Получить все пары ключ-значение */\n  List<Pair> pairSet() {\n    List<Pair> pairSet = [];\n    for (final Pair? pair in _buckets) {\n      if (pair != null) {\n        pairSet.add(pair);\n      }\n    }\n    return pairSet;\n  }\n\n  /* Получить все ключи */\n  List<int> keySet() {\n    List<int> keySet = [];\n    for (final Pair? pair in _buckets) {\n      if (pair != null) {\n        keySet.add(pair.key);\n      }\n    }\n    return keySet;\n  }\n\n  /* Получить все значения */\n  List<String> values() {\n    List<String> valueSet = [];\n    for (final Pair? pair in _buckets) {\n      if (pair != null) {\n        valueSet.add(pair.val);\n      }\n    }\n    return valueSet;\n  }\n\n  /* Вывести хеш-таблицу */\n  void printHashMap() {\n    for (final Pair kv in pairSet()) {\n      print(\"${kv.key} -> ${kv.val}\");\n    }\n  }\n}\n
        array_hash_map.rs
        /* Пара ключ-значение */\n#[derive(Debug, Clone, PartialEq)]\npub struct Pair {\n    pub key: i32,\n    pub val: String,\n}\n\n/* Хеш-таблица на основе массива */\npub struct ArrayHashMap {\n    buckets: Vec<Option<Pair>>,\n}\n\nimpl ArrayHashMap {\n    pub fn new() -> ArrayHashMap {\n        // Инициализировать массив, содержащий 100 корзин\n        Self {\n            buckets: vec![None; 100],\n        }\n    }\n\n    /* Хеш-функция */\n    fn hash_func(&self, key: i32) -> usize {\n        key as usize % 100\n    }\n\n    /* Операция поиска */\n    pub fn get(&self, key: i32) -> Option<&String> {\n        let index = self.hash_func(key);\n        self.buckets[index].as_ref().map(|pair| &pair.val)\n    }\n\n    /* Операция добавления */\n    pub fn put(&mut self, key: i32, val: &str) {\n        let index = self.hash_func(key);\n        self.buckets[index] = Some(Pair {\n            key,\n            val: val.to_string(),\n        });\n    }\n\n    /* Операция удаления */\n    pub fn remove(&mut self, key: i32) {\n        let index = self.hash_func(key);\n        // Присвоить None, что означает удаление\n        self.buckets[index] = None;\n    }\n\n    /* Получить все пары ключ-значение */\n    pub fn entry_set(&self) -> Vec<&Pair> {\n        self.buckets\n            .iter()\n            .filter_map(|pair| pair.as_ref())\n            .collect()\n    }\n\n    /* Получить все ключи */\n    pub fn key_set(&self) -> Vec<&i32> {\n        self.buckets\n            .iter()\n            .filter_map(|pair| pair.as_ref().map(|pair| &pair.key))\n            .collect()\n    }\n\n    /* Получить все значения */\n    pub fn value_set(&self) -> Vec<&String> {\n        self.buckets\n            .iter()\n            .filter_map(|pair| pair.as_ref().map(|pair| &pair.val))\n            .collect()\n    }\n\n    /* Вывести хеш-таблицу */\n    pub fn print(&self) {\n        for pair in self.entry_set() {\n            println!(\"{} -> {}\", pair.key, pair.val);\n        }\n    }\n}\n
        array_hash_map.c
        /* Пара ключ-значение int->string */\ntypedef struct {\n    int key;\n    char *val;\n} Pair;\n\n/* Хеш-таблица на основе массива */\ntypedef struct {\n    Pair *buckets[MAX_SIZE];\n} ArrayHashMap;\n\n/* Конструктор */\nArrayHashMap *newArrayHashMap() {\n    ArrayHashMap *hmap = malloc(sizeof(ArrayHashMap));\n    for (int i=0; i < MAX_SIZE; i++) {\n        hmap->buckets[i] = NULL;\n    }\n    return hmap;\n}\n\n/* Деструктор */\nvoid delArrayHashMap(ArrayHashMap *hmap) {\n    for (int i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            free(hmap->buckets[i]->val);\n            free(hmap->buckets[i]);\n        }\n    }\n    free(hmap);\n}\n\n/* Операция добавления */\nvoid put(ArrayHashMap *hmap, const int key, const char *val) {\n    Pair *Pair = malloc(sizeof(Pair));\n    Pair->key = key;\n    Pair->val = malloc(strlen(val) + 1);\n    strcpy(Pair->val, val);\n\n    int index = hashFunc(key);\n    hmap->buckets[index] = Pair;\n}\n\n/* Операция удаления */\nvoid removeItem(ArrayHashMap *hmap, const int key) {\n    int index = hashFunc(key);\n    free(hmap->buckets[index]->val);\n    free(hmap->buckets[index]);\n    hmap->buckets[index] = NULL;\n}\n\n/* Получить все пары ключ-значение */\nvoid pairSet(ArrayHashMap *hmap, MapSet *set) {\n    Pair *entries;\n    int i = 0, index = 0;\n    int total = 0;\n    /* Подсчитать число действительных пар ключ-значение */\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            total++;\n        }\n    }\n    entries = malloc(sizeof(Pair) * total);\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            entries[index].key = hmap->buckets[i]->key;\n            entries[index].val = malloc(strlen(hmap->buckets[i]->val) + 1);\n            strcpy(entries[index].val, hmap->buckets[i]->val);\n            index++;\n        }\n    }\n    set->set = entries;\n    set->len = total;\n}\n\n/* Получить все ключи */\nvoid keySet(ArrayHashMap *hmap, MapSet *set) {\n    int *keys;\n    int i = 0, index = 0;\n    int total = 0;\n    /* Подсчитать число действительных пар ключ-значение */\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            total++;\n        }\n    }\n    keys = malloc(total * sizeof(int));\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            keys[index] = hmap->buckets[i]->key;\n            index++;\n        }\n    }\n    set->set = keys;\n    set->len = total;\n}\n\n/* Получить все значения */\nvoid valueSet(ArrayHashMap *hmap, MapSet *set) {\n    char **vals;\n    int i = 0, index = 0;\n    int total = 0;\n    /* Подсчитать число действительных пар ключ-значение */\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            total++;\n        }\n    }\n    vals = malloc(total * sizeof(char *));\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            vals[index] = hmap->buckets[i]->val;\n            index++;\n        }\n    }\n    set->set = vals;\n    set->len = total;\n}\n\n/* Вывести хеш-таблицу */\nvoid print(ArrayHashMap *hmap) {\n    int i;\n    MapSet set;\n    pairSet(hmap, &set);\n    Pair *entries = (Pair *)set.set;\n    for (i = 0; i < set.len; i++) {\n        printf(\"%d -> %s\\n\", entries[i].key, entries[i].val);\n    }\n    free(set.set);\n}\n
        array_hash_map.kt
        /* Пара ключ-значение */\nclass Pair(\n    var key: Int,\n    var _val: String\n)\n\n/* Хеш-таблица на основе массива */\nclass ArrayHashMap {\n    // Инициализировать массив, содержащий 100 корзин\n    private val buckets = arrayOfNulls<Pair>(100)\n\n    /* Хеш-функция */\n    fun hashFunc(key: Int): Int {\n        val index = key % 100\n        return index\n    }\n\n    /* Операция поиска */\n    fun get(key: Int): String? {\n        val index = hashFunc(key)\n        val pair = buckets[index] ?: return null\n        return pair._val\n    }\n\n    /* Операция добавления */\n    fun put(key: Int, _val: String) {\n        val pair = Pair(key, _val)\n        val index = hashFunc(key)\n        buckets[index] = pair\n    }\n\n    /* Операция удаления */\n    fun remove(key: Int) {\n        val index = hashFunc(key)\n        // Присвоить null, что означает удаление\n        buckets[index] = null\n    }\n\n    /* Получить все пары ключ-значение */\n    fun pairSet(): MutableList<Pair> {\n        val pairSet = mutableListOf<Pair>()\n        for (pair in buckets) {\n            if (pair != null)\n                pairSet.add(pair)\n        }\n        return pairSet\n    }\n\n    /* Получить все ключи */\n    fun keySet(): MutableList<Int> {\n        val keySet = mutableListOf<Int>()\n        for (pair in buckets) {\n            if (pair != null)\n                keySet.add(pair.key)\n        }\n        return keySet\n    }\n\n    /* Получить все значения */\n    fun valueSet(): MutableList<String> {\n        val valueSet = mutableListOf<String>()\n        for (pair in buckets) {\n            if (pair != null)\n                valueSet.add(pair._val)\n        }\n        return valueSet\n    }\n\n    /* Вывести хеш-таблицу */\n    fun print() {\n        for (kv in pairSet()) {\n            val key = kv.key\n            val _val = kv._val\n            println(\"$key -> $_val\")\n        }\n    }\n}\n
        array_hash_map.rb
        ### Пара ключ-значение ###\nclass Pair\n  attr_accessor :key, :val\n\n  def initialize(key, val)\n    @key = key\n    @val = val\n  end\nend\n\n### Хеш-таблица на основе массива ###\nclass ArrayHashMap\n  ### Конструктор ###\n  def initialize\n    # Инициализировать массив, содержащий 100 корзин\n    @buckets = Array.new(100)\n  end\n\n  ### Хеш-функция ###\n  def hash_func(key)\n    index = key % 100\n  end\n\n  ### Операция поиска ###\n  def get(key)\n    index = hash_func(key)\n    pair = @buckets[index]\n\n    return if pair.nil?\n    pair.val\n  end\n\n  ### Операция добавления ###\n  def put(key, val)\n    pair = Pair.new(key, val)\n    index = hash_func(key)\n    @buckets[index] = pair\n  end\n\n  ### Операция удаления ###\n  def remove(key)\n    index = hash_func(key)\n    # Присвоить nil, что означает удаление\n    @buckets[index] = nil\n  end\n\n  ### Получить все пары ключ-значение ###\n  def entry_set\n    result = []\n    @buckets.each { |pair| result << pair unless pair.nil? }\n    result\n  end\n\n  ### Получить все ключи ###\n  def key_set\n    result = []\n    @buckets.each { |pair| result << pair.key unless pair.nil? }\n    result\n  end\n\n  ### Получить все значения ###\n  def value_set\n    result = []\n    @buckets.each { |pair| result << pair.val unless pair.nil? }\n    result\n  end\n\n  ### Вывести хеш-таблицу ###\n  def print\n    @buckets.each { |pair| puts \"#{pair.key} -> #{pair.val}\" unless pair.nil? }\n  end\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 6. Хеш-таблицы","6.1   Хеш-таблица"],"tags":[]},{"location":"chapter_hashing/hash_map/#613-","level":2,"title":"6.1.3   Хеш-коллизии и расширение","text":"

        По сути, хеш-функция отображает входное пространство, состоящее из всех key , в выходное пространство, состоящее из всех индексов массива, а входное пространство обычно значительно больше выходного. Поэтому теоретически неизбежно существование ситуации «несколько входов соответствуют одному выходу».

        Для хеш-функции из приведенного выше примера, если последние две цифры key совпадают, то совпадает и результат хеш-функции. Например, если искать студентов с номерами 12836 и 20336, то получим:

        12836 % 100 = 36\n20336 % 100 = 36\n

        Как показано на рисунке 6-3, два номера указывают на одно и то же имя, что, очевидно, неверно. Такую ситуацию, когда нескольким входам соответствует один и тот же выход, называют хеш-коллизией (hash collision).

        Рисунок 6-3   Пример хеш-коллизии

        Легко понять, что чем больше емкость хеш-таблицы \\(n\\) , тем ниже вероятность того, что несколько key попадут в один и тот же бакет, а значит, тем меньше коллизий. Поэтому мы можем уменьшать число хеш-коллизий путем расширения хеш-таблицы.

        Как показано на рисунке 6-4, до расширения пары ключ-значение (136, A) и (236, D) конфликтовали, а после расширения коллизия исчезла.

        Рисунок 6-4   Расширение хеш-таблицы

        Подобно расширению массива, расширение хеш-таблицы требует перенести все пары ключ-значение из старой таблицы в новую, а это очень затратно по времени. Кроме того, поскольку емкость хеш-таблицы capacity изменилась, нам приходится с помощью хеш-функции заново вычислять позиции хранения всех пар ключ-значение, что дополнительно увеличивает вычислительные расходы процесса расширения. Поэтому языки программирования обычно заранее резервируют достаточно большую емкость хеш-таблицы, чтобы избежать частых расширений.

        Коэффициент загрузки (load factor) - важное понятие хеш-таблицы. Он определяется как отношение числа элементов в хеш-таблице к числу бакетов и используется для оценки степени серьезности хеш-коллизий, а также часто служит условием срабатывания расширения хеш-таблицы. Например, в Java, когда коэффициент загрузки превышает \\(0.75\\) , система расширяет хеш-таблицу до \\(2\\) раз от исходной емкости.

        ","path":["Глава 6. Хеш-таблицы","6.1   Хеш-таблица"],"tags":[]},{"location":"chapter_hashing/summary/","level":1,"title":"6.4   Резюме","text":"","path":["Глава 6. Хеш-таблицы","6.4   Резюме"],"tags":[]},{"location":"chapter_hashing/summary/#1","level":3,"title":"1.   Ключевые выводы","text":"
        • Передав key , мы можем получить value из хеш-таблицы за \\(O(1)\\) времени, поэтому она очень эффективна.
        • К типичным операциям хеш-таблицы относятся поиск, добавление пары ключ-значение, удаление пары ключ-значение и обход хеш-таблицы.
        • Хеш-функция отображает key в индекс массива, после чего можно обратиться к соответствующему бакету и получить value .
        • Два разных key после хеш-функции могут дать один и тот же индекс массива, что приводит к ошибочному результату поиска. Это явление называется хеш-коллизией.
        • Чем больше емкость хеш-таблицы, тем ниже вероятность хеш-коллизий. Поэтому хеш-коллизии можно смягчать путем расширения хеш-таблицы. Как и у массива, операция расширения у хеш-таблицы очень затратна.
        • Коэффициент загрузки определяется как отношение числа элементов в хеш-таблице к числу бакетов, отражает степень серьезности хеш-коллизий и часто используется как условие запуска расширения хеш-таблицы.
        • Метод цепочек превращает одиночный элемент в связный список и хранит все конфликтующие элементы в одном списке. Однако слишком длинный список снижает эффективность поиска, поэтому его можно дополнительно преобразовать в красно-черное дерево.
        • Открытая адресация обрабатывает хеш-коллизии за счет многократного пробирования. Линейное пробирование использует фиксированный шаг, его недостатки - невозможность прямого удаления элементов и склонность к кластеризации. Повторное хеширование использует несколько хеш-функций и по сравнению с линейным пробированием меньше подвержено кластеризации, но требует больше вычислений.
        • Разные языки программирования выбирают разные стратегии реализации хеш-таблиц. Например, HashMap в Java использует метод цепочек, а Dict в Python - открытую адресацию.
        • Для хеш-таблицы желательно, чтобы хеш-алгоритм был детерминированным, быстрым и обеспечивал равномерное распределение. В криптографии от него дополнительно требуют устойчивости к коллизиям и эффекта лавины.
        • В качестве модуля хеш-алгоритмы обычно используют большое простое число, чтобы максимально обеспечить равномерность распределения хеш-значений и снизить число хеш-коллизий.
        • К распространенным хеш-алгоритмам относятся MD5, SHA-1, SHA-2 и SHA-3. MD5 часто применяли для проверки целостности файлов, а SHA-2 широко используется в протоколах и приложениях, связанных с безопасностью.
        • Языки программирования обычно предоставляют для типов данных встроенные хеш-алгоритмы, чтобы вычислять индексы бакетов в хеш-таблице. Как правило, хешируемыми могут быть только неизменяемые объекты.
        ","path":["Глава 6. Хеш-таблицы","6.4   Резюме"],"tags":[]},{"location":"chapter_hashing/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

        Q: В каких случаях временная сложность хеш-таблицы становится \\(O(n)\\) ?

        Когда хеш-коллизии становятся достаточно серьезными, временная сложность хеш-таблицы деградирует до \\(O(n)\\) . Если хеш-функция спроектирована хорошо, емкость выбрана разумно, а конфликты распределены достаточно равномерно, то временная сложность обычно считается \\(O(1)\\) . При использовании встроенной хеш-таблицы языка программирования мы, как правило, и принимаем ее за \\(O(1)\\) .

        Q: Почему бы не использовать хеш-функцию \\(f(x) = x\\) ? Тогда ведь коллизий не будет.

        При хеш-функции \\(f(x) = x\\) каждому элементу соответствует уникальный индекс бакета, и такая структура становится эквивалентна массиву. Однако входное пространство обычно намного больше выходного пространства (длины массива), поэтому последним шагом хеш-функции обычно выступает взятие по модулю длины массива. Иначе говоря, цель хеш-таблицы состоит в том, чтобы отобразить большее пространство состояний в меньшее пространство и при этом обеспечить \\(O(1)\\) поиска.

        Q: В основе хеш-таблицы лежат массив, связный список и двоичное дерево. Почему же она может быть быстрее них?

        Во-первых, у хеш-таблицы повышается временная эффективность, но снижается пространственная эффективность. Значительная часть ее памяти остается неиспользованной.

        Во-вторых, она быстрее только в определенных сценариях. Если одну и ту же задачу можно реализовать на массиве или связном списке с той же асимптотикой, то часто такая реализация окажется быстрее, чем хеш-таблица. Причина в том, что вычисление хеш-функции само по себе стоит времени, то есть константа в сложности получается выше.

        Наконец, временная сложность хеш-таблицы тоже может деградировать. Например, при методе цепочек мы все равно выполняем поиск в связном списке или красно-черном дереве, поэтому риск деградации до \\(O(n)\\) сохраняется.

        Q: Есть ли у повторного хеширования недостаток «нельзя напрямую удалять элементы»? Можно ли повторно использовать место, помеченное как удаленное?

        Повторное хеширование - это разновидность открытой адресации, а у всех методов открытой адресации есть недостаток: элементы нельзя удалять напрямую, поэтому приходится использовать метку удаления. Пространство, помеченное как удаленное, можно использовать повторно. Когда новый элемент вставляется в хеш-таблицу и в процессе пробирования попадает на такую отмеченную позицию, эта позиция может быть занята новым элементом. Такой подход сохраняет последовательность пробирования и одновременно поддерживает приемлемую эффективность использования памяти.

        Q: Почему при линейном пробировании во время поиска элемента вообще возникает хеш-коллизия?

        Во время поиска мы через хеш-функцию находим соответствующий бакет и соответствующую пару ключ-значение, но видим, что key не совпадает, а это и означает наличие хеш-коллизии. Поэтому метод линейного пробирования в соответствии с заранее заданным шагом последовательно движется дальше, пока не найдет правильную пару ключ-значение или не убедится, что поиск завершился неудачей.

        Q: Почему расширение хеш-таблицы помогает смягчать хеш-коллизии?

        Последний шаг хеш-функции обычно состоит во взятии по модулю длины массива \\(n\\) , чтобы результат попадал в диапазон индексов массива. После расширения длина массива \\(n\\) меняется, а значит, может измениться и индекс, соответствующий данному key . Несколько key , которые раньше попадали в один бакет, после расширения могут распределиться по нескольким бакетам, и тем самым хеш-коллизии будут ослаблены.

        Q: Если нам нужен быстрый доступ, почему бы просто не использовать массив?

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

        ","path":["Глава 6. Хеш-таблицы","6.4   Резюме"],"tags":[]},{"location":"chapter_heap/","level":1,"title":"Глава 8.   Куча","text":"

        Abstract

        Куча - это полное двоичное дерево, удовлетворяющее определенным условиям.

        В максимальной и минимальной куче элемент на вершине всегда обладает самым выраженным приоритетом.

        ","path":["Глава 8. Куча","Глава 8.   Куча"],"tags":[]},{"location":"chapter_heap/#_1","level":2,"title":"Содержание главы","text":"
        • 8.1   Куча
        • 8.2   Построение кучи
        • 8.3   Задача Top-k
        • 8.4   Резюме
        ","path":["Глава 8. Куча","Глава 8.   Куча"],"tags":[]},{"location":"chapter_heap/build_heap/","level":1,"title":"8.2   Построение кучи","text":"

        В некоторых случаях требуется построить кучу, используя сразу все элементы списка. Этот процесс называется построением кучи.

        ","path":["Глава 8. Куча","8.2   Построение кучи"],"tags":[]},{"location":"chapter_heap/build_heap/#821","level":2,"title":"8.2.1   Реализация через операцию добавления в кучу","text":"

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

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

        Пусть число элементов равно \\(n\\). Так как каждая операция добавления требует \\(O(\\log{n})\\) времени, временная сложность такого построения кучи составляет \\(O(n \\log n)\\) .

        ","path":["Глава 8. Куча","8.2   Построение кучи"],"tags":[]},{"location":"chapter_heap/build_heap/#822","level":2,"title":"8.2.2   Реализация через обход и упорядочивание","text":"

        На самом деле можно реализовать и более эффективный способ построения кучи, который состоит из двух шагов.

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

        После того как некоторый узел был упорядочен, поддерево с этим узлом в качестве корня становится корректной подкучей. А поскольку обход выполняется в обратном порядке, куча строится снизу вверх.

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

        Стоит отметить, что листовые узлы не имеют дочерних узлов, поэтому они естественным образом являются корректными подкучами и не требуют упорядочивания. Как показано в коде ниже, последний нелистовой узел является родителем последнего узла, и именно с него мы начинаем обратный обход и упорядочивание:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
        def __init__(self, nums: list[int]):\n    \"\"\"Конструктор, строящий кучу по входному списку\"\"\"\n    # Добавить элементы списка в кучу без изменений\n    self.max_heap = nums\n    # Выполнить heapify для всех узлов, кроме листовых\n    for i in range(self.parent(self.size() - 1), -1, -1):\n        self.sift_down(i)\n
        my_heap.cpp
        /* Конструктор, строящий кучу по входному списку */\nMaxHeap(vector<int> nums) {\n    // Добавить элементы списка в кучу без изменений\n    maxHeap = nums;\n    // Выполнить heapify для всех узлов, кроме листовых\n    for (int i = parent(size() - 1); i >= 0; i--) {\n        siftDown(i);\n    }\n}\n
        my_heap.java
        /* Конструктор, строящий кучу по входному списку */\nMaxHeap(List<Integer> nums) {\n    // Добавить элементы списка в кучу без изменений\n    maxHeap = new ArrayList<>(nums);\n    // Выполнить heapify для всех узлов, кроме листовых\n    for (int i = parent(size() - 1); i >= 0; i--) {\n        siftDown(i);\n    }\n}\n
        my_heap.cs
        /* Конструктор: построить кучу по входному списку */\nMaxHeap(IEnumerable<int> nums) {\n    // Добавить элементы списка в кучу без изменений\n    maxHeap = new List<int>(nums);\n    // Выполнить heapify для всех узлов, кроме листовых\n    var size = Parent(this.Size() - 1);\n    for (int i = size; i >= 0; i--) {\n        SiftDown(i);\n    }\n}\n
        my_heap.go
        /* Конструктор, строящий кучу по срезу */\nfunc newMaxHeap(nums []any) *maxHeap {\n    // Добавить элементы списка в кучу без изменений\n    h := &maxHeap{data: nums}\n    for i := h.parent(len(h.data) - 1); i >= 0; i-- {\n        // Выполнить heapify для всех узлов, кроме листовых\n        h.siftDown(i)\n    }\n    return h\n}\n
        my_heap.swift
        /* Конструктор, строящий кучу по входному списку */\ninit(nums: [Int]) {\n    // Добавить элементы списка в кучу без изменений\n    maxHeap = nums\n    // Выполнить heapify для всех узлов, кроме листовых\n    for i in (0 ... parent(i: size() - 1)).reversed() {\n        siftDown(i: i)\n    }\n}\n
        my_heap.js
        /* Конструктор, создающий пустую кучу или строящий кучу по входному списку */\nconstructor(nums) {\n    // Добавить элементы списка в кучу без изменений\n    this.#maxHeap = nums === undefined ? [] : [...nums];\n    // Выполнить heapify для всех узлов, кроме листовых\n    for (let i = this.#parent(this.size() - 1); i >= 0; i--) {\n        this.#siftDown(i);\n    }\n}\n
        my_heap.ts
        /* Конструктор, создающий пустую кучу или строящий кучу по входному списку */\nconstructor(nums?: number[]) {\n    // Добавить элементы списка в кучу без изменений\n    this.maxHeap = nums === undefined ? [] : [...nums];\n    // Выполнить heapify для всех узлов, кроме листовых\n    for (let i = this.parent(this.size() - 1); i >= 0; i--) {\n        this.siftDown(i);\n    }\n}\n
        my_heap.dart
        /* Конструктор, строящий кучу по входному списку */\nMaxHeap(List<int> nums) {\n  // Добавить элементы списка в кучу без изменений\n  _maxHeap = nums;\n  // Выполнить heapify для всех узлов, кроме листовых\n  for (int i = _parent(size() - 1); i >= 0; i--) {\n    siftDown(i);\n  }\n}\n
        my_heap.rs
        /* Конструктор, строящий кучу по входному списку */\nfn new(nums: Vec<i32>) -> Self {\n    // Добавить элементы списка в кучу без изменений\n    let mut heap = MaxHeap { max_heap: nums };\n    // Выполнить heapify для всех узлов, кроме листовых\n    for i in (0..=Self::parent(heap.size() - 1)).rev() {\n        heap.sift_down(i);\n    }\n    heap\n}\n
        my_heap.c
        /* Конструктор, строящий кучу по срезу */\nMaxHeap *newMaxHeap(int nums[], int size) {\n    // Поместить все элементы в кучу\n    MaxHeap *maxHeap = (MaxHeap *)malloc(sizeof(MaxHeap));\n    maxHeap->size = size;\n    memcpy(maxHeap->data, nums, size * sizeof(int));\n    for (int i = parent(maxHeap, size - 1); i >= 0; i--) {\n        // Выполнить heapify для всех узлов, кроме листовых\n        siftDown(maxHeap, i);\n    }\n    return maxHeap;\n}\n
        my_heap.kt
        /* Максимальная куча */\nclass MaxHeap(nums: MutableList<Int>?) {\n    // Использовать список вместо массива, чтобы не учитывать проблему расширения\n    private val maxHeap = mutableListOf<Int>()\n\n    /* Конструктор, строящий кучу по входному списку */\n    init {\n        // Добавить элементы списка в кучу без изменений\n        maxHeap.addAll(nums!!)\n        // Выполнить heapify для всех узлов, кроме листовых\n        for (i in parent(size() - 1) downTo 0) {\n            siftDown(i)\n        }\n    }\n\n    /* Получить индекс левого дочернего узла */\n    private fun left(i: Int): Int {\n        return 2 * i + 1\n    }\n\n    /* Получить индекс правого дочернего узла */\n    private fun right(i: Int): Int {\n        return 2 * i + 2\n    }\n\n    /* Получить индекс родительского узла */\n    private fun parent(i: Int): Int {\n        return (i - 1) / 2 // Округление вниз при делении\n    }\n\n    /* Поменять элементы местами */\n    private fun swap(i: Int, j: Int) {\n        val temp = maxHeap[i]\n        maxHeap[i] = maxHeap[j]\n        maxHeap[j] = temp\n    }\n\n    /* Получение размера кучи */\n    fun size(): Int {\n        return maxHeap.size\n    }\n\n    /* Проверка, пуста ли куча */\n    fun isEmpty(): Boolean {\n        /* Проверка, пуста ли куча */\n        return size() == 0\n    }\n\n    /* Доступ к элементу на вершине кучи */\n    fun peek(): Int {\n        return maxHeap[0]\n    }\n\n    /* Добавление элемента в кучу */\n    fun push(_val: Int) {\n        // Добавление узла\n        maxHeap.add(_val)\n        // Просеивание снизу вверх\n        siftUp(size() - 1)\n    }\n\n    /* Начиная с узла i, выполнить просеивание снизу вверх */\n    private fun siftUp(it: Int) {\n        // Параметры функций в Kotlin неизменяемы, поэтому создается временная переменная\n        var i = it\n        while (true) {\n            // Получение родительского узла для узла i\n            val p = parent(i)\n            // Завершить heapify, когда «корневой узел уже пройден» или «узел не требует исправления»\n            if (p < 0 || maxHeap[i] <= maxHeap[p]) break\n            // Поменять два узла местами\n            swap(i, p)\n            // Циклическое просеивание вверх\n            i = p\n        }\n    }\n\n    /* Извлечение элемента из кучи */\n    fun pop(): Int {\n        // Обработка пустого случая\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n        swap(0, size() - 1)\n        // Удаление узла\n        val _val = maxHeap.removeAt(size() - 1)\n        // Просеивание сверху вниз\n        siftDown(0)\n        // Вернуть элемент с вершины кучи\n        return _val\n    }\n\n    /* Начиная с узла i, выполнить просеивание сверху вниз */\n    private fun siftDown(it: Int) {\n        // Параметры функций в Kotlin неизменяемы, поэтому создается временная переменная\n        var i = it\n        while (true) {\n            // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n            val l = left(i)\n            val r = right(i)\n            var ma = i\n            if (l < size() && maxHeap[l] > maxHeap[ma]) ma = l\n            if (r < size() && maxHeap[r] > maxHeap[ma]) ma = r\n            // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n            if (ma == i) break\n            // Поменять два узла местами\n            swap(i, ma)\n            // Циклическое просеивание вниз\n            i = ma\n        }\n    }\n\n    /* Вывести кучу (двоичное дерево) */\n    fun print() {\n        val queue = PriorityQueue { a: Int, b: Int -> b - a }\n        queue.addAll(maxHeap)\n        printHeap(queue)\n    }\n}\n
        my_heap.rb
        ### Конструктор, строящий кучу по входному списку ###\ndef initialize(nums)\n  # Добавить элементы списка в кучу без изменений\n  @max_heap = nums\n  # Выполнить heapify для всех узлов, кроме листовых\n  parent(size - 1).downto(0) do |i|\n    sift_down(i)\n  end\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 8. Куча","8.2   Построение кучи"],"tags":[]},{"location":"chapter_heap/build_heap/#823","level":2,"title":"8.2.3   Анализ сложности","text":"

        Теперь попробуем оценить временную сложность второго способа построения кучи.

        • Пусть число узлов полного двоичного дерева равно \\(n\\) , тогда число листовых узлов равно \\((n + 1) / 2\\) , где \\(/\\) означает целочисленное деление вниз. Следовательно, число узлов, которые нужно упорядочивать, равно \\((n - 1) / 2\\) .
        • В процессе упорядочивания сверху вниз каждый узел в худшем случае может просеяться до листа, поэтому максимальное число итераций равно высоте двоичного дерева \\(\\log n\\) .

        Перемножив эти два значения, можно получить временную сложность построения кучи \\(O(n \\log n)\\) . Но эта оценка неточна, потому что мы не учли свойство двоичного дерева: на нижних уровнях узлов гораздо больше, чем на верхних.

        Далее выполним более точный расчет. Чтобы упростить вычисления, предположим, что дано «идеальное двоичное дерево» высоты \\(h\\) с числом узлов \\(n\\). Это предположение не повлияет на корректность результата.

        Рисунок 8-5   Число узлов на каждом уровне идеального двоичного дерева

        Как показано на рисунке 8-5, максимальное число итераций упорядочивания сверху вниз для некоторого узла равно расстоянию от этого узла до листового узла, а это расстояние как раз и есть высота узла. Поэтому мы можем просуммировать для каждого уровня выражение «число узлов \\(\\times\\) высота узла» и получить суммарное число итераций упорядочивания для всех узлов.

        \\[ T(h) = 2^0h + 2^1(h-1) + 2^2(h-2) + \\dots + 2^{(h-1)}\\times1 \\]

        Чтобы упростить это выражение, воспользуемся школьными знаниями о последовательностях и сначала умножим \\(T(h)\\) на \\(2\\) :

        \\[ \\begin{aligned} T(h) & = 2^0h + 2^1(h-1) + 2^2(h-2) + \\dots + 2^{h-1}\\times1 \\newline 2 T(h) & = 2^1h + 2^2(h-1) + 2^3(h-2) + \\dots + 2^{h}\\times1 \\newline \\end{aligned} \\]

        Используя метод вычитания со сдвигом, вычтем из нижней строки \\(2 T(h)\\) верхнюю строку \\(T(h)\\) , тогда получим:

        \\[ 2T(h) - T(h) = T(h) = -2^0h + 2^1 + 2^2 + \\dots + 2^{h-1} + 2^h \\]

        Из этого выражения видно, что \\(T(h)\\) представляет собой геометрическую прогрессию, поэтому можно напрямую применить формулу суммы и получить временную сложность:

        \\[ \\begin{aligned} T(h) & = 2 \\frac{1 - 2^h}{1 - 2} - h \\newline & = 2^{h+1} - h - 2 \\newline & = O(2^h) \\end{aligned} \\]

        Далее, число узлов идеального двоичного дерева высоты \\(h\\) равно \\(n = 2^{h+1} - 1\\) , поэтому несложно получить сложность \\(O(2^h) = O(n)\\) . Из этого вывода следует, что построение кучи из входного списка имеет временную сложность \\(O(n)\\) , то есть выполняется очень эффективно.

        ","path":["Глава 8. Куча","8.2   Построение кучи"],"tags":[]},{"location":"chapter_heap/heap/","level":1,"title":"8.1   Куча","text":"

        Куча (heap) - это полное двоичное дерево, удовлетворяющее определенным условиям. Основных типов кучи два, как показано на рисунке 8-1.

        • Минимальная куча (min heap): значение любого узла \\(\\leq\\) значения его дочерних узлов.
        • Максимальная куча (max heap): значение любого узла \\(\\geq\\) значения его дочерних узлов.

        Рисунок 8-1   Минимальная куча и максимальная куча

        Куча, являясь частным случаем полного двоичного дерева, обладает следующими свойствами.

        • Узлы самого нижнего уровня заполняются слева, а все остальные уровни заполнены полностью.
        • Корневой узел двоичного дерева мы называем вершиной кучи, а самый правый узел нижнего уровня - основанием кучи.
        • Для максимальной (минимальной) кучи значение элемента на вершине, то есть у корневого узла, является максимальным (минимальным).
        ","path":["Глава 8. Куча","8.1   Куча"],"tags":[]},{"location":"chapter_heap/heap/#811","level":2,"title":"8.1.1   Распространенные операции с кучей","text":"

        Нужно отметить, что многие языки программирования предоставляют не саму кучу, а очередь с приоритетом (priority queue) - абстрактную структуру данных, определяемую как очередь, в которой элементы извлекаются в соответствии с приоритетом.

        На практике куча обычно используется для реализации очереди с приоритетом, а максимальная куча эквивалентна очереди с приоритетом, в которой элементы извлекаются по убыванию. С точки зрения использования очередь с приоритетом и куча могут считаться эквивалентными структурами данных. Поэтому в этой книге мы не будем специально различать их и в дальнейшем будем единообразно называть кучей.

        Распространенные операции с кучей приведены в таблице 8-1. Конкретные имена методов зависят от языка программирования.

        Таблица 8-1   Эффективность операций с кучей

        Имя метода Описание Временная сложность push() Поместить элемент в кучу \\(O(\\log n)\\) pop() Извлечь элемент с вершины кучи \\(O(\\log n)\\) peek() Получить доступ к вершине кучи (для max / min кучи это соответственно максимум / минимум) \\(O(1)\\) size() Получить число элементов в куче \\(O(1)\\) isEmpty() Проверить, пуста ли куча \\(O(1)\\)

        В реальных приложениях мы можем напрямую использовать классы кучи, предоставляемые языком программирования, или классы очереди с приоритетом.

        Подобно сортировкам «по возрастанию» и «по убыванию», мы можем переключаться между «минимальной кучей» и «максимальной кучей», изменяя flag или модифицируя Comparator . Код приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby heap.py
        # Инициализация минимальной кучи\nmin_heap, flag = [], 1\n# Инициализация максимальной кучи\nmax_heap, flag = [], -1\n\n# Модуль heapq в Python по умолчанию реализует минимальную кучу\n# Если инвертировать знак элемента перед добавлением, то отношение порядка перевернется и так реализуется максимальная куча\n# В этом примере flag = 1 соответствует минимальной куче, а flag = -1 - максимальной\n\n# Добавление элементов в кучу\nheapq.heappush(max_heap, flag * 1)\nheapq.heappush(max_heap, flag * 3)\nheapq.heappush(max_heap, flag * 2)\nheapq.heappush(max_heap, flag * 5)\nheapq.heappush(max_heap, flag * 4)\n\n# Получение элемента на вершине кучи\npeek: int = flag * max_heap[0] # 5\n\n# Извлечение элемента с вершины кучи\n# Извлеченные элементы образуют последовательность по убыванию\nval = flag * heapq.heappop(max_heap) # 5\nval = flag * heapq.heappop(max_heap) # 4\nval = flag * heapq.heappop(max_heap) # 3\nval = flag * heapq.heappop(max_heap) # 2\nval = flag * heapq.heappop(max_heap) # 1\n\n# Получение размера кучи\nsize: int = len(max_heap)\n\n# Проверка, пуста ли куча\nis_empty: bool = not max_heap\n\n# Построение кучи из входного списка\nmin_heap: list[int] = [1, 3, 2, 5, 4]\nheapq.heapify(min_heap)\n
        heap.cpp
        /* Инициализация кучи */\n// Инициализация минимальной кучи\npriority_queue<int, vector<int>, greater<int>> minHeap;\n// Инициализация максимальной кучи\npriority_queue<int, vector<int>, less<int>> maxHeap;\n\n/* Добавление элементов в кучу */\nmaxHeap.push(1);\nmaxHeap.push(3);\nmaxHeap.push(2);\nmaxHeap.push(5);\nmaxHeap.push(4);\n\n/* Получение элемента на вершине кучи */\nint peek = maxHeap.top(); // 5\n\n/* Извлечение элемента с вершины кучи */\n// Извлеченные элементы образуют последовательность по убыванию\nmaxHeap.pop(); // 5\nmaxHeap.pop(); // 4\nmaxHeap.pop(); // 3\nmaxHeap.pop(); // 2\nmaxHeap.pop(); // 1\n\n/* Получение размера кучи */\nint size = maxHeap.size();\n\n/* Проверка, пуста ли куча */\nbool isEmpty = maxHeap.empty();\n\n/* Построение кучи из входного списка */\nvector<int> input{1, 3, 2, 5, 4};\npriority_queue<int, vector<int>, greater<int>> minHeap(input.begin(), input.end());\n
        heap.java
        /* Инициализация кучи */\n// Инициализация минимальной кучи\nQueue<Integer> minHeap = new PriorityQueue<>();\n// Инициализация максимальной кучи (достаточно изменить Comparator через lambda)\nQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a);\n\n/* Добавление элементов в кучу */\nmaxHeap.offer(1);\nmaxHeap.offer(3);\nmaxHeap.offer(2);\nmaxHeap.offer(5);\nmaxHeap.offer(4);\n\n/* Получение элемента на вершине кучи */\nint peek = maxHeap.peek(); // 5\n\n/* Извлечение элемента с вершины кучи */\n// Извлеченные элементы образуют последовательность по убыванию\npeek = maxHeap.poll(); // 5\npeek = maxHeap.poll(); // 4\npeek = maxHeap.poll(); // 3\npeek = maxHeap.poll(); // 2\npeek = maxHeap.poll(); // 1\n\n/* Получение размера кучи */\nint size = maxHeap.size();\n\n/* Проверка, пуста ли куча */\nboolean isEmpty = maxHeap.isEmpty();\n\n/* Построение кучи из входного списка */\nminHeap = new PriorityQueue<>(Arrays.asList(1, 3, 2, 5, 4));\n
        heap.cs
        /* Инициализация кучи */\n// Инициализация минимальной кучи\nPriorityQueue<int, int> minHeap = new();\n// Инициализация максимальной кучи (достаточно изменить Comparer через lambda)\nPriorityQueue<int, int> maxHeap = new(Comparer<int>.Create((x, y) => y.CompareTo(x)));\n\n/* Добавление элементов в кучу */\nmaxHeap.Enqueue(1, 1);\nmaxHeap.Enqueue(3, 3);\nmaxHeap.Enqueue(2, 2);\nmaxHeap.Enqueue(5, 5);\nmaxHeap.Enqueue(4, 4);\n\n/* Получение элемента на вершине кучи */\nint peek = maxHeap.Peek();//5\n\n/* Извлечение элемента с вершины кучи */\n// Извлеченные элементы образуют последовательность по убыванию\npeek = maxHeap.Dequeue();  // 5\npeek = maxHeap.Dequeue();  // 4\npeek = maxHeap.Dequeue();  // 3\npeek = maxHeap.Dequeue();  // 2\npeek = maxHeap.Dequeue();  // 1\n\n/* Получение размера кучи */\nint size = maxHeap.Count;\n\n/* Проверка, пуста ли куча */\nbool isEmpty = maxHeap.Count == 0;\n\n/* Построение кучи из входного списка */\nminHeap = new PriorityQueue<int, int>([(1, 1), (3, 3), (2, 2), (5, 5), (4, 4)]);\n
        heap.go
        // В Go можно построить целочисленную максимальную кучу, реализовав heap.Interface\n// Для реализации heap.Interface также нужно реализовать sort.Interface\ntype intHeap []any\n\n// Метод Push из heap.Interface, реализует добавление элемента в кучу\nfunc (h *intHeap) Push(x any) {\n    // Push и Pop используют pointer receiver\n    // Потому что они не только изменяют содержимое среза, но и его длину\n    *h = append(*h, x.(int))\n}\n\n// Метод Pop из heap.Interface, реализует извлечение элемента с вершины кучи\nfunc (h *intHeap) Pop() any {\n    // Извлекаемый элемент хранится в конце\n    last := (*h)[len(*h)-1]\n    *h = (*h)[:len(*h)-1]\n    return last\n}\n\n// Метод Len из sort.Interface\nfunc (h *intHeap) Len() int {\n    return len(*h)\n}\n\n// Метод Less из sort.Interface\nfunc (h *intHeap) Less(i, j int) bool {\n    // Для минимальной кучи здесь нужно заменить сравнение на <\n    return (*h)[i].(int) > (*h)[j].(int)\n}\n\n// Метод Swap из sort.Interface\nfunc (h *intHeap) Swap(i, j int) {\n    (*h)[i], (*h)[j] = (*h)[j], (*h)[i]\n}\n\n// Top получает элемент на вершине кучи\nfunc (h *intHeap) Top() any {\n    return (*h)[0]\n}\n\n/* Driver Code */\nfunc TestHeap(t *testing.T) {\n    /* Инициализация кучи */\n    // Инициализация максимальной кучи\n    maxHeap := &intHeap{}\n    heap.Init(maxHeap)\n    /* Добавление элементов в кучу */\n    // Вызываем методы heap.Interface для добавления элементов\n    heap.Push(maxHeap, 1)\n    heap.Push(maxHeap, 3)\n    heap.Push(maxHeap, 2)\n    heap.Push(maxHeap, 4)\n    heap.Push(maxHeap, 5)\n\n    /* Получение элемента на вершине кучи */\n    top := maxHeap.Top()\n    fmt.Printf(\"Элемент на вершине кучи: %d\\n\", top)\n\n    /* Извлечение элемента с вершины кучи */\n    // Вызываем методы heap.Interface для удаления элементов\n    heap.Pop(maxHeap) // 5\n    heap.Pop(maxHeap) // 4\n    heap.Pop(maxHeap) // 3\n    heap.Pop(maxHeap) // 2\n    heap.Pop(maxHeap) // 1\n\n    /* Получение размера кучи */\n    size := len(*maxHeap)\n    fmt.Printf(\"Число элементов в куче: %d\\n\", size)\n\n    /* Проверка, пуста ли куча */\n    isEmpty := len(*maxHeap) == 0\n    fmt.Printf(\"Пуста ли куча: %t\\n\", isEmpty)\n}\n
        heap.swift
        /* Инициализация кучи */\n// Тип Heap в Swift поддерживает и max-heap, и min-heap, но требует swift-collections\nvar heap = Heap<Int>()\n\n/* Добавление элементов в кучу */\nheap.insert(1)\nheap.insert(3)\nheap.insert(2)\nheap.insert(5)\nheap.insert(4)\n\n/* Получение элемента на вершине кучи */\nvar peek = heap.max()!\n\n/* Извлечение элемента с вершины кучи */\npeek = heap.removeMax() // 5\npeek = heap.removeMax() // 4\npeek = heap.removeMax() // 3\npeek = heap.removeMax() // 2\npeek = heap.removeMax() // 1\n\n/* Получение размера кучи */\nlet size = heap.count\n\n/* Проверка, пуста ли куча */\nlet isEmpty = heap.isEmpty\n\n/* Построение кучи из входного списка */\nlet heap2 = Heap([1, 3, 2, 5, 4])\n
        heap.js
        // В JavaScript нет встроенного класса Heap\n
        heap.ts
        // В TypeScript нет встроенного класса Heap\n
        heap.dart
        // В Dart нет встроенного класса Heap\n
        heap.rs
        use std::collections::BinaryHeap;\nuse std::cmp::Reverse;\n\n/* Инициализация кучи */\n// Инициализация минимальной кучи\nlet mut min_heap = BinaryHeap::<Reverse<i32>>::new();\n// Инициализация максимальной кучи\nlet mut max_heap = BinaryHeap::new();\n\n/* Добавление элементов в кучу */\nmax_heap.push(1);\nmax_heap.push(3);\nmax_heap.push(2);\nmax_heap.push(5);\nmax_heap.push(4);\n\n/* Получение элемента на вершине кучи */\nlet peek = max_heap.peek().unwrap();  // 5\n\n/* Извлечение элемента с вершины кучи */\n// Извлеченные элементы образуют последовательность по убыванию\nlet peek = max_heap.pop().unwrap();   // 5\nlet peek = max_heap.pop().unwrap();   // 4\nlet peek = max_heap.pop().unwrap();   // 3\nlet peek = max_heap.pop().unwrap();   // 2\nlet peek = max_heap.pop().unwrap();   // 1\n\n/* Получение размера кучи */\nlet size = max_heap.len();\n\n/* Проверка, пуста ли куча */\nlet is_empty = max_heap.is_empty();\n\n/* Построение кучи из входного списка */\nlet min_heap = BinaryHeap::from(vec![Reverse(1), Reverse(3), Reverse(2), Reverse(5), Reverse(4)]);\n
        heap.c
        // В C нет встроенного класса Heap\n
        heap.kt
        /* Инициализация кучи */\n// Инициализация минимальной кучи\nvar minHeap = PriorityQueue<Int>()\n// Инициализация максимальной кучи (достаточно изменить Comparator через lambda)\nval maxHeap = PriorityQueue { a: Int, b: Int -> b - a }\n\n/* Добавление элементов в кучу */\nmaxHeap.offer(1)\nmaxHeap.offer(3)\nmaxHeap.offer(2)\nmaxHeap.offer(5)\nmaxHeap.offer(4)\n\n/* Получение элемента на вершине кучи */\nvar peek = maxHeap.peek() // 5\n\n/* Извлечение элемента с вершины кучи */\n// Извлеченные элементы образуют последовательность по убыванию\npeek = maxHeap.poll() // 5\npeek = maxHeap.poll() // 4\npeek = maxHeap.poll() // 3\npeek = maxHeap.poll() // 2\npeek = maxHeap.poll() // 1\n\n/* Получение размера кучи */\nval size = maxHeap.size\n\n/* Проверка, пуста ли куча */\nval isEmpty = maxHeap.isEmpty()\n\n/* Построение кучи из входного списка */\nminHeap = PriorityQueue(mutableListOf(1, 3, 2, 5, 4))\n
        heap.rb
        # В Ruby нет встроенного класса Heap\n
        Визуализация выполнения

        https://pythontutor.com/render.html#code=import%20heapq%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20min-%D0%BA%D1%83%D1%87%D1%83%0A%20%20%20%20min_heap%2C%20flag%20%3D%20%5B%5D%2C%201%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20max-%D0%BA%D1%83%D1%87%D1%83%0A%20%20%20%20max_heap%2C%20flag%20%3D%20%5B%5D%2C%20-1%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%9C%D0%BE%D0%B4%D1%83%D0%BB%D1%8C%20heapq%20%D0%B2%20Python%20%D0%BF%D0%BE%20%D1%83%D0%BC%D0%BE%D0%BB%D1%87%D0%B0%D0%BD%D0%B8%D1%8E%20%D1%80%D0%B5%D0%B0%D0%BB%D0%B8%D0%B7%D1%83%D0%B5%D1%82%20min-%D0%BA%D1%83%D1%87%D1%83%0A%20%20%20%20%23%20%D0%95%D1%81%D0%BB%D0%B8%20%D0%BF%D0%B5%D1%80%D0%B5%D0%B4%20%D0%BF%D0%BE%D0%BC%D0%B5%D1%89%D0%B5%D0%BD%D0%B8%D0%B5%D0%BC%20%D0%B2%20%D0%BA%D1%83%D1%87%D1%83%20%D0%B1%D1%80%D0%B0%D1%82%D1%8C%20%D0%BE%D1%82%D1%80%D0%B8%D1%86%D0%B0%D0%BD%D0%B8%D0%B5%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D0%B0%2C%20%D0%BC%D0%BE%D0%B6%D0%BD%D0%BE%20%D0%BE%D0%B1%D1%80%D0%B0%D1%82%D0%B8%D1%82%D1%8C%20%D0%BE%D1%82%D0%BD%D0%BE%D1%88%D0%B5%D0%BD%D0%B8%D0%B5%20%D0%BF%D0%BE%D1%80%D1%8F%D0%B4%D0%BA%D0%B0%20%D0%B8%20%D1%82%D0%B5%D0%BC%20%D1%81%D0%B0%D0%BC%D1%8B%D0%BC%20%D1%80%D0%B5%D0%B0%D0%BB%D0%B8%D0%B7%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20max-%D0%BA%D1%83%D1%87%D1%83%0A%20%20%20%20%23%20%D0%92%20%D1%8D%D1%82%D0%BE%D0%BC%20%D0%BF%D1%80%D0%B8%D0%BC%D0%B5%D1%80%D0%B5%20flag%20%3D%201%20%D1%81%D0%BE%D0%BE%D1%82%D0%B2%D0%B5%D1%82%D1%81%D1%82%D0%B2%D1%83%D0%B5%D1%82%20min-%D0%BA%D1%83%D1%87%D0%B5%2C%20%D0%B0%20flag%20%3D%20-1%20%D1%81%D0%BE%D0%BE%D1%82%D0%B2%D0%B5%D1%82%D1%81%D1%82%D0%B2%D1%83%D0%B5%D1%82%20max-%D0%BA%D1%83%D1%87%D0%B5%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%94%D0%BE%D0%B1%D0%B0%D0%B2%D0%B8%D1%82%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B2%20%D0%BA%D1%83%D1%87%D1%83%0A%20%20%20%20heapq.heappush%28max_heap%2C%20flag%20%2A%201%29%0A%20%20%20%20heapq.heappush%28max_heap%2C%20flag%20%2A%203%29%0A%20%20%20%20heapq.heappush%28max_heap%2C%20flag%20%2A%202%29%0A%20%20%20%20heapq.heappush%28max_heap%2C%20flag%20%2A%205%29%0A%20%20%20%20heapq.heappush%28max_heap%2C%20flag%20%2A%204%29%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B8%D1%82%D1%8C%20%D0%B2%D0%B5%D1%80%D1%85%D0%BD%D0%B8%D0%B9%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%BA%D1%83%D1%87%D0%B8%0A%20%20%20%20peek%20%3D%20flag%20%2A%20max_heap%5B0%5D%20%23%205%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%98%D0%B7%D0%B2%D0%BB%D0%B5%D1%87%D1%8C%20%D0%B2%D0%B5%D1%80%D1%85%D0%BD%D0%B8%D0%B9%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B8%D0%B7%20%D0%BA%D1%83%D1%87%D0%B8%0A%20%20%20%20%23%20%D0%98%D0%B7%D0%B2%D0%BB%D0%B5%D1%87%D0%B5%D0%BD%D0%BD%D1%8B%D0%B5%20%D0%B8%D0%B7%20%D0%BA%D1%83%D1%87%D0%B8%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D1%8B%20%D0%BE%D0%B1%D1%80%D0%B0%D0%B7%D1%83%D1%8E%D1%82%20%D0%BF%D0%BE%D1%81%D0%BB%D0%B5%D0%B4%D0%BE%D0%B2%D0%B0%D1%82%D0%B5%D0%BB%D1%8C%D0%BD%D0%BE%D1%81%D1%82%D1%8C%20%D0%BE%D1%82%20%D0%B1%D0%BE%D0%BB%D1%8C%D1%88%D0%B5%D0%B3%D0%BE%20%D0%BA%20%D0%BC%D0%B5%D0%BD%D1%8C%D1%88%D0%B5%D0%BC%D1%83%0A%20%20%20%20val%20%3D%20flag%20%2A%20heapq.heappop%28max_heap%29%20%23%205%0A%20%20%20%20val%20%3D%20flag%20%2A%20heapq.heappop%28max_heap%29%20%23%204%0A%20%20%20%20val%20%3D%20flag%20%2A%20heapq.heappop%28max_heap%29%20%23%203%0A%20%20%20%20val%20%3D%20flag%20%2A%20heapq.heappop%28max_heap%29%20%23%202%0A%20%20%20%20val%20%3D%20flag%20%2A%20heapq.heappop%28max_heap%29%20%23%201%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B8%D1%82%D1%8C%20%D1%80%D0%B0%D0%B7%D0%BC%D0%B5%D1%80%20%D0%BA%D1%83%D1%87%D0%B8%0A%20%20%20%20size%20%3D%20len%28max_heap%29%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%9F%D1%80%D0%BE%D0%B2%D0%B5%D1%80%D0%B8%D1%82%D1%8C%2C%20%D0%BF%D1%83%D1%81%D1%82%D0%B0%20%D0%BB%D0%B8%20%D0%BA%D1%83%D1%87%D0%B0%0A%20%20%20%20is_empty%20%3D%20not%20max_heap%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%92%D1%85%D0%BE%D0%B4%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%D0%B8%D0%BF%D0%BE%D1%81%D1%82%D1%80%D0%BE%D0%B5%D0%BD%D0%B8%D0%B5%20%D0%BA%D1%83%D1%87%D0%B8%0A%20%20%20%20min_heap%20%3D%20%5B1%2C%203%2C%202%2C%205%2C%204%5D%0A%20%20%20%20heapq.heapify%28min_heap%29&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

        ","path":["Глава 8. Куча","8.1   Куча"],"tags":[]},{"location":"chapter_heap/heap/#812","level":2,"title":"8.1.2   Реализация кучи","text":"

        Ниже реализуется максимальная куча. Чтобы преобразовать ее в минимальную кучу, достаточно инвертировать всю логику сравнений по величине, например заменить \\(\\geq\\) на \\(\\leq\\) . Заинтересованные читатели могут попробовать реализовать это самостоятельно.

        ","path":["Глава 8. Куча","8.1   Куча"],"tags":[]},{"location":"chapter_heap/heap/#1","level":3,"title":"1.   Хранение и представление кучи","text":"

        В разделе «Двоичные деревья» мы уже говорили, что полное двоичное дерево очень удобно представлять массивом. Поскольку куча как раз и является полным двоичным деревом, для хранения кучи мы также будем использовать массив.

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

        Как показано на рисунке 8-2, для заданного индекса \\(i\\) индекс левого дочернего узла равен \\(2i + 1\\) , правого дочернего узла - \\(2i + 2\\) , а родительского узла - \\((i - 1) / 2\\) с округлением вниз. Если индекс выходит за допустимые границы, это означает пустой узел или отсутствие узла.

        Рисунок 8-2   Представление и хранение кучи

        Мы можем инкапсулировать формулы отображения индексов в функции, чтобы потом было удобнее ими пользоваться:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
        def left(self, i: int) -> int:\n    \"\"\"Получить индекс левого дочернего узла\"\"\"\n    return 2 * i + 1\n\ndef right(self, i: int) -> int:\n    \"\"\"Получить индекс правого дочернего узла\"\"\"\n    return 2 * i + 2\n\ndef parent(self, i: int) -> int:\n    \"\"\"Получить индекс родительского узла\"\"\"\n    return (i - 1) // 2  # Округление вниз при делении\n
        my_heap.cpp
        /* Получить индекс левого дочернего узла */\nint left(int i) {\n    return 2 * i + 1;\n}\n\n/* Получить индекс правого дочернего узла */\nint right(int i) {\n    return 2 * i + 2;\n}\n\n/* Получить индекс родительского узла */\nint parent(int i) {\n    return (i - 1) / 2; // Округление вниз при делении\n}\n
        my_heap.java
        /* Получить индекс левого дочернего узла */\nint left(int i) {\n    return 2 * i + 1;\n}\n\n/* Получить индекс правого дочернего узла */\nint right(int i) {\n    return 2 * i + 2;\n}\n\n/* Получить индекс родительского узла */\nint parent(int i) {\n    return (i - 1) / 2; // Округление вниз при делении\n}\n
        my_heap.cs
        /* Получить индекс левого дочернего узла */\nint Left(int i) {\n    return 2 * i + 1;\n}\n\n/* Получить индекс правого дочернего узла */\nint Right(int i) {\n    return 2 * i + 2;\n}\n\n/* Получить индекс родительского узла */\nint Parent(int i) {\n    return (i - 1) / 2; // Округление вниз при делении\n}\n
        my_heap.go
        /* Получить индекс левого дочернего узла */\nfunc (h *maxHeap) left(i int) int {\n    return 2*i + 1\n}\n\n/* Получить индекс правого дочернего узла */\nfunc (h *maxHeap) right(i int) int {\n    return 2*i + 2\n}\n\n/* Получить индекс родительского узла */\nfunc (h *maxHeap) parent(i int) int {\n    // Округление вниз при делении\n    return (i - 1) / 2\n}\n
        my_heap.swift
        /* Получить индекс левого дочернего узла */\nfunc left(i: Int) -> Int {\n    2 * i + 1\n}\n\n/* Получить индекс правого дочернего узла */\nfunc right(i: Int) -> Int {\n    2 * i + 2\n}\n\n/* Получить индекс родительского узла */\nfunc parent(i: Int) -> Int {\n    (i - 1) / 2 // Округление вниз при делении\n}\n
        my_heap.js
        /* Получить индекс левого дочернего узла */\n#left(i) {\n    return 2 * i + 1;\n}\n\n/* Получить индекс правого дочернего узла */\n#right(i) {\n    return 2 * i + 2;\n}\n\n/* Получить индекс родительского узла */\n#parent(i) {\n    return Math.floor((i - 1) / 2); // Округление вниз при делении\n}\n
        my_heap.ts
        /* Получить индекс левого дочернего узла */\nleft(i: number): number {\n    return 2 * i + 1;\n}\n\n/* Получить индекс правого дочернего узла */\nright(i: number): number {\n    return 2 * i + 2;\n}\n\n/* Получить индекс родительского узла */\nparent(i: number): number {\n    return Math.floor((i - 1) / 2); // Округление вниз при делении\n}\n
        my_heap.dart
        /* Получить индекс левого дочернего узла */\nint _left(int i) {\n  return 2 * i + 1;\n}\n\n/* Получить индекс правого дочернего узла */\nint _right(int i) {\n  return 2 * i + 2;\n}\n\n/* Получить индекс родительского узла */\nint _parent(int i) {\n  return (i - 1) ~/ 2; // Округление вниз при делении\n}\n
        my_heap.rs
        /* Получить индекс левого дочернего узла */\nfn left(i: usize) -> usize {\n    2 * i + 1\n}\n\n/* Получить индекс правого дочернего узла */\nfn right(i: usize) -> usize {\n    2 * i + 2\n}\n\n/* Получить индекс родительского узла */\nfn parent(i: usize) -> usize {\n    (i - 1) / 2 // Округление вниз при делении\n}\n
        my_heap.c
        /* Получить индекс левого дочернего узла */\nint left(MaxHeap *maxHeap, int i) {\n    return 2 * i + 1;\n}\n\n/* Получить индекс правого дочернего узла */\nint right(MaxHeap *maxHeap, int i) {\n    return 2 * i + 2;\n}\n\n/* Получить индекс родительского узла */\nint parent(MaxHeap *maxHeap, int i) {\n    return (i - 1) / 2; // Округление вниз\n}\n
        my_heap.kt
        /* Получить индекс левого дочернего узла */\nfun left(i: Int): Int {\n    return 2 * i + 1\n}\n\n/* Получить индекс правого дочернего узла */\nfun right(i: Int): Int {\n    return 2 * i + 2\n}\n\n/* Получить индекс родительского узла */\nfun parent(i: Int): Int {\n    return (i - 1) / 2 // Округление вниз при делении\n}\n
        my_heap.rb
        ### Получить индекс левого дочернего узла ###\ndef left(i)\n  2 * i + 1\nend\n\n### Получить индекс правого дочернего узла ###\ndef right(i)\n  2 * i + 2\nend\n\n### Получить индекс родительского узла ###\ndef parent(i)\n  (i - 1) / 2     # Округление вниз при делении\nend\n
        ","path":["Глава 8. Куча","8.1   Куча"],"tags":[]},{"location":"chapter_heap/heap/#2","level":3,"title":"2.   Доступ к элементу на вершине кучи","text":"

        Элемент на вершине кучи - это корневой узел двоичного дерева, то есть первый элемент списка:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
        def peek(self) -> int:\n    \"\"\"Доступ к элементу на вершине кучи\"\"\"\n    return self.max_heap[0]\n
        my_heap.cpp
        /* Доступ к элементу на вершине кучи */\nint peek() {\n    return maxHeap[0];\n}\n
        my_heap.java
        /* Доступ к элементу на вершине кучи */\nint peek() {\n    return maxHeap.get(0);\n}\n
        my_heap.cs
        /* Доступ к элементу на вершине кучи */\nint Peek() {\n    return maxHeap[0];\n}\n
        my_heap.go
        /* Доступ к элементу на вершине кучи */\nfunc (h *maxHeap) peek() any {\n    return h.data[0]\n}\n
        my_heap.swift
        /* Доступ к элементу на вершине кучи */\nfunc peek() -> Int {\n    maxHeap[0]\n}\n
        my_heap.js
        /* Доступ к элементу на вершине кучи */\npeek() {\n    return this.#maxHeap[0];\n}\n
        my_heap.ts
        /* Доступ к элементу на вершине кучи */\npeek(): number {\n    return this.maxHeap[0];\n}\n
        my_heap.dart
        /* Доступ к элементу на вершине кучи */\nint peek() {\n  return _maxHeap[0];\n}\n
        my_heap.rs
        /* Доступ к элементу на вершине кучи */\nfn peek(&self) -> Option<i32> {\n    self.max_heap.first().copied()\n}\n
        my_heap.c
        /* Доступ к элементу на вершине кучи */\nint peek(MaxHeap *maxHeap) {\n    return maxHeap->data[0];\n}\n
        my_heap.kt
        /* Доступ к элементу на вершине кучи */\nfun peek(): Int {\n    return maxHeap[0]\n}\n
        my_heap.rb
        ### Доступ к элементу на вершине кучи ###\ndef peek\n  @max_heap[0]\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 8. Куча","8.1   Куча"],"tags":[]},{"location":"chapter_heap/heap/#3","level":3,"title":"3.   Добавление элемента в кучу","text":"

        Пусть дан элемент val . Сначала мы помещаем его в основание кучи. После добавления свойства кучи могут нарушиться, потому что val может оказаться больше, чем другие элементы в куче. Поэтому необходимо восстановить порядок на пути от вставленного узла к корню. Эта операция называется упорядочиванием кучи.

        Рассмотрим ситуацию, когда упорядочивание выполняется снизу вверх, начиная от только что вставленного узла. Как показано на рисунке 8-3, мы сравниваем значение вставленного узла со значением его родителя. Если вставленный узел больше, то меняем их местами. Затем продолжаем выполнять ту же операцию и последовательно восстанавливать корректность всех узлов по пути снизу вверх, пока не выйдем за корень или не встретим узел, для которого обмен не требуется.

        <1><2><3><4><5><6><7><8><9>

        Рисунок 8-3   Шаги добавления элемента в кучу

        Пусть общее число узлов равно \\(n\\) , тогда высота дерева равна \\(O(\\log n)\\) . Следовательно, максимальное число итераций операции упорядочивания кучи тоже не превышает \\(O(\\log n)\\) . Отсюда временная сложность добавления элемента в кучу равна \\(O(\\log n)\\) . Код приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
        def push(self, val: int):\n    \"\"\"Добавление элемента в кучу\"\"\"\n    # Добавление узла\n    self.max_heap.append(val)\n    # Просеивание снизу вверх\n    self.sift_up(self.size() - 1)\n\ndef sift_up(self, i: int):\n    \"\"\"Начиная с узла i, выполнить просеивание снизу вверх\"\"\"\n    while True:\n        # Получение родительского узла для узла i\n        p = self.parent(i)\n        # Завершить heapify, когда «корневой узел уже пройден» или «узел не требует исправления»\n        if p < 0 or self.max_heap[i] <= self.max_heap[p]:\n            break\n        # Поменять два узла местами\n        self.swap(i, p)\n        # Циклическое просеивание вверх\n        i = p\n
        my_heap.cpp
        /* Добавление элемента в кучу */\nvoid push(int val) {\n    // Добавление узла\n    maxHeap.push_back(val);\n    // Просеивание снизу вверх\n    siftUp(size() - 1);\n}\n\n/* Начиная с узла i, выполнить просеивание снизу вверх */\nvoid siftUp(int i) {\n    while (true) {\n        // Получение родительского узла для узла i\n        int p = parent(i);\n        // Завершить heapify, когда «корневой узел уже пройден» или «узел не требует исправления»\n        if (p < 0 || maxHeap[i] <= maxHeap[p])\n            break;\n        // Поменять два узла местами\n        swap(maxHeap[i], maxHeap[p]);\n        // Циклическое просеивание вверх\n        i = p;\n    }\n}\n
        my_heap.java
        /* Добавление элемента в кучу */\nvoid push(int val) {\n    // Добавление узла\n    maxHeap.add(val);\n    // Просеивание снизу вверх\n    siftUp(size() - 1);\n}\n\n/* Начиная с узла i, выполнить просеивание снизу вверх */\nvoid siftUp(int i) {\n    while (true) {\n        // Получение родительского узла для узла i\n        int p = parent(i);\n        // Завершить heapify, когда «корневой узел уже пройден» или «узел не требует исправления»\n        if (p < 0 || maxHeap.get(i) <= maxHeap.get(p))\n            break;\n        // Поменять два узла местами\n        swap(i, p);\n        // Циклическое просеивание вверх\n        i = p;\n    }\n}\n
        my_heap.cs
        /* Добавление элемента в кучу */\nvoid Push(int val) {\n    // Добавление узла\n    maxHeap.Add(val);\n    // Просеивание снизу вверх\n    SiftUp(Size() - 1);\n}\n\n/* Начиная с узла i, выполнить просеивание снизу вверх */\nvoid SiftUp(int i) {\n    while (true) {\n        // Получение родительского узла для узла i\n        int p = Parent(i);\n        // Если «выход за пределы корневого узла» или «узел не требует исправления», завершить просеивание\n        if (p < 0 || maxHeap[i] <= maxHeap[p])\n            break;\n        // Поменять два узла местами\n        Swap(i, p);\n        // Циклическое просеивание вверх\n        i = p;\n    }\n}\n
        my_heap.go
        /* Добавление элемента в кучу */\nfunc (h *maxHeap) push(val any) {\n    // Добавление узла\n    h.data = append(h.data, val)\n    // Просеивание снизу вверх\n    h.siftUp(len(h.data) - 1)\n}\n\n/* Начиная с узла i, выполнить просеивание снизу вверх */\nfunc (h *maxHeap) siftUp(i int) {\n    for true {\n        // Получение родительского узла для узла i\n        p := h.parent(i)\n        // Завершить heapify, когда «корневой узел уже пройден» или «узел не требует исправления»\n        if p < 0 || h.data[i].(int) <= h.data[p].(int) {\n            break\n        }\n        // Поменять два узла местами\n        h.swap(i, p)\n        // Циклическое просеивание вверх\n        i = p\n    }\n}\n
        my_heap.swift
        /* Добавление элемента в кучу */\nfunc push(val: Int) {\n    // Добавление узла\n    maxHeap.append(val)\n    // Просеивание снизу вверх\n    siftUp(i: size() - 1)\n}\n\n/* Начиная с узла i, выполнить просеивание снизу вверх */\nfunc siftUp(i: Int) {\n    var i = i\n    while true {\n        // Получение родительского узла для узла i\n        let p = parent(i: i)\n        // Завершить heapify, когда «корневой узел уже пройден» или «узел не требует исправления»\n        if p < 0 || maxHeap[i] <= maxHeap[p] {\n            break\n        }\n        // Поменять два узла местами\n        swap(i: i, j: p)\n        // Циклическое просеивание вверх\n        i = p\n    }\n}\n
        my_heap.js
        /* Добавление элемента в кучу */\npush(val) {\n    // Добавление узла\n    this.#maxHeap.push(val);\n    // Просеивание снизу вверх\n    this.#siftUp(this.size() - 1);\n}\n\n/* Начиная с узла i, выполнить просеивание снизу вверх */\n#siftUp(i) {\n    while (true) {\n        // Получение родительского узла для узла i\n        const p = this.#parent(i);\n        // Завершить heapify, когда «корневой узел уже пройден» или «узел не требует исправления»\n        if (p < 0 || this.#maxHeap[i] <= this.#maxHeap[p]) break;\n        // Поменять два узла местами\n        this.#swap(i, p);\n        // Циклическое просеивание вверх\n        i = p;\n    }\n}\n
        my_heap.ts
        /* Добавление элемента в кучу */\npush(val: number): void {\n    // Добавление узла\n    this.maxHeap.push(val);\n    // Просеивание снизу вверх\n    this.siftUp(this.size() - 1);\n}\n\n/* Начиная с узла i, выполнить просеивание снизу вверх */\nsiftUp(i: number): void {\n    while (true) {\n        // Получение родительского узла для узла i\n        const p = this.parent(i);\n        // Завершить heapify, когда «корневой узел уже пройден» или «узел не требует исправления»\n        if (p < 0 || this.maxHeap[i] <= this.maxHeap[p]) break;\n        // Поменять два узла местами\n        this.swap(i, p);\n        // Циклическое просеивание вверх\n        i = p;\n    }\n}\n
        my_heap.dart
        /* Добавление элемента в кучу */\nvoid push(int val) {\n  // Добавление узла\n  _maxHeap.add(val);\n  // Просеивание снизу вверх\n  siftUp(size() - 1);\n}\n\n/* Начиная с узла i, выполнить просеивание снизу вверх */\nvoid siftUp(int i) {\n  while (true) {\n    // Получение родительского узла для узла i\n    int p = _parent(i);\n    // Завершить heapify, когда «корневой узел уже пройден» или «узел не требует исправления»\n    if (p < 0 || _maxHeap[i] <= _maxHeap[p]) {\n      break;\n    }\n    // Поменять два узла местами\n    _swap(i, p);\n    // Циклическое просеивание вверх\n    i = p;\n  }\n}\n
        my_heap.rs
        /* Добавление элемента в кучу */\nfn push(&mut self, val: i32) {\n    // Добавление узла\n    self.max_heap.push(val);\n    // Просеивание снизу вверх\n    self.sift_up(self.size() - 1);\n}\n\n/* Начиная с узла i, выполнить просеивание снизу вверх */\nfn sift_up(&mut self, mut i: usize) {\n    loop {\n        // Если узел i уже является вершиной кучи, завершить просеивание\n        if i == 0 {\n            break;\n        }\n        // Получение родительского узла для узла i\n        let p = Self::parent(i);\n        // Когда «узел не требует исправления», завершить просеивание\n        if self.max_heap[i] <= self.max_heap[p] {\n            break;\n        }\n        // Поменять два узла местами\n        self.swap(i, p);\n        // Циклическое просеивание вверх\n        i = p;\n    }\n}\n
        my_heap.c
        /* Добавление элемента в кучу */\nvoid push(MaxHeap *maxHeap, int val) {\n    // По умолчанию не следует добавлять так много узлов\n    if (maxHeap->size == MAX_SIZE) {\n        printf(\"heap is full!\");\n        return;\n    }\n    // Добавление узла\n    maxHeap->data[maxHeap->size] = val;\n    maxHeap->size++;\n\n    // Просеивание снизу вверх\n    siftUp(maxHeap, maxHeap->size - 1);\n}\n\n/* Начиная с узла i, выполнить просеивание снизу вверх */\nvoid siftUp(MaxHeap *maxHeap, int i) {\n    while (true) {\n        // Получение родительского узла для узла i\n        int p = parent(maxHeap, i);\n        // Завершить heapify, когда «корневой узел уже пройден» или «узел не требует исправления»\n        if (p < 0 || maxHeap->data[i] <= maxHeap->data[p]) {\n            break;\n        }\n        // Поменять два узла местами\n        swap(maxHeap, i, p);\n        // Циклическое просеивание вверх\n        i = p;\n    }\n}\n
        my_heap.kt
        /* Добавление элемента в кучу */\nfun push(_val: Int) {\n    // Добавление узла\n    maxHeap.add(_val)\n    // Просеивание снизу вверх\n    siftUp(size() - 1)\n}\n\n/* Начиная с узла i, выполнить просеивание снизу вверх */\nfun siftUp(it: Int) {\n    // Параметры функций в Kotlin неизменяемы, поэтому создается временная переменная\n    var i = it\n    while (true) {\n        // Получение родительского узла для узла i\n        val p = parent(i)\n        // Завершить heapify, когда «корневой узел уже пройден» или «узел не требует исправления»\n        if (p < 0 || maxHeap[i] <= maxHeap[p]) break\n        // Поменять два узла местами\n        swap(i, p)\n        // Циклическое просеивание вверх\n        i = p\n    }\n}\n
        my_heap.rb
        ### Добавление элемента в кучу ###\ndef push(val)\n  # Добавление узла\n  @max_heap << val\n  # Просеивание снизу вверх\n  sift_up(size - 1)\nend\n\n### Начиная с узла i, выполнить просеивание снизу вверх ###\ndef sift_up(i)\n  loop do\n    # Получение родительского узла для узла i\n    p = parent(i)\n    # Завершить heapify, когда «корневой узел уже пройден» или «узел не требует исправления»\n    break if p < 0 || @max_heap[i] <= @max_heap[p]\n    # Поменять два узла местами\n    swap(i, p)\n    # Циклическое просеивание вверх\n    i = p\n  end\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 8. Куча","8.1   Куча"],"tags":[]},{"location":"chapter_heap/heap/#4","level":3,"title":"4.   Извлечение элемента с вершины кучи","text":"

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

        1. Поменять местами элемент на вершине кучи и элемент у основания кучи, то есть поменять корневой узел с самым правым листовым узлом.
        2. После обмена удалить основание кучи из списка. Стоит отметить, что, поскольку обмен уже выполнен, фактически удаляется исходный элемент вершины кучи.
        3. Начиная от корневого узла, выполнить упорядочивание кучи сверху вниз.

        Как показано на рисунке 8-4, направление операции упорядочивания кучи сверху вниз противоположно операции упорядочивания кучи снизу вверх. Мы сравниваем значение корневого узла со значениями двух дочерних узлов, выбираем больший дочерний узел и меняем его местами с корневым узлом. Затем циклически повторяем ту же операцию, пока не выйдем за листовой узел или не встретим узел, который уже не требует обмена.

        <1><2><3><4><5><6><7><8><9><10>

        Рисунок 8-4   Шаги извлечения элемента с вершины кучи

        Как и операция добавления в кучу, операция извлечения элемента с вершины кучи также имеет временную сложность \\(O(\\log n)\\) . Код приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
        def pop(self) -> int:\n    \"\"\"Извлечение элемента из кучи\"\"\"\n    # Обработка пустого случая\n    if self.is_empty():\n        raise IndexError(\"куча пуста\")\n    # Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n    self.swap(0, self.size() - 1)\n    # Удаление узла\n    val = self.max_heap.pop()\n    # Просеивание сверху вниз\n    self.sift_down(0)\n    # Вернуть элемент с вершины кучи\n    return val\n\ndef sift_down(self, i: int):\n    \"\"\"Начиная с узла i, выполнить просеивание сверху вниз\"\"\"\n    while True:\n        # Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n        l, r, ma = self.left(i), self.right(i), i\n        if l < self.size() and self.max_heap[l] > self.max_heap[ma]:\n            ma = l\n        if r < self.size() and self.max_heap[r] > self.max_heap[ma]:\n            ma = r\n        # Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if ma == i:\n            break\n        # Поменять два узла местами\n        self.swap(i, ma)\n        # Циклическое просеивание вниз\n        i = ma\n
        my_heap.cpp
        /* Извлечение элемента из кучи */\nvoid pop() {\n    // Обработка пустого случая\n    if (isEmpty()) {\n        throw out_of_range(\"куча пуста\");\n    }\n    // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n    swap(maxHeap[0], maxHeap[size() - 1]);\n    // Удаление узла\n    maxHeap.pop_back();\n    // Просеивание сверху вниз\n    siftDown(0);\n}\n\n/* Начиная с узла i, выполнить просеивание сверху вниз */\nvoid siftDown(int i) {\n    while (true) {\n        // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n        int l = left(i), r = right(i), ma = i;\n        if (l < size() && maxHeap[l] > maxHeap[ma])\n            ma = l;\n        if (r < size() && maxHeap[r] > maxHeap[ma])\n            ma = r;\n        // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if (ma == i)\n            break;\n        swap(maxHeap[i], maxHeap[ma]);\n        // Циклическое просеивание вниз\n        i = ma;\n    }\n}\n
        my_heap.java
        /* Извлечение элемента из кучи */\nint pop() {\n    // Обработка пустого случая\n    if (isEmpty())\n        throw new IndexOutOfBoundsException();\n    // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n    swap(0, size() - 1);\n    // Удаление узла\n    int val = maxHeap.remove(size() - 1);\n    // Просеивание сверху вниз\n    siftDown(0);\n    // Вернуть элемент с вершины кучи\n    return val;\n}\n\n/* Начиная с узла i, выполнить просеивание сверху вниз */\nvoid siftDown(int i) {\n    while (true) {\n        // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n        int l = left(i), r = right(i), ma = i;\n        if (l < size() && maxHeap.get(l) > maxHeap.get(ma))\n            ma = l;\n        if (r < size() && maxHeap.get(r) > maxHeap.get(ma))\n            ma = r;\n        // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if (ma == i)\n            break;\n        // Поменять два узла местами\n        swap(i, ma);\n        // Циклическое просеивание вниз\n        i = ma;\n    }\n}\n
        my_heap.cs
        /* Извлечение элемента из кучи */\nint Pop() {\n    // Обработка пустого случая\n    if (IsEmpty())\n        throw new IndexOutOfRangeException();\n    // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n    Swap(0, Size() - 1);\n    // Удаление узла\n    int val = maxHeap.Last();\n    maxHeap.RemoveAt(Size() - 1);\n    // Просеивание сверху вниз\n    SiftDown(0);\n    // Вернуть элемент с вершины кучи\n    return val;\n}\n\n/* Начиная с узла i, выполнить просеивание сверху вниз */\nvoid SiftDown(int i) {\n    while (true) {\n        // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n        int l = Left(i), r = Right(i), ma = i;\n        if (l < Size() && maxHeap[l] > maxHeap[ma])\n            ma = l;\n        if (r < Size() && maxHeap[r] > maxHeap[ma])\n            ma = r;\n        // Если «узел i максимален» или «выход за пределы листовых узлов», завершить просеивание\n        if (ma == i) break;\n        // Поменять два узла местами\n        Swap(i, ma);\n        // Циклическое просеивание вниз\n        i = ma;\n    }\n}\n
        my_heap.go
        /* Извлечение элемента из кучи */\nfunc (h *maxHeap) pop() any {\n    // Обработка пустого случая\n    if h.isEmpty() {\n        fmt.Println(\"error\")\n        return nil\n    }\n    // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n    h.swap(0, h.size()-1)\n    // Удаление узла\n    val := h.data[len(h.data)-1]\n    h.data = h.data[:len(h.data)-1]\n    // Просеивание сверху вниз\n    h.siftDown(0)\n\n    // Вернуть элемент с вершины кучи\n    return val\n}\n\n/* Начиная с узла i, выполнить просеивание сверху вниз */\nfunc (h *maxHeap) siftDown(i int) {\n    for true {\n        // Определить узел с максимальным значением среди i, l и r и обозначить его как max\n        l, r, max := h.left(i), h.right(i), i\n        if l < h.size() && h.data[l].(int) > h.data[max].(int) {\n            max = l\n        }\n        if r < h.size() && h.data[r].(int) > h.data[max].(int) {\n            max = r\n        }\n        // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if max == i {\n            break\n        }\n        // Поменять два узла местами\n        h.swap(i, max)\n        // Циклическое просеивание вниз\n        i = max\n    }\n}\n
        my_heap.swift
        /* Извлечение элемента из кучи */\nfunc pop() -> Int {\n    // Обработка пустого случая\n    if isEmpty() {\n        fatalError(\"куча пуста\")\n    }\n    // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n    swap(i: 0, j: size() - 1)\n    // Удаление узла\n    let val = maxHeap.remove(at: size() - 1)\n    // Просеивание сверху вниз\n    siftDown(i: 0)\n    // Вернуть элемент с вершины кучи\n    return val\n}\n\n/* Начиная с узла i, выполнить просеивание сверху вниз */\nfunc siftDown(i: Int) {\n    var i = i\n    while true {\n        // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n        let l = left(i: i)\n        let r = right(i: i)\n        var ma = i\n        if l < size(), maxHeap[l] > maxHeap[ma] {\n            ma = l\n        }\n        if r < size(), maxHeap[r] > maxHeap[ma] {\n            ma = r\n        }\n        // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if ma == i {\n            break\n        }\n        // Поменять два узла местами\n        swap(i: i, j: ma)\n        // Циклическое просеивание вниз\n        i = ma\n    }\n}\n
        my_heap.js
        /* Извлечение элемента из кучи */\npop() {\n    // Обработка пустого случая\n    if (this.isEmpty()) throw new Error('куча пуста');\n    // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n    this.#swap(0, this.size() - 1);\n    // Удаление узла\n    const val = this.#maxHeap.pop();\n    // Просеивание сверху вниз\n    this.#siftDown(0);\n    // Вернуть элемент с вершины кучи\n    return val;\n}\n\n/* Начиная с узла i, выполнить просеивание сверху вниз */\n#siftDown(i) {\n    while (true) {\n        // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n        const l = this.#left(i),\n            r = this.#right(i);\n        let ma = i;\n        if (l < this.size() && this.#maxHeap[l] > this.#maxHeap[ma]) ma = l;\n        if (r < this.size() && this.#maxHeap[r] > this.#maxHeap[ma]) ma = r;\n        // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if (ma === i) break;\n        // Поменять два узла местами\n        this.#swap(i, ma);\n        // Циклическое просеивание вниз\n        i = ma;\n    }\n}\n
        my_heap.ts
        /* Извлечение элемента из кучи */\npop(): number {\n    // Обработка пустого случая\n    if (this.isEmpty()) throw new RangeError('Heap is empty.');\n    // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n    this.swap(0, this.size() - 1);\n    // Удаление узла\n    const val = this.maxHeap.pop();\n    // Просеивание сверху вниз\n    this.siftDown(0);\n    // Вернуть элемент с вершины кучи\n    return val;\n}\n\n/* Начиная с узла i, выполнить просеивание сверху вниз */\nsiftDown(i: number): void {\n    while (true) {\n        // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n        const l = this.left(i),\n            r = this.right(i);\n        let ma = i;\n        if (l < this.size() && this.maxHeap[l] > this.maxHeap[ma]) ma = l;\n        if (r < this.size() && this.maxHeap[r] > this.maxHeap[ma]) ma = r;\n        // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if (ma === i) break;\n        // Поменять два узла местами\n        this.swap(i, ma);\n        // Циклическое просеивание вниз\n        i = ma;\n    }\n}\n
        my_heap.dart
        /* Извлечение элемента из кучи */\nint pop() {\n  // Обработка пустого случая\n  if (isEmpty()) throw Exception('куча пуста');\n  // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n  _swap(0, size() - 1);\n  // Удаление узла\n  int val = _maxHeap.removeLast();\n  // Просеивание сверху вниз\n  siftDown(0);\n  // Вернуть элемент с вершины кучи\n  return val;\n}\n\n/* Начиная с узла i, выполнить просеивание сверху вниз */\nvoid siftDown(int i) {\n  while (true) {\n    // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n    int l = _left(i);\n    int r = _right(i);\n    int ma = i;\n    if (l < size() && _maxHeap[l] > _maxHeap[ma]) ma = l;\n    if (r < size() && _maxHeap[r] > _maxHeap[ma]) ma = r;\n    // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n    if (ma == i) break;\n    // Поменять два узла местами\n    _swap(i, ma);\n    // Циклическое просеивание вниз\n    i = ma;\n  }\n}\n
        my_heap.rs
        /* Извлечение элемента из кучи */\nfn pop(&mut self) -> i32 {\n    // Обработка пустого случая\n    if self.is_empty() {\n        panic!(\"index out of bounds\");\n    }\n    // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n    self.swap(0, self.size() - 1);\n    // Удаление узла\n    let val = self.max_heap.pop().unwrap();\n    // Просеивание сверху вниз\n    self.sift_down(0);\n    // Вернуть элемент с вершины кучи\n    val\n}\n\n/* Начиная с узла i, выполнить просеивание сверху вниз */\nfn sift_down(&mut self, mut i: usize) {\n    loop {\n        // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n        let (l, r, mut ma) = (Self::left(i), Self::right(i), i);\n        if l < self.size() && self.max_heap[l] > self.max_heap[ma] {\n            ma = l;\n        }\n        if r < self.size() && self.max_heap[r] > self.max_heap[ma] {\n            ma = r;\n        }\n        // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if ma == i {\n            break;\n        }\n        // Поменять два узла местами\n        self.swap(i, ma);\n        // Циклическое просеивание вниз\n        i = ma;\n    }\n}\n
        my_heap.c
        /* Извлечение элемента из кучи */\nint pop(MaxHeap *maxHeap) {\n    // Обработка пустого случая\n    if (isEmpty(maxHeap)) {\n        printf(\"heap is empty!\");\n        return INT_MAX;\n    }\n    // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n    swap(maxHeap, 0, size(maxHeap) - 1);\n    // Удаление узла\n    int val = maxHeap->data[maxHeap->size - 1];\n    maxHeap->size--;\n    // Просеивание сверху вниз\n    siftDown(maxHeap, 0);\n\n    // Вернуть элемент с вершины кучи\n    return val;\n}\n\n/* Начиная с узла i, выполнить просеивание сверху вниз */\nvoid siftDown(MaxHeap *maxHeap, int i) {\n    while (true) {\n        // Определить узел с максимальным значением среди i, l и r и обозначить его как max\n        int l = left(maxHeap, i);\n        int r = right(maxHeap, i);\n        int max = i;\n        if (l < size(maxHeap) && maxHeap->data[l] > maxHeap->data[max]) {\n            max = l;\n        }\n        if (r < size(maxHeap) && maxHeap->data[r] > maxHeap->data[max]) {\n            max = r;\n        }\n        // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if (max == i) {\n            break;\n        }\n        // Поменять два узла местами\n        swap(maxHeap, i, max);\n        // Циклическое просеивание вниз\n        i = max;\n    }\n}\n
        my_heap.kt
        /* Извлечение элемента из кучи */\nfun pop(): Int {\n    // Обработка пустого случая\n    if (isEmpty()) throw IndexOutOfBoundsException()\n    // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n    swap(0, size() - 1)\n    // Удаление узла\n    val _val = maxHeap.removeAt(size() - 1)\n    // Просеивание сверху вниз\n    siftDown(0)\n    // Вернуть элемент с вершины кучи\n    return _val\n}\n\n/* Начиная с узла i, выполнить просеивание сверху вниз */\nfun siftDown(it: Int) {\n    // Параметры функций в Kotlin неизменяемы, поэтому создается временная переменная\n    var i = it\n    while (true) {\n        // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n        val l = left(i)\n        val r = right(i)\n        var ma = i\n        if (l < size() && maxHeap[l] > maxHeap[ma]) ma = l\n        if (r < size() && maxHeap[r] > maxHeap[ma]) ma = r\n        // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if (ma == i) break\n        // Поменять два узла местами\n        swap(i, ma)\n        // Циклическое просеивание вниз\n        i = ma\n    }\n}\n
        my_heap.rb
        ### Извлечение элемента из кучи ###\ndef pop\n  # Обработка пустого случая\n  raise IndexError, \"куча пуста\" if is_empty?\n  # Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n  swap(0, size - 1)\n  # Удаление узла\n  val = @max_heap.pop\n  # Просеивание сверху вниз\n  sift_down(0)\n  # Вернуть элемент с вершины кучи\n  val\nend\n\n### Начиная с узла i, выполнить просеивание сверху вниз ###\ndef sift_down(i)\n  loop do\n    # Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n    l, r, ma = left(i), right(i), i\n    ma = l if l < size && @max_heap[l] > @max_heap[ma]\n    ma = r if r < size && @max_heap[r] > @max_heap[ma]\n\n    # Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n    break if ma == i\n\n    # Поменять два узла местами\n    swap(i, ma)\n    # Циклическое просеивание вниз\n    i = ma\n  end\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 8. Куча","8.1   Куча"],"tags":[]},{"location":"chapter_heap/heap/#813","level":2,"title":"8.1.3   Типичные применения кучи","text":"
        • Очередь с приоритетом: куча обычно является предпочтительной структурой данных для реализации очереди с приоритетом. Добавление и извлечение элементов имеют временную сложность \\(O(\\log n)\\) , а построение кучи - \\(O(n)\\) , и все эти операции выполняются очень эффективно.
        • Пирамидальная сортировка: для заданного набора данных можно построить кучу, а затем непрерывно извлекать из нее элементы, получая отсортированные данные. Однако на практике мы обычно используем более изящную реализацию пирамидальной сортировки. Подробности см. в разделе «Пирамидальная сортировка».
        • Получение наибольших \\(k\\) элементов: это классическая алгоритмическая задача и одновременно типичное применение кучи. Например, выбор 10 самых горячих новостей для списка популярных тем или выбор 10 самых продаваемых товаров.
        ","path":["Глава 8. Куча","8.1   Куча"],"tags":[]},{"location":"chapter_heap/summary/","level":1,"title":"8.4   Резюме","text":"","path":["Глава 8. Куча","8.4   Резюме"],"tags":[]},{"location":"chapter_heap/summary/#1","level":3,"title":"1.   Ключевые выводы","text":"
        • Куча представляет собой полное двоичное дерево и делится на максимальную кучу и минимальную кучу. Элемент на вершине максимальной (минимальной) кучи является наибольшим (наименьшим).
        • Очередь с приоритетом определяется как очередь, элементы которой извлекаются в соответствии с приоритетом. Обычно ее реализуют с помощью кучи.
        • К основным операциям кучи и их временным сложностям относятся: добавление элемента в кучу \\(O(\\log n)\\) , извлечение элемента с вершины кучи \\(O(\\log n)\\) и доступ к вершине кучи \\(O(1)\\) .
        • Полное двоичное дерево очень удобно представлять массивом, поэтому кучу обычно тоже хранят в массиве.
        • Операция упорядочивания кучи используется для поддержания свойств кучи и применяется как при добавлении элемента, так и при извлечении элемента.
        • Временную сложность построения кучи из \\(n\\) элементов можно оптимизировать до \\(O(n)\\) , что очень эффективно.
        • Top-k - это классическая алгоритмическая задача, которую можно эффективно решать с помощью кучи за \\(O(n \\log k)\\) .
        ","path":["Глава 8. Куча","8.4   Резюме"],"tags":[]},{"location":"chapter_heap/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

        Q: Является ли «куча» как структура данных тем же самым понятием, что и «куча» в управлении памятью?

        Это не одно и то же, просто у них случайно совпало название. Куча в памяти компьютерной системы является частью динамического распределения памяти: во время выполнения программы она используется для хранения данных. Программа может запросить определенный объем памяти в куче для хранения сложных структур, таких как объекты и массивы. Когда эти данные больше не нужны, память нужно освободить, чтобы не допустить утечек. По сравнению со стековой памятью управление памятью в куче требует большей осторожности, а неправильное использование может привести к утечкам памяти и проблемам с указателями.

        ","path":["Глава 8. Куча","8.4   Резюме"],"tags":[]},{"location":"chapter_heap/top_k/","level":1,"title":"8.3   Задача Top-k","text":"

        Question

        Дан неупорядоченный массив nums длины \\(n\\) . Требуется вернуть наибольшие \\(k\\) элементов массива.

        Для этой задачи мы сначала покажем два относительно прямолинейных способа решения, а затем более эффективный способ на основе кучи.

        ","path":["Глава 8. Куча","8.3   Задача Top-k"],"tags":[]},{"location":"chapter_heap/top_k/#831-1","level":2,"title":"8.3.1   Метод 1: выбор через обход","text":"

        Как показано на рисунке 8-6, можно выполнить \\(k\\) проходов по массиву и на каждом проходе извлекать соответственно \\(1\\)-й, \\(2\\)-й, \\(\\dots\\) , \\(k\\)-й по величине элемент. Временная сложность такого подхода равна \\(O(nk)\\) .

        Этот метод подходит только для случая \\(k \\ll n\\) , потому что когда \\(k\\) приближается к \\(n\\) , его временная сложность стремится к \\(O(n^2)\\) , а это уже очень затратно.

        Рисунок 8-6   Поиск наибольших k элементов через обход

        Tip

        Когда \\(k = n\\) , мы получаем полную упорядоченную последовательность, и в этот момент задача становится эквивалентной алгоритму «сортировка выбором».

        ","path":["Глава 8. Куча","8.3   Задача Top-k"],"tags":[]},{"location":"chapter_heap/top_k/#832-2","level":2,"title":"8.3.2   Метод 2: сортировка","text":"

        Как показано на рисунке 8-7, можно сначала отсортировать массив nums , а затем вернуть его крайние правые \\(k\\) элементов. Временная сложность такого метода равна \\(O(n \\log n)\\) .

        Очевидно, что этот способ делает слишком много, потому что нам нужно только найти наибольшие \\(k\\) элементов, а сортировать остальные элементы совсем не обязательно.

        Рисунок 8-7   Поиск наибольших k элементов через сортировку

        ","path":["Глава 8. Куча","8.3   Задача Top-k"],"tags":[]},{"location":"chapter_heap/top_k/#833-3","level":2,"title":"8.3.3   Метод 3: куча","text":"

        Задачу Top-k можно решить гораздо эффективнее с помощью кучи, как показано на рисунке 8-8.

        1. Инициализировать минимальную кучу, у которой вершина содержит наименьший элемент.
        2. Сначала по очереди поместить в кучу первые \\(k\\) элементов массива.
        3. Начиная с элемента номер \\(k + 1\\) , если текущий элемент больше элемента на вершине кучи, то извлечь вершину кучи и поместить в кучу текущий элемент.
        4. После завершения обхода в куче будут храниться как раз наибольшие \\(k\\) элементов.
        <1><2><3><4><5><6><7><8><9>

        Рисунок 8-8   Поиск наибольших k элементов с помощью кучи

        Пример кода приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby top_k.py
        def top_k_heap(nums: list[int], k: int) -> list[int]:\n    \"\"\"Найти k наибольших элементов массива с помощью кучи\"\"\"\n    # Инициализация минимальной кучи\n    heap = []\n    # Поместить первые k элементов массива в кучу\n    for i in range(k):\n        heapq.heappush(heap, nums[i])\n    # Начиная с элемента k+1, поддерживать длину кучи равной k\n    for i in range(k, len(nums)):\n        # Если текущий элемент больше элемента на вершине кучи, извлечь вершину кучи и добавить текущий элемент в кучу\n        if nums[i] > heap[0]:\n            heapq.heappop(heap)\n            heapq.heappush(heap, nums[i])\n    return heap\n
        top_k.cpp
        /* Найти k наибольших элементов массива с помощью кучи */\npriority_queue<int, vector<int>, greater<int>> topKHeap(vector<int> &nums, int k) {\n    // Инициализация минимальной кучи\n    priority_queue<int, vector<int>, greater<int>> heap;\n    // Поместить первые k элементов массива в кучу\n    for (int i = 0; i < k; i++) {\n        heap.push(nums[i]);\n    }\n    // Начиная с элемента k+1, поддерживать длину кучи равной k\n    for (int i = k; i < nums.size(); i++) {\n        // Если текущий элемент больше элемента на вершине кучи, извлечь вершину кучи и добавить текущий элемент в кучу\n        if (nums[i] > heap.top()) {\n            heap.pop();\n            heap.push(nums[i]);\n        }\n    }\n    return heap;\n}\n
        top_k.java
        /* Найти k наибольших элементов массива с помощью кучи */\nQueue<Integer> topKHeap(int[] nums, int k) {\n    // Инициализация минимальной кучи\n    Queue<Integer> heap = new PriorityQueue<Integer>();\n    // Поместить первые k элементов массива в кучу\n    for (int i = 0; i < k; i++) {\n        heap.offer(nums[i]);\n    }\n    // Начиная с элемента k+1, поддерживать длину кучи равной k\n    for (int i = k; i < nums.length; i++) {\n        // Если текущий элемент больше элемента на вершине кучи, извлечь вершину кучи и добавить текущий элемент в кучу\n        if (nums[i] > heap.peek()) {\n            heap.poll();\n            heap.offer(nums[i]);\n        }\n    }\n    return heap;\n}\n
        top_k.cs
        /* Найти k наибольших элементов массива с помощью кучи */\nPriorityQueue<int, int> TopKHeap(int[] nums, int k) {\n    // Инициализация минимальной кучи\n    PriorityQueue<int, int> heap = new();\n    // Поместить первые k элементов массива в кучу\n    for (int i = 0; i < k; i++) {\n        heap.Enqueue(nums[i], nums[i]);\n    }\n    // Начиная с элемента k+1, поддерживать длину кучи равной k\n    for (int i = k; i < nums.Length; i++) {\n        // Если текущий элемент больше элемента на вершине кучи, извлечь вершину кучи и добавить текущий элемент в кучу\n        if (nums[i] > heap.Peek()) {\n            heap.Dequeue();\n            heap.Enqueue(nums[i], nums[i]);\n        }\n    }\n    return heap;\n}\n
        top_k.go
        /* Найти k наибольших элементов массива с помощью кучи */\nfunc topKHeap(nums []int, k int) *minHeap {\n    // Инициализация минимальной кучи\n    h := &minHeap{}\n    heap.Init(h)\n    // Поместить первые k элементов массива в кучу\n    for i := 0; i < k; i++ {\n        heap.Push(h, nums[i])\n    }\n    // Начиная с элемента k+1, поддерживать длину кучи равной k\n    for i := k; i < len(nums); i++ {\n        // Если текущий элемент больше элемента на вершине кучи, извлечь вершину кучи и добавить текущий элемент в кучу\n        if nums[i] > h.Top().(int) {\n            heap.Pop(h)\n            heap.Push(h, nums[i])\n        }\n    }\n    return h\n}\n
        top_k.swift
        /* Найти k наибольших элементов массива с помощью кучи */\nfunc topKHeap(nums: [Int], k: Int) -> [Int] {\n    // Инициализировать минимальную кучу и построить ее по первым k элементам\n    var heap = Heap(nums.prefix(k))\n    // Начиная с элемента k+1, поддерживать длину кучи равной k\n    for i in nums.indices.dropFirst(k) {\n        // Если текущий элемент больше элемента на вершине кучи, извлечь вершину кучи и добавить текущий элемент в кучу\n        if nums[i] > heap.min()! {\n            _ = heap.removeMin()\n            heap.insert(nums[i])\n        }\n    }\n    return heap.unordered\n}\n
        top_k.js
        /* Добавление элемента в кучу */\nfunction pushMinHeap(maxHeap, val) {\n    // Инвертировать знак элемента\n    maxHeap.push(-val);\n}\n\n/* Извлечение элемента из кучи */\nfunction popMinHeap(maxHeap) {\n    // Инвертировать знак элемента\n    return -maxHeap.pop();\n}\n\n/* Доступ к элементу на вершине кучи */\nfunction peekMinHeap(maxHeap) {\n    // Инвертировать знак элемента\n    return -maxHeap.peek();\n}\n\n/* Извлечь элементы из кучи */\nfunction getMinHeap(maxHeap) {\n    // Инвертировать знак элемента\n    return maxHeap.getMaxHeap().map((num) => -num);\n}\n\n/* Найти k наибольших элементов массива с помощью кучи */\nfunction topKHeap(nums, k) {\n    // Инициализация минимальной кучи\n    // Обратите внимание: мы инвертируем все элементы кучи, чтобы с помощью максимальной кучи имитировать минимальную\n    const maxHeap = new MaxHeap([]);\n    // Поместить первые k элементов массива в кучу\n    for (let i = 0; i < k; i++) {\n        pushMinHeap(maxHeap, nums[i]);\n    }\n    // Начиная с элемента k+1, поддерживать длину кучи равной k\n    for (let i = k; i < nums.length; i++) {\n        // Если текущий элемент больше элемента на вершине кучи, извлечь вершину кучи и добавить текущий элемент в кучу\n        if (nums[i] > peekMinHeap(maxHeap)) {\n            popMinHeap(maxHeap);\n            pushMinHeap(maxHeap, nums[i]);\n        }\n    }\n    // Вернуть элементы кучи\n    return getMinHeap(maxHeap);\n}\n
        top_k.ts
        /* Добавление элемента в кучу */\nfunction pushMinHeap(maxHeap: MaxHeap, val: number): void {\n    // Инвертировать знак элемента\n    maxHeap.push(-val);\n}\n\n/* Извлечение элемента из кучи */\nfunction popMinHeap(maxHeap: MaxHeap): number {\n    // Инвертировать знак элемента\n    return -maxHeap.pop();\n}\n\n/* Доступ к элементу на вершине кучи */\nfunction peekMinHeap(maxHeap: MaxHeap): number {\n    // Инвертировать знак элемента\n    return -maxHeap.peek();\n}\n\n/* Извлечь элементы из кучи */\nfunction getMinHeap(maxHeap: MaxHeap): number[] {\n    // Инвертировать знак элемента\n    return maxHeap.getMaxHeap().map((num: number) => -num);\n}\n\n/* Найти k наибольших элементов массива с помощью кучи */\nfunction topKHeap(nums: number[], k: number): number[] {\n    // Инициализация минимальной кучи\n    // Обратите внимание: мы инвертируем все элементы кучи, чтобы с помощью максимальной кучи имитировать минимальную\n    const maxHeap = new MaxHeap([]);\n    // Поместить первые k элементов массива в кучу\n    for (let i = 0; i < k; i++) {\n        pushMinHeap(maxHeap, nums[i]);\n    }\n    // Начиная с элемента k+1, поддерживать длину кучи равной k\n    for (let i = k; i < nums.length; i++) {\n        // Если текущий элемент больше элемента на вершине кучи, извлечь вершину кучи и добавить текущий элемент в кучу\n        if (nums[i] > peekMinHeap(maxHeap)) {\n            popMinHeap(maxHeap);\n            pushMinHeap(maxHeap, nums[i]);\n        }\n    }\n    // Вернуть элементы кучи\n    return getMinHeap(maxHeap);\n}\n
        top_k.dart
        /* Найти k наибольших элементов массива с помощью кучи */\nMinHeap topKHeap(List<int> nums, int k) {\n  // Инициализировать минимальную кучу, поместив в нее первые k элементов массива\n  MinHeap heap = MinHeap(nums.sublist(0, k));\n  // Начиная с элемента k+1, поддерживать длину кучи равной k\n  for (int i = k; i < nums.length; i++) {\n    // Если текущий элемент больше элемента на вершине кучи, извлечь вершину кучи и добавить текущий элемент в кучу\n    if (nums[i] > heap.peek()) {\n      heap.pop();\n      heap.push(nums[i]);\n    }\n  }\n  return heap;\n}\n
        top_k.rs
        /* Найти k наибольших элементов массива с помощью кучи */\nfn top_k_heap(nums: Vec<i32>, k: usize) -> BinaryHeap<Reverse<i32>> {\n    // BinaryHeap — это максимальная куча; с помощью Reverse элементы инвертируются, чтобы реализовать минимальную кучу\n    let mut heap = BinaryHeap::<Reverse<i32>>::new();\n    // Поместить первые k элементов массива в кучу\n    for &num in nums.iter().take(k) {\n        heap.push(Reverse(num));\n    }\n    // Начиная с элемента k+1, поддерживать длину кучи равной k\n    for &num in nums.iter().skip(k) {\n        // Если текущий элемент больше элемента на вершине кучи, извлечь вершину кучи и добавить текущий элемент в кучу\n        if num > heap.peek().unwrap().0 {\n            heap.pop();\n            heap.push(Reverse(num));\n        }\n    }\n    heap\n}\n
        top_k.c
        /* Добавление элемента в кучу */\nvoid pushMinHeap(MaxHeap *maxHeap, int val) {\n    // Инвертировать знак элемента\n    push(maxHeap, -val);\n}\n\n/* Извлечение элемента из кучи */\nint popMinHeap(MaxHeap *maxHeap) {\n    // Инвертировать знак элемента\n    return -pop(maxHeap);\n}\n\n/* Доступ к элементу на вершине кучи */\nint peekMinHeap(MaxHeap *maxHeap) {\n    // Инвертировать знак элемента\n    return -peek(maxHeap);\n}\n\n/* Извлечь элементы из кучи */\nint *getMinHeap(MaxHeap *maxHeap) {\n    // Инвертировать все элементы кучи и записать их в массив res\n    int *res = (int *)malloc(maxHeap->size * sizeof(int));\n    for (int i = 0; i < maxHeap->size; i++) {\n        res[i] = -maxHeap->data[i];\n    }\n    return res;\n}\n\n/* Извлечь элементы из кучи */\nint *getMinHeap(MaxHeap *maxHeap) {\n    // Инвертировать все элементы кучи и записать их в массив res\n    int *res = (int *)malloc(maxHeap->size * sizeof(int));\n    for (int i = 0; i < maxHeap->size; i++) {\n        res[i] = -maxHeap->data[i];\n    }\n    return res;\n}\n\n// Функция поиска k наибольших элементов массива на основе кучи\nint *topKHeap(int *nums, int sizeNums, int k) {\n    // Инициализация минимальной кучи\n    // Обратите внимание: мы инвертируем все элементы кучи, чтобы с помощью максимальной кучи имитировать минимальную\n    int *empty = (int *)malloc(0);\n    MaxHeap *maxHeap = newMaxHeap(empty, 0);\n    // Поместить первые k элементов массива в кучу\n    for (int i = 0; i < k; i++) {\n        pushMinHeap(maxHeap, nums[i]);\n    }\n    // Начиная с элемента k+1, поддерживать длину кучи равной k\n    for (int i = k; i < sizeNums; i++) {\n        // Если текущий элемент больше элемента на вершине кучи, извлечь вершину кучи и добавить текущий элемент в кучу\n        if (nums[i] > peekMinHeap(maxHeap)) {\n            popMinHeap(maxHeap);\n            pushMinHeap(maxHeap, nums[i]);\n        }\n    }\n    int *res = getMinHeap(maxHeap);\n    // Освободить память\n    delMaxHeap(maxHeap);\n    return res;\n}\n
        top_k.kt
        /* Найти k наибольших элементов массива с помощью кучи */\nfun topKHeap(nums: IntArray, k: Int): Queue<Int> {\n    // Инициализация минимальной кучи\n    val heap = PriorityQueue<Int>()\n    // Поместить первые k элементов массива в кучу\n    for (i in 0..<k) {\n        heap.offer(nums[i])\n    }\n    // Начиная с элемента k+1, поддерживать длину кучи равной k\n    for (i in k..<nums.size) {\n        // Если текущий элемент больше элемента на вершине кучи, извлечь вершину кучи и добавить текущий элемент в кучу\n        if (nums[i] > heap.peek()) {\n            heap.poll()\n            heap.offer(nums[i])\n        }\n    }\n    return heap\n}\n
        top_k.rb
        ### Поиск k наибольших элементов массива с помощью кучи ###\ndef top_k_heap(nums, k)\n  # Инициализация минимальной кучи\n  # Обратите внимание: мы инвертируем все элементы кучи, чтобы с помощью максимальной кучи имитировать минимальную\n  max_heap = MaxHeap.new([])\n\n  # Поместить первые k элементов массива в кучу\n  for i in 0...k\n    push_min_heap(max_heap, nums[i])\n  end\n\n  # Начиная с элемента k+1, поддерживать длину кучи равной k\n  for i in k...nums.length\n    # Если текущий элемент больше элемента на вершине кучи, извлечь вершину кучи и добавить текущий элемент в кучу\n    if nums[i] > peek_min_heap(max_heap)\n      pop_min_heap(max_heap)\n      push_min_heap(max_heap, nums[i])\n    end\n  end\n\n  get_min_heap(max_heap)\nend\n
        Визуализация кода

        Во весь экран >

        Всего выполняется \\(n\\) операций добавления и извлечения из кучи, а максимальная длина кучи равна \\(k\\) , поэтому временная сложность равна \\(O(n \\log k)\\) . Этот метод очень эффективен: когда \\(k\\) мало, временная сложность стремится к \\(O(n)\\). Когда \\(k\\) велико, она все равно не превышает \\(O(n \\log n)\\) .

        Кроме того, этот метод подходит и для сценариев с динамическим потоком данных. При непрерывном поступлении новых данных мы можем продолжать поддерживать содержимое кучи, тем самым динамически обновляя наибольшие \\(k\\) элементов.

        ","path":["Глава 8. Куча","8.3   Задача Top-k"],"tags":[]},{"location":"chapter_hello_algo/","level":1,"title":"Перед началом","text":"

        Несколько лет назад я публиковал на LeetCode разборы серии задач «Sword for Offer» и получил поддержку и ободрение от многих читателей. Во время общения с ними мне чаще всего задавали один и тот же вопрос: «как начать изучать алгоритмы?» Постепенно этот вопрос начал меня по-настоящему занимать.

        Слепо бросаться в решение задач кажется самым популярным способом: он прост, прямолинеен и действительно работает. Но решение задач похоже на игру в «Сапера»: люди с сильными навыками самообучения способны обезвредить мины одну за другой, а тем, у кого не хватает базы, легко набить себе шишки и шаг за шагом отступить под давлением неудач. Полностью проходить учебники тоже принято часто, но для тех, кто готовится к поиску работы, диплом, резюме, письменные тесты и собеседования уже отнимают большую часть сил, и потому толстые книги нередко превращаются в тяжелое испытание.

        Если ты тоже сталкиваешься с такими трудностями, то можно сказать, что эта книга сама «нашла» тебя. Она стала моим ответом на этот вопрос: пусть и не идеальным, но как минимум честной и активной попыткой. Эта книга сама по себе не гарантирует предложения о работе, но поможет тебе увидеть «карту знаний» по структурам данных и алгоритмам, понять форму, размер и расположение разных «мин» и освоить разные «способы разминирования». Освоив это, ты сможешь увереннее решать задачи и читать технические материалы, шаг за шагом выстраивая целостную систему знаний.

        Я глубоко согласен со словами профессора Фейнмана: «Knowledge isn't free. You have to pay attention.» В этом смысле книга не совсем «бесплатна». Чтобы не подвести то драгоценное «внимание», которое ты ей уделишь, я постараюсь вложить в ее создание максимум собственного «внимания».

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

        Hello, алгоритмы!

        Появление компьютеров радикально изменило мир. Благодаря высокой скорости вычислений и отличной программируемости они стали идеальной средой для исполнения алгоритмов и обработки данных. Реалистичная графика в играх, интеллектуальные решения в автономном вождении, впечатляющие партии AlphaGo и естественное взаимодействие ChatGPT: все это изящные проявления алгоритмов на компьютере.

        На самом деле еще до появления компьютеров алгоритмы и структуры данных уже существовали во всех уголках мира. Ранние алгоритмы были сравнительно простыми: например, древние способы счета или последовательности действий при изготовлении инструментов. По мере развития цивилизации алгоритмы становились тоньше и сложнее. За мастерством ремесленников, промышленными продуктами, освобождающими производительные силы, и даже за научными законами движения Вселенной почти всегда стоит изобретательная алгоритмическая мысль.

        Точно так же структуры данных встречаются повсюду: от социальных сетей до схем метро многие системы можно моделировать как «граф». От государства до семьи основные формы общественной организации обладают свойствами «дерева». Зимняя одежда похожа на «стек», где то, что надевают первым, снимают последним. Тубус для бадминтонных воланов похож на «очередь», где элементы добавляются с одного конца и извлекаются с другого. Словарь похож на «хеш-таблицу», позволяющую быстро находить нужную статью.

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

        ","path":["Перед началом"],"tags":[]},{"location":"chapter_introduction/","level":1,"title":"Глава 1.   Введение в алгоритмы","text":"

        Abstract

        Юная девушка кружится в танце, переплетаясь с данными, а по подолу ее платья струится мелодия алгоритмов.

        Она приглашает вас присоединиться к танцу: следуйте за ее шагами и войдите в мир алгоритмов, полный логики и красоты.

        ","path":["Глава 1. Введение в алгоритмы","Глава 1.   Введение в алгоритмы"],"tags":[]},{"location":"chapter_introduction/#_1","level":2,"title":"Содержание главы","text":"
        • 1.1   Алгоритмы повсюду
        • 1.2   Что такое алгоритм
        • 1.3   Резюме
        ","path":["Глава 1. Введение в алгоритмы","Глава 1.   Введение в алгоритмы"],"tags":[]},{"location":"chapter_introduction/algorithms_are_everywhere/","level":1,"title":"1.1   Алгоритмы повсюду","text":"

        Говоря об алгоритмах, естественно вспомнить о математике. Однако на самом деле многие алгоритмы не связаны со сложной математикой, а больше полагаются на базовую логику, которая повсеместно встречается в нашей повседневной жизни.

        Прежде чем углубиться в обсуждение алгоритмов, стоит упомянуть интересный факт: вы уже точно освоили множество алгоритмов и привыкли применять их в повседневной жизни. Далее приведем несколько конкретных примеров, чтобы подтвердить этот факт.

        Пример 1: поиск в словаре. В словаре все слова упорядочены по алфавиту. Предположим, нам нужно найти слово, начинающееся на букву \\(r\\). Обычно это делают так, как показано на рисунке 1-1.

        1. Откройте словарь примерно на половине страниц и посмотрите, какая буква является первой на этой странице. Предположим, это буква \\(m\\).
        2. Поскольку в алфавите буква \\(r\\) идет после \\(m\\), исключаем первую половину словаря, и область поиска сужается до второй половины.
        3. Продолжайте повторять шаги 1. и 2. , пока не найдете страницу, где первой буквой слов будет \\(r\\).
        <1><2><3><4><5>

        Рисунок 1-1   Этапы поиска в словаре

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

        Пример 2: упорядочивание карт. Во время игры в карты необходимо каждый раз упорядочивать карты в руке от меньшего к большему. Обычно это делают так, как показано на рисунке 1-2.

        1. Разделите карты на упорядоченную и неупорядоченную части, предполагая, что изначально самая левая карта уже упорядочена.
        2. Из неупорядоченной части извлеките одну карту и вставьте ее в правильное место в упорядоченной части. После этого две самые левые карты станут упорядоченными.
        3. Повторяйте шаг 2. , каждый раз перемещая одну карту из неупорядоченной части в упорядоченную, пока все карты не станут упорядоченными.

        Рисунок 1-2   Этапы упорядочивания карт

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

        Пример 3: сдача. Предположим, что в супермаркете мы купили товар стоимостью \\(69\\) руб. и дали кассиру купюру в \\(100\\) руб. Кассир должен вернуть нам \\(31\\) руб. Обычно он рассуждает так, как показано на рисунке 1-3.

        1. Варианты выбора - это купюры номиналом меньше \\(31\\) руб. Пусть у нас имеются номиналы \\(1\\) , \\(5\\) , \\(10\\) и \\(20\\) руб.
        2. Возьмем самую крупную доступную купюру в \\(20\\) руб. Остаток сдачи составит \\(31 - 20 = 11\\) руб.
        3. Возьмем самую крупную из оставшихся купюр в \\(10\\) руб. Остаток составит \\(11 - 10 = 1\\) руб.
        4. Возьмем самую крупную из оставшихся купюр в \\(1\\) руб. Остаток составит \\(1 - 1 = 0\\) руб.
        5. Завершим выдачу сдачи, схема: \\(20 + 10 + 1 = 31\\) руб.

        Рисунок 1-3   Этапы выдачи сдачи

        В этих шагах мы на каждом этапе выбираем наилучший вариант, используя купюры наибольшего номинала, и в итоге получаем рабочую схему сдачи. С точки зрения структуры данных и алгоритмов этот метод по своей сути является жадным алгоритмом.

        От приготовления блюда до межзвездных путешествий решение практически любой задачи неразрывно связано с алгоритмами. Появление компьютеров позволило нам с помощью программирования хранить структуры данных в памяти, а также писать код для вызовов к CPU и GPU для выполнения алгоритмов. Таким образом, мы можем переносить задачи из реальной жизни в компьютер и решать различные сложные проблемы более эффективно.

        Tip

        Если представление о структурах данных, алгоритмах, массивах и двоичном поиске пока остается расплывчатым, просто продолжайте читать. Эта книга постепенно введет вас в мир структур данных и алгоритмов.

        ","path":["Глава 1. Введение в алгоритмы","1.1   Алгоритмы повсюду"],"tags":[]},{"location":"chapter_introduction/summary/","level":1,"title":"1.3   Резюме","text":"","path":["Глава 1. Введение в алгоритмы","1.3   Резюме"],"tags":[]},{"location":"chapter_introduction/summary/#1","level":3,"title":"1.   Ключевые выводы","text":"
        • Алгоритмы повсеместно присутствуют в нашей повседневной жизни и не являются недосягаемыми сложными знаниями. На самом деле мы уже освоили множество алгоритмов, которые помогают решать различные жизненные задачи.
        • Принцип поиска в словаре соответствует алгоритму двоичного поиска. Двоичный поиск иллюстрирует важную идею алгоритмов «разделяй и властвуй».
        • Процесс сортировки карт в колоде очень похож на алгоритм сортировки вставками, который хорошо подходит для сортировки небольших наборов данных.
        • Процесс размена по своей сути является жадным алгоритмом, в котором на каждом этапе принимается наилучшее на данный момент решение.
        • Алгоритм представляет собой набор инструкций или шагов, предназначенных для решения конкретной задачи в ограниченное время, а структура данных - это способ организации и хранения данных в компьютере.
        • Структуры данных и алгоритмы тесно связаны. Структуры данных являются основой для алгоритмов, а алгоритмы оживляют структуры данных.
        • Структуры данных и алгоритмы можно сравнить с конструктором: детали конструктора представляют данные, их форма и способы соединения - структуры данных, а этапы сборки конструктора соответствуют алгоритмам.
        ","path":["Глава 1. Введение в алгоритмы","1.3   Резюме"],"tags":[]},{"location":"chapter_introduction/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

        Q: Я программист и в повседневной работе никогда не использовал алгоритмы для решения задач, поскольку часто используемые алгоритмы уже встроены в языки программирования и ими можно пользоваться напрямую. Значит ли это, что рабочие задачи еще не требуют применения алгоритмов?

        Если сравнить конкретные профессиональные навыки с приемами в боевых искусствах, то базовые дисциплины скорее напоминают «внутреннюю силу».

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

        • Если бы мы не изучали структуры данных и алгоритмы, то, получив любые данные, возможно, просто передали бы их этой функции сортировки. Все работает гладко, производительность хорошая, и на первый взгляд проблем нет.
        • Однако если мы изучили алгоритмы, то знаем, что временная сложность встроенной функции сортировки составляет \\(O(n \\log n)\\). Если же данные представлены целыми числами фиксированной разрядности, например номерами студентов, то можно использовать более эффективный метод поразрядной сортировки, снизив временную сложность до \\(O(nk)\\) , где \\(k\\) - это количество разрядов, а при больших объемах данных выиграть во времени, затратах и пользовательском опыте.

        В инженерной практике множество задач трудно решить оптимальным образом, и многие из них решаются «как-то». Сложность задачи зависит как от ее природы, так и от уровня знаний и опыта человека, который ее анализирует. Чем более полными знаниями и большим опытом обладает человек, тем глубже он может проанализировать проблему и тем изящнее может быть ее решение.

        ","path":["Глава 1. Введение в алгоритмы","1.3   Резюме"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/","level":1,"title":"1.2   Что такое алгоритм","text":"","path":["Глава 1. Введение в алгоритмы","1.2   Что такое алгоритм"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/#121","level":2,"title":"1.2.1   Определение алгоритма","text":"

        Алгоритм (algorithm) - это набор инструкций или шагов, предназначенных для решения конкретной задачи за ограниченное время. Он обладает следующими свойствами.

        • Задача четко определена и включает ясные определения входных и выходных данных.
        • Обладает осуществимостью и может быть выполнен за ограниченное количество шагов, времени и памяти.
        • Каждый шаг имеет определенное значение, и при одинаковых входных данных и условиях выполнения результат всегда будет одинаковым.
        ","path":["Глава 1. Введение в алгоритмы","1.2   Что такое алгоритм"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/#122","level":2,"title":"1.2.2   Определение структуры данных","text":"

        Структура данных (data structure) - это способ организации и хранения данных, включающий содержимое данных, их взаимосвязи и методы операций с ними. Структура данных преследует следующие цели.

        • Минимизировать занимаемое пространство для экономии памяти компьютера.
        • Обеспечивать максимально быструю обработку данных, включая доступ, добавление, удаление и обновление данных.
        • Обеспечивать простое представление данных и логическую информацию для эффективного выполнения алгоритмов.

        Проектирование структуры данных - это процесс, полный компромиссов. Если вы хотите улучшить один аспект, часто приходится идти на уступки в другом. Приведем два примера.

        • Связный список, по сравнению с массивом, более удобен для добавления и удаления данных, но имеет проблемы со скоростью доступа к данным.
        • Граф, по сравнению со связным списком, предоставляет более богатую логическую информацию, но требует большего объема памяти.
        ","path":["Глава 1. Введение в алгоритмы","1.2   Что такое алгоритм"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/#123","level":2,"title":"1.2.3   Связь между структурами данных и алгоритмами","text":"

        Как показано на рисунке 1-4, структуры данных и алгоритмы тесно взаимосвязаны, что проявляется в следующих трех аспектах.

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

        Рисунок 1-4   Связь между структурами данных и алгоритмами

        Структуры данных и алгоритмы подобны конструктору, как показано на рисунке 1-5. Комплект конструктора, помимо множества деталей, содержит также подробную инструкцию по сборке. Следуя этой инструкции шаг за шагом, можно собрать красивую модель.

        Рисунок 1-5   Сборка конструктора

        Подробное описание аналогии с конструктором представлено в таблице 1-1.

        Таблица 1-1   Сравнение структур данных и алгоритмов с конструктором

        Структуры данных и алгоритмы Конструктор Входные данные Несобранные детали конструктора Структура данных Организация деталей конструктора, включая форму, размер, способы соединения и т. д. Алгоритм Последовательность действий по сборке деталей в целевую модель Выходные данные Собранная модель конструктора

        Стоит отметить, что структуры данных и алгоритмы не зависят от языка программирования. Именно поэтому данная книга предлагает их реализации на различных языках.

        Принятое сокращение

        В реальных обсуждениях выражение «структуры данных и алгоритмы» обычно сокращают до просто «алгоритмы». Например, хорошо известные задачи LeetCode на деле одновременно проверяют знания и по структурам данных, и по алгоритмам.

        ","path":["Глава 1. Введение в алгоритмы","1.2   Что такое алгоритм"],"tags":[]},{"location":"chapter_preface/","level":1,"title":"Глава 0.   Предисловие","text":"

        Abstract

        Алгоритмы подобны прекрасной симфонии, а каждая строка кода льется подобно мелодии.

        Пусть эта книга тихо зазвучит в вашем сознании и оставит после себя особую и глубокую мелодию.

        ","path":["Глава 0. Предисловие","Глава 0.   Предисловие"],"tags":[]},{"location":"chapter_preface/#_1","level":2,"title":"Содержание главы","text":"
        • 0.1   Об этой книге
        • 0.2   Как пользоваться этой книгой
        • 0.3   Резюме
        ","path":["Глава 0. Предисловие","Глава 0.   Предисловие"],"tags":[]},{"location":"chapter_preface/about_the_book/","level":1,"title":"0.1   Об этой книге","text":"

        Этот проект задуман как открытое, бесплатное и дружелюбное к новичкам введение в структуры данных и алгоритмы.

        • В книге используются анимированные иллюстрации: материал изложен ясно и последовательно, что облегчает освоение и помогает начинающим выстроить карту знаний по структурам данных и алгоритмам.
        • Исходный код можно запустить одним нажатием, что позволяет тренироваться, развивать навыки программирования и понимать принципы работы алгоритмов и реализации структур данных на фундаментальном уровне.
        • Мы призываем читателей к взаимопомощи: задавайте вопросы и делитесь идеями в комментариях. Обсуждения помогают двигаться вперед всем вместе.
        ","path":["Глава 0. Предисловие","0.1   Об этой книге"],"tags":[]},{"location":"chapter_preface/about_the_book/#011","level":2,"title":"0.1.1   Целевая аудитория","text":"

        Если вы новичок в алгоритмах, никогда с ними не сталкивались или уже имеете некоторый опыт решения задач, но еще не обладаете четким пониманием структур данных и алгоритмов, эта книга создана специально для вас!

        Если у вас уже есть определенный опыт решения задач и вы знакомы с большинством типов задач, эта книга поможет вам освежить и систематизировать знания об алгоритмах, а исходный код может служить набором инструментов для решения задач или алгоритмическим словарем.

        Если вы владеете алгоритмами на экспертном уровне, мы будем рады вашим ценным советам или совместному участию в создании книги.

        Предварительные требования

        Необходимо иметь хотя бы базовую подготовку в одном из языков программирования, чтобы читать и писать простой код.

        ","path":["Глава 0. Предисловие","0.1   Об этой книге"],"tags":[]},{"location":"chapter_preface/about_the_book/#012","level":2,"title":"0.1.2   Структура содержания","text":"

        Основное содержание книги представлено на рисунке 0-1.

        • Анализ сложности: критерии и методы оценки структур данных и алгоритмов. Методы расчета временной и пространственной сложности, распространенные типы, примеры и т. д.
        • Структуры данных: классификация основных типов данных и структур данных. Определение, преимущества и недостатки, основные операции, распространенные типы, типичные приложения и методы реализации массивов, списков, стеков, очередей, хеш-таблиц, деревьев, куч и графов.
        • Алгоритмы: определение, преимущества и недостатки, эффективность, области применения, этапы решения и примеры задач для поиска, сортировки, алгоритма «разделяй и властвуй», поиска с возвратом, динамического программирования и жадных алгоритмов.

        Рисунок 0-1   Основное содержание книги

        ","path":["Глава 0. Предисловие","0.1   Об этой книге"],"tags":[]},{"location":"chapter_preface/about_the_book/#013","level":2,"title":"0.1.3   Благодарности","text":"

        Эта книга постоянно совершенствуется благодаря совместным усилиям множества участников открытого сообщества. Благодарим каждого автора, вложившего свое время и силы. Их имена перечислены в порядке, автоматически сгенерированном GitHub: krahets, coderonion, Gonglja, nuomi1, Reanon, justin-tse, hpstory, danielsss, curtishd, night-cruise, S-N-O-R-L-A-X, rongyi, msk397, gvenusleo, khoaxuantu, rivertwilight, K3v123, gyt95, zhuoqinyue, yuelinxin, Zuoxun, mingXta, Phoenix0415, FangYuan33, GN-Yu, longsizhuo, pengchzn, QiLOL, Cathay-Chen, guowei-gong, xBLACKICEx, IsChristina, JoseHung, qualifier1024, hello-ikun, magentaqin, Guanngxu, thomasq0, sunshinesDL, L-Super, Transmigration-zhou, WSL0809, Slone123c, lhxsm, yuan0221, what-is-me, theNefelibatas, Shyam-Chen, sangxiaai, longranger2, codeberg-user, xiongsp, JeffersonHuang, prinpal, seven1240, Wonderdch, malone6, xiaomiusa87, gaofer, bluebean-cloud, a16su, SamJin98, hongyun-robot, nanlei, XiaChuerwu, yd-j, iron-irax, mgisr, steventimes, junminhong, heshuyue, danny900714, Nigh, Dr-XYZ, MolDuM, XC-Zero, reeswell, PXG-XPG, NI-SW, Horbin-Magician, Enlightenus, YangXuanyi, xjr7670, beatrix-chan, DullSword, qq909244296, iStig, boloboloda, hts0000, gledfish, fbigm, echo1937, jiaxianhua, wenjianmin, keshida, kilikilikid, lclc6, lwbaptx, linyejoe2, liuxjerry, szu17dmy, dshlstarr, Yucao-cy, coderlef, czruby, bongbongbakudan, beintentional, ZongYangL, ZhongYuuu, ZhongGuanbin, hezhizhen, linzeyan, ZJKung, JTCPOWI, KawaiiAsh, luluxia, xb534, ztkuaikuai, yw-1021, ElaBosak233, baagod, zhouLion, yishangzhang, yi427, yanedie, yabo083, weibk, wangwang105, th1nk3r-ing, tao363, 4yDX3906, syd168, sslmj2020, smilelsb, siqyka, selear, sdshaoda, Xi-Row, popozhu, nuquist19, noobcodemaker, XiaoK29, chadyi, lyl625760, lucaswangdev, llql1211, 0130w, shanghai-Jerry, EJackYang, Javesun99, eltociear, lipusheng, KNChiu, BlindTerran, ShiMaRing, lovelock, FreddieLi, FloranceYeh, fanchenggang, gltianwen, goerll, nedchu, curly210102, CuB3y0nd, KraHsu, CarrotDLaw, youshaoXG, bubble9um, Asashishi, Asa0oo0o0o, fanenr, eagleanurag, akshiterate, 52coder, foursevenlove, KorsChen, hopkings2008, yang-le, realwujing, Evilrabbit520, Umer-Jahangir, Turing-1024-Lee, Suremotoo, paoxiaomooo, Chieko-Seren, Senrian, Allen-Scai, 19santosh99, ymmmas, Risuntsy, Richard-Zhang1019, RafaelCaso, qingpeng9802, primexiao, Urbaner3, codetypess, nidhoggfgg, MwumLi, CreatorMetaSky, martinx, ZnYang2018, hugtyftg, logan-qiu, psychelzh, Kunchen-Luo, Keynman и KeiichiKasai.

        Рецензирование кода книги выполнили coderonion, curtishd, Gonglja, gvenusleo, hpstory, justin-tse, khoaxuantu, krahets, night-cruise, nuomi1, Reanon и rongyi (в алфавитном порядке). Благодарим их за потраченное время и силы, которые обеспечили стандартизацию и единообразие кода на различных языках.

        Английскую версию книги вычитали yuelinxin, K3v123, magentaqin, QiLOL, Phoenix0415, SamJin98, yanedie, RafaelCaso, pengchzn и thomasq0. Японскую версию - eltociear. Русскую версию - И. А. Шевкун и Yuyan Huang. Традиционную китайскую версию - Shyam-Chen и Dr-XYZ. Именно благодаря их вкладу эта книга может служить более широкому кругу читателей, и мы искренне благодарим их.

        Инструмент генерации ePub-версии этой книги разработал zhongfq. Благодарим его за вклад, который дал читателям более гибкий способ чтения.

        В процессе создания этой книги мне помогало много людей.

        • Благодарю моего наставника в компании, доктора Ли Си: в одной из бесед вы вдохновили меня быстрее начать, что укрепило мою решимость написать эту книгу. - Благодарю мою девушку Bubble, первого читателя этой книги: с позиции новичка в алгоритмах она дала много ценных советов, благодаря которым книга стала более понятной и доступной. - Благодарю Tengbao, Qibao и Feibao за креативное название книги, которое навевает приятные воспоминания о первой строке кода «Hello World!». - Благодарю Xiaoquan за профессиональную помощь в вопросах интеллектуальной собственности, что сыграло важную роль в совершенствовании этой открытой книги. - Благодарю Sutong за дизайн обложки и логотипа книги, а также за терпение при многочисленных исправлениях по моим просьбам. - Благодарю @squidfunk за советы по оформлению и за разработку открытой темы документации Material-for-MkDocs.

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

        Эта книга пропагандирует метод обучения, сочетающий умственную и практическую деятельность. В этом отношении на меня сильно повлияла Dive into Deep Learning. Я настоятельно рекомендую эту замечательную работу всем читателям.

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

        ","path":["Глава 0. Предисловие","0.1   Об этой книге"],"tags":[]},{"location":"chapter_preface/suggestions/","level":1,"title":"0.2   Как пользоваться этой книгой","text":"

        Tip

        Для получения наилучшего опыта чтения рекомендуется полностью прочитать этот раздел.

        ","path":["Глава 0. Предисловие","0.2   Как пользоваться этой книгой"],"tags":[]},{"location":"chapter_preface/suggestions/#021","level":2,"title":"0.2.1   Соглашения о стиле изложения","text":"
        • Главы, помеченные * в заголовке, являются дополнительными и содержат более сложный материал. Если времени мало, их можно пропустить.
        • Профессиональные термины выделяются полужирным шрифтом в печатной и PDF-версии или подчеркиванием в веб-версии, например массив (array). Рекомендуется запоминать их для удобства чтения литературы.
        • Важные моменты и обобщающие фразы будут выделяться полужирным шрифтом, и на такие тексты следует обращать особое внимание.
        • Слова и выражения со специальным смыслом будут отмечаться «кавычками», чтобы избежать неоднозначности.
        • Когда термины различаются между языками программирования, в качестве стандарта используется Python. Например, None применяется для обозначения «пустого» значения.
        • В некоторых местах книга отходит от стандартов комментирования программного кода ради более компактного оформления. Комментарии в основном делятся на три типа: заголовочные, содержательные и многострочные.
        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
        \"\"\"Комментарий-заголовок: используется для обозначения функций, классов, тестовых примеров и т. п.\"\"\"\n\n# Содержательный комментарий: подробно поясняет код\n\n\"\"\"\nМногострочный\nкомментарий\n\"\"\"\n
        /* Комментарий-заголовок: используется для обозначения функций, классов, тестовых примеров и т. п. */\n\n// Содержательный комментарий: подробно поясняет код\n\n/**\n * Многострочный\n * комментарий\n */\n
        /* Комментарий-заголовок: используется для обозначения функций, классов, тестовых примеров и т. п. */\n\n// Содержательный комментарий: подробно поясняет код\n\n/**\n * Многострочный\n * комментарий\n */\n
        /* Комментарий-заголовок: используется для обозначения функций, классов, тестовых примеров и т. п. */\n\n// Содержательный комментарий: подробно поясняет код\n\n/**\n * Многострочный\n * комментарий\n */\n
        /* Комментарий-заголовок: используется для обозначения функций, классов, тестовых примеров и т. п. */\n\n// Содержательный комментарий: подробно поясняет код\n\n/**\n * Многострочный\n * комментарий\n */\n
        /* Комментарий-заголовок: используется для обозначения функций, классов, тестовых примеров и т. п. */\n\n// Содержательный комментарий: подробно поясняет код\n\n/**\n * Многострочный\n * комментарий\n */\n
        /* Комментарий-заголовок: используется для обозначения функций, классов, тестовых примеров и т. п. */\n\n// Содержательный комментарий: подробно поясняет код\n\n/**\n * Многострочный\n * комментарий\n */\n
        /* Комментарий-заголовок: используется для обозначения функций, классов, тестовых примеров и т. п. */\n\n// Содержательный комментарий: подробно поясняет код\n\n/**\n * Многострочный\n * комментарий\n */\n
        /* Комментарий-заголовок: используется для обозначения функций, классов, тестовых примеров и т. п. */\n\n// Содержательный комментарий: подробно поясняет код\n\n/**\n * Многострочный\n * комментарий\n */\n
        /* Комментарий-заголовок: используется для обозначения функций, классов, тестовых примеров и т. п. */\n\n// Содержательный комментарий: подробно поясняет код\n\n// Многострочный\n// комментарий\n
        /* Комментарий-заголовок: используется для обозначения функций, классов, тестовых примеров и т. п. */\n\n// Содержательный комментарий: подробно поясняет код\n\n/**\n * Многострочный\n * комментарий\n */\n
        /* Комментарий-заголовок: используется для обозначения функций, классов, тестовых примеров и т. п. */\n\n// Содержательный комментарий: подробно поясняет код\n\n/**\n * Многострочный\n * комментарий\n */\n
        ### Комментарий-заголовок: используется для обозначения функций, классов, тестовых примеров и т. п. ###\n\n# Содержательный комментарий: подробно поясняет код\n\n# Многострочный\n# комментарий\n
        ","path":["Глава 0. Предисловие","0.2   Как пользоваться этой книгой"],"tags":[]},{"location":"chapter_preface/suggestions/#022","level":2,"title":"0.2.2   Эффективное обучение с помощью анимированных иллюстраций","text":"

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

        Если во время чтения вы встречаете фрагмент с анимированной иллюстрацией, как на рисунке 0-2, используйте иллюстрацию в качестве основного источника информации, а текст - в качестве вспомогательного, объединяя оба источника для понимания материала.

        Рисунок 0-2   Пример анимированной иллюстрации

        ","path":["Глава 0. Предисловие","0.2   Как пользоваться этой книгой"],"tags":[]},{"location":"chapter_preface/suggestions/#023","level":2,"title":"0.2.3   Углубление понимания через практику кода","text":"

        Сопроводительный код этой книги размещен в репозитории GitHub. Как показано на рисунке 0-3, исходный код содержит тестовые примеры и может быть запущен одним нажатием кнопки.

        Если позволяет время, рекомендуется самостоятельно набирать код. Если времени на обучение мало, по крайней мере просмотрите и выполните весь код.

        Процесс написания кода приносит больше пользы, чем его чтение. Настоящее обучение - это обучение на практике.

        Рисунок 0-3   Пример запуска кода

        Подготовка к запуску кода в основном состоит из трех этапов.

        Шаг 1: установка локальной среды программирования. Воспользуйтесь руководством из приложения. Если среда уже установлена, этот шаг можно пропустить.

        Шаг 2: клонирование или загрузка репозитория кода. Перейдите в репозиторий GitHub. Если у вас уже установлен Git, репозиторий можно клонировать следующей командой:

        git clone https://github.com/krahets/hello-algo.git\n

        Также можно нажать кнопку «Download ZIP» в месте, показанном на рисунке 0-4, напрямую скачать архив с кодом и затем распаковать его локально.

        Рисунок 0-4   Клонирование репозитория и загрузка кода

        Шаг 3: запуск исходного кода. Как показано на рисунке 0-5, для блоков кода, у которых сверху указано имя файла, соответствующий исходный файл можно найти в папке codes репозитория. Исходные файлы запускаются одним нажатием, что помогает не тратить лишнее время на отладку и сосредоточиться на изучении материала.

        Рисунок 0-5   Блоки кода и соответствующие исходные файлы

        Помимо локального запуска, веб-версия также поддерживает визуальное выполнение Python-кода (на базе pythontutor). Как показано на рисунке 0-6, можно нажать «Визуализировать выполнение» под блоком кода, чтобы раскрыть окно и наблюдать за выполнением алгоритма. Также можно нажать «Полноэкранный режим» для более удобного просмотра.

        Рисунок 0-6   Визуальный запуск Python-кода

        ","path":["Глава 0. Предисловие","0.2   Как пользоваться этой книгой"],"tags":[]},{"location":"chapter_preface/suggestions/#024","level":2,"title":"0.2.4   Совместный рост через вопросы и обсуждения","text":"

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

        Как показано на рисунке 0-7, в веб-версии у каждой главы внизу есть раздел комментариев. Рекомендуется уделять внимание его содержанию. С одной стороны, это поможет увидеть, с какими трудностями сталкиваются другие читатели, восполнить пробелы и подтолкнуть себя к более глубокому пониманию. С другой стороны, мы надеемся, что вы будете отвечать на вопросы других участников и делиться своими мнениями.

        Рисунок 0-7   Пример раздела комментариев

        ","path":["Глава 0. Предисловие","0.2   Как пользоваться этой книгой"],"tags":[]},{"location":"chapter_preface/suggestions/#025","level":2,"title":"0.2.5   Дорожная карта изучения алгоритмов","text":"

        В целом процесс изучения структур данных и алгоритмов можно разделить на три этапа.

        1. Этап 1: введение в алгоритмы. Необходимо познакомиться с особенностями и применением различных структур данных, изучить принципы, процессы, назначение и эффективность различных алгоритмов.
        2. Этап 2: решение алгоритмических задач. Рекомендуется начинать с популярных задач и решить не менее 100 из них, чтобы познакомиться с основными алгоритмическими проблемами. При первых попытках «забывание знаний» может стать испытанием, но это нормально. Следуйте при повторении задач «кривой забывания Эббингауза», и обычно после 3-5 циклов повторения материал хорошо запоминается. Рекомендуемые списки задач и планы практики см. в этом репозитории GitHub.
        3. Этап 3: построение системы знаний. В процессе обучения можно читать статьи по алгоритмам, изучать каркасы решений и учебники, чтобы постоянно обогащать свою систему знаний. В решении задач можно применять продвинутые стратегии, например классификацию по темам, несколько решений одной задачи или одно решение для нескольких задач. Соответствующий опыт можно найти в различных сообществах.

        Как показано на рисунке 0-8, содержание этой книги в основном охватывает «этап 1» и призвано помочь вам более эффективно перейти к обучению на этапах 2 и 3.

        Рисунок 0-8   Дорожная карта изучения алгоритмов

        ","path":["Глава 0. Предисловие","0.2   Как пользоваться этой книгой"],"tags":[]},{"location":"chapter_preface/summary/","level":1,"title":"0.3   Резюме","text":"","path":["Глава 0. Предисловие","0.3   Резюме"],"tags":[]},{"location":"chapter_preface/summary/#1","level":3,"title":"1.   Ключевые выводы","text":"
        • Основная аудитория этой книги - новички в изучении алгоритмов. Если у вас уже есть определенная база, книга поможет систематизировать знания, а исходный код послужит инструментальной библиотекой для решения задач.
        • Содержание книги включает три основные части - анализ сложности, структуры данных и алгоритмы - и охватывает большинство тем в этой области.
        • Для новичков в алгоритмах крайне важно изучить начальные разделы книги, чтобы избежать множества ошибок в будущем.
        • Анимированные иллюстрации в книге обычно используются для представления ключевых и сложных аспектов. При чтении книги следует уделять этим материалам больше внимания.
        • Практика - лучший способ изучения программирования. Настоятельно рекомендуется запускать исходный код и самостоятельно писать программы.
        • В веб-версии книги каждая глава имеет область комментариев, где можно задавать вопросы и делиться своими мыслями.
        ","path":["Глава 0. Предисловие","0.3   Резюме"],"tags":[]},{"location":"chapter_reference/","level":1,"title":"Список литературы","text":"

        [1] Thomas H. Cormen и др. Introduction to Algorithms (3rd Edition).

        [2] Aditya Bhargava. Grokking Algorithms: An Illustrated Guide for Programmers and Other Curious People (1st Edition).

        [3] Robert Sedgewick и др. Algorithms (4th Edition).

        [4] Yan Weimin. Data Structures (C Language Edition).

        [5] Deng Junhui. Data Structures (C++ Language Edition, 3rd Edition).

        [6] Mark Allen Weiss. Пер. Chen Yue. Data Structures and Algorithm Analysis: Java Description (3rd Edition).

        [7] Cheng Jie. A Plainspoken Guide to Data Structures.

        [8] Wang Zheng. The Beauty of Data Structures and Algorithms.

        [9] Gayle Laakmann McDowell. Cracking the Coding Interview: 189 Programming Questions and Solutions (6th Edition).

        [10] Aston Zhang и др. Dive into Deep Learning.

        ","path":["Список литературы"],"tags":[]},{"location":"chapter_searching/","level":1,"title":"Глава 10.   Поиск","text":"

        Abstract

        Поиск - это движение в неизвестность: иногда приходится пройти каждый уголок пространства, а иногда удается быстро найти цель.

        В этом пути каждый новый шаг может привести к ответу, которого мы не ожидали.

        ","path":["Глава 10. Поиск","Глава 10.   Поиск"],"tags":[]},{"location":"chapter_searching/#_1","level":2,"title":"Содержание главы","text":"
        • 10.1   Двоичный поиск
        • 10.2   Двоичный поиск точки вставки
        • 10.3   Двоичный поиск границ
        • 10.4   Стратегии оптимизации хеширования
        • 10.5   Переосмысление алгоритмов поиска
        • 10.6   Резюме
        ","path":["Глава 10. Поиск","Глава 10.   Поиск"],"tags":[]},{"location":"chapter_searching/binary_search/","level":1,"title":"10.1   Двоичный поиск","text":"

        Двоичный поиск (binary search) - это эффективный алгоритм поиска, основанный на стратегии «разделяй и властвуй». Он использует упорядоченность данных, сокращая на каждом шаге область поиска вдвое, пока не будет найден целевой элемент или пока интервал поиска не опустеет.

        Question

        Дан массив nums длины \\(n\\), элементы которого расположены в порядке возрастания и не повторяются. Найдите и верните индекс элемента target в этом массиве. Если массив не содержит этого элемента, верните \\(-1\\) . Пример показан на рисунке 10-1.

        Рисунок 10-1   Пример данных для двоичного поиска

        Как показано на рисунке 10-2, сначала инициализируем указатели \\(i = 0\\) и \\(j = n - 1\\) , которые указывают на первый и последний элементы массива и задают интервал поиска \\([0, n - 1]\\) . Обратите внимание: квадратные скобки обозначают замкнутый интервал и включают граничные значения.

        Далее в цикле выполняются следующие два шага.

        1. Вычислить индекс середины \\(m = \\lfloor {(i + j) / 2} \\rfloor\\) , где \\(\\lfloor \\: \\rfloor\\) означает операцию округления вниз.
        2. Сравнить nums[m] и target , после чего возможны три случая.
          1. Если nums[m] < target , это означает, что target находится в интервале \\([m + 1, j]\\) , поэтому выполняется \\(i = m + 1\\) .
          2. Если nums[m] > target , это означает, что target находится в интервале \\([i, m - 1]\\) , поэтому выполняется \\(j = m - 1\\) .
          3. Если nums[m] = target , значит, элемент target найден, поэтому возвращается индекс \\(m\\) .

        Если массив не содержит целевой элемент, область поиска в итоге сузится до пустого интервала. В этом случае возвращается \\(-1\\) .

        <1><2><3><4><5><6><7>

        Рисунок 10-2   Процесс двоичного поиска

        Стоит отметить, что поскольку и \\(i\\) , и \\(j\\) имеют тип int , то сумма \\(i + j\\) может выйти за пределы диапазона типа int. Чтобы избежать переполнения, обычно используют формулу \\(m = \\lfloor {i + (j - i) / 2} \\rfloor\\) для вычисления середины.

        Код приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search.py
        def binary_search(nums: list[int], target: int) -> int:\n    \"\"\"Бинарный поиск (двусторонне замкнутый интервал)\"\"\"\n    # Инициализировать двусторонне замкнутый интервал [0, n-1], то есть i и j указывают на первый и последний элементы массива соответственно\n    i, j = 0, len(nums) - 1\n    # Цикл завершается, когда диапазон поиска пуст (при i > j диапазон пуст)\n    while i <= j:\n        # Теоретически числа в Python могут быть сколь угодно большими (ограничены только объемом памяти), поэтому не нужно учитывать переполнение больших чисел\n        m = (i + j) // 2  # Вычислить индекс середины m\n        if nums[m] < target:\n            i = m + 1  # Это означает, что target находится в интервале [m+1, j]\n        elif nums[m] > target:\n            j = m - 1  # Это означает, что target находится в интервале [i, m-1]\n        else:\n            return m  # Целевой элемент найден, вернуть его индекс\n    return -1  # Целевой элемент не найден, вернуть -1\n
        binary_search.cpp
        /* Бинарный поиск (двусторонне замкнутый интервал) */\nint binarySearch(vector<int> &nums, int target) {\n    // Инициализировать двусторонне замкнутый интервал [0, n-1], то есть i и j указывают на первый и последний элементы массива соответственно\n    int i = 0, j = nums.size() - 1;\n    // Цикл завершается, когда диапазон поиска пуст (при i > j диапазон пуст)\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Вычислить индекс середины m\n        if (nums[m] < target)    // Это означает, что target находится в интервале [m+1, j]\n            i = m + 1;\n        else if (nums[m] > target) // Это означает, что target находится в интервале [i, m-1]\n            j = m - 1;\n        else // Целевой элемент найден, вернуть его индекс\n            return m;\n    }\n    // Целевой элемент не найден, вернуть -1\n    return -1;\n}\n
        binary_search.java
        /* Бинарный поиск (двусторонне замкнутый интервал) */\nint binarySearch(int[] nums, int target) {\n    // Инициализировать двусторонне замкнутый интервал [0, n-1], то есть i и j указывают на первый и последний элементы массива соответственно\n    int i = 0, j = nums.length - 1;\n    // Цикл завершается, когда диапазон поиска пуст (при i > j диапазон пуст)\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Вычислить индекс середины m\n        if (nums[m] < target) // Это означает, что target находится в интервале [m+1, j]\n            i = m + 1;\n        else if (nums[m] > target) // Это означает, что target находится в интервале [i, m-1]\n            j = m - 1;\n        else // Целевой элемент найден, вернуть его индекс\n            return m;\n    }\n    // Целевой элемент не найден, вернуть -1\n    return -1;\n}\n
        binary_search.cs
        /* Бинарный поиск (двусторонне замкнутый интервал) */\nint BinarySearch(int[] nums, int target) {\n    // Инициализировать двусторонне замкнутый интервал [0, n-1], то есть i и j указывают на первый и последний элементы массива соответственно\n    int i = 0, j = nums.Length - 1;\n    // Цикл завершается, когда диапазон поиска пуст (при i > j диапазон пуст)\n    while (i <= j) {\n        int m = i + (j - i) / 2;   // Вычислить индекс середины m\n        if (nums[m] < target)      // Это означает, что target находится в интервале [m+1, j]\n            i = m + 1;\n        else if (nums[m] > target) // Это означает, что target находится в интервале [i, m-1]\n            j = m - 1;\n        else                       // Целевой элемент найден, вернуть его индекс\n            return m;\n    }\n    // Целевой элемент не найден, вернуть -1\n    return -1;\n}\n
        binary_search.go
        /* Бинарный поиск (двусторонне замкнутый интервал) */\nfunc binarySearch(nums []int, target int) int {\n    // Инициализировать двусторонне замкнутый интервал [0, n-1], то есть i и j указывают на первый и последний элементы массива соответственно\n    i, j := 0, len(nums)-1\n    // Цикл завершается, когда диапазон поиска пуст (при i > j диапазон пуст)\n    for i <= j {\n        m := i + (j-i)/2      // Вычислить индекс середины m\n        if nums[m] < target { // Это означает, что target находится в интервале [m+1, j]\n            i = m + 1\n        } else if nums[m] > target { // Это означает, что target находится в интервале [i, m-1]\n            j = m - 1\n        } else { // Целевой элемент найден, вернуть его индекс\n            return m\n        }\n    }\n    // Целевой элемент не найден, вернуть -1\n    return -1\n}\n
        binary_search.swift
        /* Бинарный поиск (двусторонне замкнутый интервал) */\nfunc binarySearch(nums: [Int], target: Int) -> Int {\n    // Инициализировать двусторонне замкнутый интервал [0, n-1], то есть i и j указывают на первый и последний элементы массива соответственно\n    var i = nums.startIndex\n    var j = nums.endIndex - 1\n    // Цикл завершается, когда диапазон поиска пуст (при i > j диапазон пуст)\n    while i <= j {\n        let m = i + (j - i) / 2 // Вычислить индекс середины m\n        if nums[m] < target { // Это означает, что target находится в интервале [m+1, j]\n            i = m + 1\n        } else if nums[m] > target { // Это означает, что target находится в интервале [i, m-1]\n            j = m - 1\n        } else { // Целевой элемент найден, вернуть его индекс\n            return m\n        }\n    }\n    // Целевой элемент не найден, вернуть -1\n    return -1\n}\n
        binary_search.js
        /* Бинарный поиск (двусторонне замкнутый интервал) */\nfunction binarySearch(nums, target) {\n    // Инициализировать двусторонне замкнутый интервал [0, n-1], то есть i и j указывают на первый и последний элементы массива соответственно\n    let i = 0,\n        j = nums.length - 1;\n    // Цикл завершается, когда диапазон поиска пуст (при i > j диапазон пуст)\n    while (i <= j) {\n        // Вычислить индекс середины m, используя parseInt() для округления вниз\n        const m = parseInt(i + (j - i) / 2);\n        if (nums[m] < target)\n            // Это означает, что target находится в интервале [m+1, j]\n            i = m + 1;\n        else if (nums[m] > target)\n            // Это означает, что target находится в интервале [i, m-1]\n            j = m - 1;\n        else return m; // Целевой элемент найден, вернуть его индекс\n    }\n    // Целевой элемент не найден, вернуть -1\n    return -1;\n}\n
        binary_search.ts
        /* Бинарный поиск (двусторонне замкнутый интервал) */\nfunction binarySearch(nums: number[], target: number): number {\n    // Инициализировать двусторонне замкнутый интервал [0, n-1], то есть i и j указывают на первый и последний элементы массива соответственно\n    let i = 0,\n        j = nums.length - 1;\n    // Цикл завершается, когда диапазон поиска пуст (при i > j диапазон пуст)\n    while (i <= j) {\n        // Вычислить индекс середины m\n        const m = Math.floor(i + (j - i) / 2);\n        if (nums[m] < target) {\n            // Это означает, что target находится в интервале [m+1, j]\n            i = m + 1;\n        } else if (nums[m] > target) {\n            // Это означает, что target находится в интервале [i, m-1]\n            j = m - 1;\n        } else {\n            // Целевой элемент найден, вернуть его индекс\n            return m;\n        }\n    }\n    return -1; // Целевой элемент не найден, вернуть -1\n}\n
        binary_search.dart
        /* Бинарный поиск (двусторонне замкнутый интервал) */\nint binarySearch(List<int> nums, int target) {\n  // Инициализировать двусторонне замкнутый интервал [0, n-1], то есть i и j указывают на первый и последний элементы массива соответственно\n  int i = 0, j = nums.length - 1;\n  // Цикл завершается, когда диапазон поиска пуст (при i > j диапазон пуст)\n  while (i <= j) {\n    int m = i + (j - i) ~/ 2; // Вычислить индекс середины m\n    if (nums[m] < target) {\n      // Это означает, что target находится в интервале [m+1, j]\n      i = m + 1;\n    } else if (nums[m] > target) {\n      // Это означает, что target находится в интервале [i, m-1]\n      j = m - 1;\n    } else {\n      // Целевой элемент найден, вернуть его индекс\n      return m;\n    }\n  }\n  // Целевой элемент не найден, вернуть -1\n  return -1;\n}\n
        binary_search.rs
        /* Бинарный поиск (двусторонне замкнутый интервал) */\nfn binary_search(nums: &[i32], target: i32) -> i32 {\n    // Инициализировать двусторонне замкнутый интервал [0, n-1], то есть i и j указывают на первый и последний элементы массива соответственно\n    let mut i = 0;\n    let mut j = nums.len() as i32 - 1;\n    // Цикл завершается, когда диапазон поиска пуст (при i > j диапазон пуст)\n    while i <= j {\n        let m = i + (j - i) / 2; // Вычислить индекс середины m\n        if nums[m as usize] < target {\n            // Это означает, что target находится в интервале [m+1, j]\n            i = m + 1;\n        } else if nums[m as usize] > target {\n            // Это означает, что target находится в интервале [i, m-1]\n            j = m - 1;\n        } else {\n            // Целевой элемент найден, вернуть его индекс\n            return m;\n        }\n    }\n    // Целевой элемент не найден, вернуть -1\n    return -1;\n}\n
        binary_search.c
        /* Бинарный поиск (двусторонне замкнутый интервал) */\nint binarySearch(int *nums, int len, int target) {\n    // Инициализировать двусторонне замкнутый интервал [0, n-1], то есть i и j указывают на первый и последний элементы массива соответственно\n    int i = 0, j = len - 1;\n    // Цикл завершается, когда диапазон поиска пуст (при i > j диапазон пуст)\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Вычислить индекс середины m\n        if (nums[m] < target)    // Это означает, что target находится в интервале [m+1, j]\n            i = m + 1;\n        else if (nums[m] > target) // Это означает, что target находится в интервале [i, m-1]\n            j = m - 1;\n        else // Целевой элемент найден, вернуть его индекс\n            return m;\n    }\n    // Целевой элемент не найден, вернуть -1\n    return -1;\n}\n
        binary_search.kt
        /* Бинарный поиск (двусторонне замкнутый интервал) */\nfun binarySearch(nums: IntArray, target: Int): Int {\n    // Инициализировать двусторонне замкнутый интервал [0, n-1], то есть i и j указывают на первый и последний элементы массива соответственно\n    var i = 0\n    var j = nums.size - 1\n    // Цикл завершается, когда диапазон поиска пуст (при i > j диапазон пуст)\n    while (i <= j) {\n        val m = i + (j - i) / 2 // Вычислить индекс середины m\n        if (nums[m] < target) // Это означает, что target находится в интервале [m+1, j]\n            i = m + 1\n        else if (nums[m] > target) // Это означает, что target находится в интервале [i, m-1]\n            j = m - 1\n        else  // Целевой элемент найден, вернуть его индекс\n            return m\n    }\n    // Целевой элемент не найден, вернуть -1\n    return -1\n}\n
        binary_search.rb
        ### Бинарный поиск (двусторонне замкнутый интервал) ###\ndef binary_search(nums, target)\n  # Инициализировать двусторонне замкнутый интервал [0, n-1], то есть i и j указывают на первый и последний элементы массива соответственно\n  i, j = 0, nums.length - 1\n\n  # Цикл завершается, когда диапазон поиска пуст (при i > j диапазон пуст)\n  while i <= j\n    # Теоретически числа в Ruby могут быть сколь угодно большими (ограничены только объемом памяти), поэтому не нужно учитывать переполнение больших чисел\n    m = (i + j) / 2   # Вычислить индекс середины m\n\n    if nums[m] < target\n      i = m + 1 # Это означает, что target находится в интервале [m+1, j]\n    elsif nums[m] > target\n      j = m - 1 # Это означает, что target находится в интервале [i, m-1]\n    else\n      return m  # Целевой элемент найден, вернуть его индекс\n    end\n  end\n\n  -1  # Целевой элемент не найден, вернуть -1\nend\n
        Визуализация кода

        Во весь экран >

        Временная сложность равна \\(O(\\log n)\\) : в цикле двоичного поиска интервал каждый раз сокращается вдвое, поэтому число итераций равно \\(\\log_2 n\\) .

        Пространственная сложность равна \\(O(1)\\) : указатели \\(i\\) и \\(j\\) занимают константный объем памяти.

        ","path":["Глава 10. Поиск","10.1   Двоичный поиск"],"tags":[]},{"location":"chapter_searching/binary_search/#1011","level":2,"title":"10.1.1   Методы представления интервалов","text":"

        Помимо описанного выше двойного замкнутого интервала, часто используется и левозамкнутый правооткрытый интервал, который задается как \\([0, n)\\) , то есть левая граница включается, а правая - нет. В этом представлении интервал \\([i, j)\\) пуст, когда \\(i = j\\) .

        На основе этого представления можно реализовать двоичный поиск с той же функциональностью:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search.py
        def binary_search_lcro(nums: list[int], target: int) -> int:\n    \"\"\"Бинарный поиск (лево замкнутый, право открытый интервал)\"\"\"\n    # Инициализировать лево замкнутый, право открытый интервал [0, n), то есть i и j указывают на первый элемент массива и позицию сразу за последним элементом соответственно\n    i, j = 0, len(nums)\n    # Цикл завершается, когда диапазон поиска пуст (при i = j диапазон пуст)\n    while i < j:\n        m = (i + j) // 2  # Вычислить индекс середины m\n        if nums[m] < target:\n            i = m + 1  # Это означает, что target находится в интервале [m+1, j)\n        elif nums[m] > target:\n            j = m  # Это означает, что target находится в интервале [i, m)\n        else:\n            return m  # Целевой элемент найден, вернуть его индекс\n    return -1  # Целевой элемент не найден, вернуть -1\n
        binary_search.cpp
        /* Бинарный поиск (лево замкнутый, право открытый интервал) */\nint binarySearchLCRO(vector<int> &nums, int target) {\n    // Инициализировать лево замкнутый, право открытый интервал [0, n), то есть i и j указывают на первый элемент массива и позицию сразу за последним элементом соответственно\n    int i = 0, j = nums.size();\n    // Цикл завершается, когда диапазон поиска пуст (при i = j диапазон пуст)\n    while (i < j) {\n        int m = i + (j - i) / 2; // Вычислить индекс середины m\n        if (nums[m] < target)    // Это означает, что target находится в интервале [m+1, j)\n            i = m + 1;\n        else if (nums[m] > target) // Это означает, что target находится в интервале [i, m)\n            j = m;\n        else // Целевой элемент найден, вернуть его индекс\n            return m;\n    }\n    // Целевой элемент не найден, вернуть -1\n    return -1;\n}\n
        binary_search.java
        /* Бинарный поиск (лево замкнутый, право открытый интервал) */\nint binarySearchLCRO(int[] nums, int target) {\n    // Инициализировать лево замкнутый, право открытый интервал [0, n), то есть i и j указывают на первый элемент массива и позицию сразу за последним элементом соответственно\n    int i = 0, j = nums.length;\n    // Цикл завершается, когда диапазон поиска пуст (при i = j диапазон пуст)\n    while (i < j) {\n        int m = i + (j - i) / 2; // Вычислить индекс середины m\n        if (nums[m] < target) // Это означает, что target находится в интервале [m+1, j)\n            i = m + 1;\n        else if (nums[m] > target) // Это означает, что target находится в интервале [i, m)\n            j = m;\n        else // Целевой элемент найден, вернуть его индекс\n            return m;\n    }\n    // Целевой элемент не найден, вернуть -1\n    return -1;\n}\n
        binary_search.cs
        /* Бинарный поиск (лево замкнутый, право открытый интервал) */\nint BinarySearchLCRO(int[] nums, int target) {\n    // Инициализировать лево замкнутый, право открытый интервал [0, n), то есть i и j указывают на первый элемент массива и позицию сразу за последним элементом соответственно\n    int i = 0, j = nums.Length;\n    // Цикл завершается, когда диапазон поиска пуст (при i = j диапазон пуст)\n    while (i < j) {\n        int m = i + (j - i) / 2;   // Вычислить индекс середины m\n        if (nums[m] < target)      // Это означает, что target находится в интервале [m+1, j)\n            i = m + 1;\n        else if (nums[m] > target) // Это означает, что target находится в интервале [i, m)\n            j = m;\n        else                       // Целевой элемент найден, вернуть его индекс\n            return m;\n    }\n    // Целевой элемент не найден, вернуть -1\n    return -1;\n}\n
        binary_search.go
        /* Бинарный поиск (лево замкнутый, право открытый интервал) */\nfunc binarySearchLCRO(nums []int, target int) int {\n    // Инициализировать лево замкнутый, право открытый интервал [0, n), то есть i и j указывают на первый элемент массива и позицию сразу за последним элементом соответственно\n    i, j := 0, len(nums)\n    // Цикл завершается, когда диапазон поиска пуст (при i = j диапазон пуст)\n    for i < j {\n        m := i + (j-i)/2      // Вычислить индекс середины m\n        if nums[m] < target { // Это означает, что target находится в интервале [m+1, j)\n            i = m + 1\n        } else if nums[m] > target { // Это означает, что target находится в интервале [i, m)\n            j = m\n        } else { // Целевой элемент найден, вернуть его индекс\n            return m\n        }\n    }\n    // Целевой элемент не найден, вернуть -1\n    return -1\n}\n
        binary_search.swift
        /* Бинарный поиск (лево замкнутый, право открытый интервал) */\nfunc binarySearchLCRO(nums: [Int], target: Int) -> Int {\n    // Инициализировать лево замкнутый, право открытый интервал [0, n), то есть i и j указывают на первый элемент массива и позицию сразу за последним элементом соответственно\n    var i = nums.startIndex\n    var j = nums.endIndex\n    // Цикл завершается, когда диапазон поиска пуст (при i = j диапазон пуст)\n    while i < j {\n        let m = i + (j - i) / 2 // Вычислить индекс середины m\n        if nums[m] < target { // Это означает, что target находится в интервале [m+1, j)\n            i = m + 1\n        } else if nums[m] > target { // Это означает, что target находится в интервале [i, m)\n            j = m\n        } else { // Целевой элемент найден, вернуть его индекс\n            return m\n        }\n    }\n    // Целевой элемент не найден, вернуть -1\n    return -1\n}\n
        binary_search.js
        /* Бинарный поиск (лево замкнутый, право открытый интервал) */\nfunction binarySearchLCRO(nums, target) {\n    // Инициализировать лево замкнутый, право открытый интервал [0, n), то есть i и j указывают на первый элемент массива и позицию сразу за последним элементом соответственно\n    let i = 0,\n        j = nums.length;\n    // Цикл завершается, когда диапазон поиска пуст (при i = j диапазон пуст)\n    while (i < j) {\n        // Вычислить индекс середины m, используя parseInt() для округления вниз\n        const m = parseInt(i + (j - i) / 2);\n        if (nums[m] < target)\n            // Это означает, что target находится в интервале [m+1, j)\n            i = m + 1;\n        else if (nums[m] > target)\n            // Это означает, что target находится в интервале [i, m)\n            j = m;\n        // Целевой элемент найден, вернуть его индекс\n        else return m;\n    }\n    // Целевой элемент не найден, вернуть -1\n    return -1;\n}\n
        binary_search.ts
        /* Бинарный поиск (лево замкнутый, право открытый интервал) */\nfunction binarySearchLCRO(nums: number[], target: number): number {\n    // Инициализировать лево замкнутый, право открытый интервал [0, n), то есть i и j указывают на первый элемент массива и позицию сразу за последним элементом соответственно\n    let i = 0,\n        j = nums.length;\n    // Цикл завершается, когда диапазон поиска пуст (при i = j диапазон пуст)\n    while (i < j) {\n        // Вычислить индекс середины m\n        const m = Math.floor(i + (j - i) / 2);\n        if (nums[m] < target) {\n            // Это означает, что target находится в интервале [m+1, j)\n            i = m + 1;\n        } else if (nums[m] > target) {\n            // Это означает, что target находится в интервале [i, m)\n            j = m;\n        } else {\n            // Целевой элемент найден, вернуть его индекс\n            return m;\n        }\n    }\n    return -1; // Целевой элемент не найден, вернуть -1\n}\n
        binary_search.dart
        /* Бинарный поиск (лево замкнутый, право открытый интервал) */\nint binarySearchLCRO(List<int> nums, int target) {\n  // Инициализировать лево замкнутый, право открытый интервал [0, n), то есть i и j указывают на первый элемент массива и позицию сразу за последним элементом соответственно\n  int i = 0, j = nums.length;\n  // Цикл завершается, когда диапазон поиска пуст (при i = j диапазон пуст)\n  while (i < j) {\n    int m = i + (j - i) ~/ 2; // Вычислить индекс середины m\n    if (nums[m] < target) {\n      // Это означает, что target находится в интервале [m+1, j)\n      i = m + 1;\n    } else if (nums[m] > target) {\n      // Это означает, что target находится в интервале [i, m)\n      j = m;\n    } else {\n      // Целевой элемент найден, вернуть его индекс\n      return m;\n    }\n  }\n  // Целевой элемент не найден, вернуть -1\n  return -1;\n}\n
        binary_search.rs
        /* Бинарный поиск (лево замкнутый, право открытый интервал) */\nfn binary_search_lcro(nums: &[i32], target: i32) -> i32 {\n    // Инициализировать лево замкнутый, право открытый интервал [0, n), то есть i и j указывают на первый элемент массива и позицию сразу за последним элементом соответственно\n    let mut i = 0;\n    let mut j = nums.len() as i32;\n    // Цикл завершается, когда диапазон поиска пуст (при i = j диапазон пуст)\n    while i < j {\n        let m = i + (j - i) / 2; // Вычислить индекс середины m\n        if nums[m as usize] < target {\n            // Это означает, что target находится в интервале [m+1, j)\n            i = m + 1;\n        } else if nums[m as usize] > target {\n            // Это означает, что target находится в интервале [i, m)\n            j = m;\n        } else {\n            // Целевой элемент найден, вернуть его индекс\n            return m;\n        }\n    }\n    // Целевой элемент не найден, вернуть -1\n    return -1;\n}\n
        binary_search.c
        /* Бинарный поиск (лево замкнутый, право открытый интервал) */\nint binarySearchLCRO(int *nums, int len, int target) {\n    // Инициализировать лево замкнутый, право открытый интервал [0, n), то есть i и j указывают на первый элемент массива и позицию сразу за последним элементом соответственно\n    int i = 0, j = len;\n    // Цикл завершается, когда диапазон поиска пуст (при i = j диапазон пуст)\n    while (i < j) {\n        int m = i + (j - i) / 2; // Вычислить индекс середины m\n        if (nums[m] < target)    // Это означает, что target находится в интервале [m+1, j)\n            i = m + 1;\n        else if (nums[m] > target) // Это означает, что target находится в интервале [i, m)\n            j = m;\n        else // Целевой элемент найден, вернуть его индекс\n            return m;\n    }\n    // Целевой элемент не найден, вернуть -1\n    return -1;\n}\n
        binary_search.kt
        /* Бинарный поиск (лево замкнутый, право открытый интервал) */\nfun binarySearchLCRO(nums: IntArray, target: Int): Int {\n    // Инициализировать лево замкнутый, право открытый интервал [0, n), то есть i и j указывают на первый элемент массива и позицию сразу за последним элементом соответственно\n    var i = 0\n    var j = nums.size\n    // Цикл завершается, когда диапазон поиска пуст (при i = j диапазон пуст)\n    while (i < j) {\n        val m = i + (j - i) / 2 // Вычислить индекс середины m\n        if (nums[m] < target) // Это означает, что target находится в интервале [m+1, j)\n            i = m + 1\n        else if (nums[m] > target) // Это означает, что target находится в интервале [i, m)\n            j = m\n        else  // Целевой элемент найден, вернуть его индекс\n            return m\n    }\n    // Целевой элемент не найден, вернуть -1\n    return -1\n}\n
        binary_search.rb
        ### Бинарный поиск (лево замкнутый, право открытый интервал) ###\ndef binary_search_lcro(nums, target)\n  # Инициализировать лево замкнутый, право открытый интервал [0, n), то есть i и j указывают на первый элемент массива и позицию сразу за последним элементом соответственно\n  i, j = 0, nums.length\n\n  # Цикл завершается, когда диапазон поиска пуст (при i = j диапазон пуст)\n  while i < j\n    # Вычислить индекс середины m\n    m = (i + j) / 2\n\n    if nums[m] < target\n      i = m + 1 # Это означает, что target находится в интервале [m+1, j)\n    elsif nums[m] > target\n      j = m - 1 # Это означает, что target находится в интервале [i, m)\n    else\n      return m  # Целевой элемент найден, вернуть его индекс\n    end\n  end\n\n  -1  # Целевой элемент не найден, вернуть -1\nend\n
        Визуализация кода

        Во весь экран >

        Как показано на рисунке 10-3, в этих двух вариантах представления интервала различаются инициализация, условие цикла и операция сужения интервала в алгоритме двоичного поиска.

        Поскольку в записи «двойной замкнутый интервал» обе границы являются закрытыми, операции сужения интервала при помощи указателей \\(i\\) и \\(j\\) тоже получаются симметричными. Из-за этого в таком варианте сложнее допустить ошибку, поэтому обычно рекомендуется использовать именно запись «двойной замкнутый интервал».

        Рисунок 10-3   Два определения интервалов

        ","path":["Глава 10. Поиск","10.1   Двоичный поиск"],"tags":[]},{"location":"chapter_searching/binary_search/#1012","level":2,"title":"10.1.2   Преимущества и ограничения","text":"

        Двоичный поиск показывает хорошие результаты и по времени, и по памяти.

        • Двоичный поиск очень эффективен по времени. На больших объемах данных логарифмическая временная сложность дает заметное преимущество. Например, когда размер данных \\(n = 2^{20}\\) , линейный поиск потребует \\(2^{20} = 1048576\\) итераций, тогда как двоичный поиск выполнится всего за \\(\\log_2 2^{20} = 20\\) итераций.
        • Двоичный поиск не требует дополнительной памяти. По сравнению с алгоритмами поиска, которым нужно внешнее пространство (например, с хеш-поиском), двоичный поиск заметно экономнее по памяти.

        Однако двоичный поиск подходит не для всех ситуаций, и основные причины таковы.

        • Двоичный поиск применим только к упорядоченным данным. Если входные данные неупорядочены, специально сортировать их ради двоичного поиска невыгодно. Это связано с тем, что временная сложность алгоритмов сортировки обычно составляет \\(O(n \\log n)\\) , что выше, чем у линейного и двоичного поиска. Если элементы приходится часто вставлять, то для сохранения порядка в массиве их нужно помещать в конкретные позиции, а это требует \\(O(n)\\) времени и тоже обходится дорого.
        • Двоичный поиск применим только к массивам. Для него нужен скачкообразный доступ к элементам, а в связном списке такой доступ малоэффективен, поэтому двоичный поиск не подходит для списков и структур данных, построенных на их основе.
        • При малом объеме данных линейный поиск работает лучше. В линейном поиске на каждом шаге нужна всего одна операция сравнения. В двоичном поиске требуется 1 сложение, 1 деление, от 1 до 3 сравнений и еще 1 сложение или вычитание, то есть всего от 4 до 6 элементарных операций. Поэтому при небольшом \\(n\\) линейный поиск может оказаться быстрее двоичного.
        ","path":["Глава 10. Поиск","10.1   Двоичный поиск"],"tags":[]},{"location":"chapter_searching/binary_search_edge/","level":1,"title":"10.3   Двоичный поиск границ","text":"","path":["Глава 10. Поиск","10.3   Двоичный поиск границ"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#1031","level":2,"title":"10.3.1   Поиск левой границы","text":"

        Question

        Дан упорядоченный массив nums длины \\(n\\), который может содержать повторяющиеся элементы. Верните индекс самого левого элемента target в массиве. Если массив не содержит этот элемент, верните \\(-1\\) .

        Вспомним метод поиска точки вставки при двоичном поиске: после завершения поиска указатель \\(i\\) указывает на самый левый target , поэтому поиск точки вставки по сути является поиском индекса самого левого target.

        Рассмотрим реализацию поиска левой границы через функцию поиска точки вставки. Обратите внимание: массив может не содержать target , и тогда возможны две ситуации.

        • Индекс точки вставки \\(i\\) выходит за границы массива.
        • Элемент nums[i] не равен target .

        Если возникает любая из этих ситуаций, достаточно сразу вернуть \\(-1\\) . Код приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_edge.py
        def binary_search_left_edge(nums: list[int], target: int) -> int:\n    \"\"\"Бинарный поиск самого левого target\"\"\"\n    # Эквивалентно поиску точки вставки target\n    i = binary_search_insertion(nums, target)\n    # target не найден, вернуть -1\n    if i == len(nums) or nums[i] != target:\n        return -1\n    # Найти target и вернуть индекс i\n    return i\n
        binary_search_edge.cpp
        /* Бинарный поиск самого левого target */\nint binarySearchLeftEdge(vector<int> &nums, int target) {\n    // Эквивалентно поиску точки вставки target\n    int i = binarySearchInsertion(nums, target);\n    // target не найден, вернуть -1\n    if (i == nums.size() || nums[i] != target) {\n        return -1;\n    }\n    // Найти target и вернуть индекс i\n    return i;\n}\n
        binary_search_edge.java
        /* Бинарный поиск самого левого target */\nint binarySearchLeftEdge(int[] nums, int target) {\n    // Эквивалентно поиску точки вставки target\n    int i = binary_search_insertion.binarySearchInsertion(nums, target);\n    // target не найден, вернуть -1\n    if (i == nums.length || nums[i] != target) {\n        return -1;\n    }\n    // Найти target и вернуть индекс i\n    return i;\n}\n
        binary_search_edge.cs
        /* Бинарный поиск самого левого target */\nint BinarySearchLeftEdge(int[] nums, int target) {\n    // Эквивалентно поиску точки вставки target\n    int i = binary_search_insertion.BinarySearchInsertion(nums, target);\n    // target не найден, вернуть -1\n    if (i == nums.Length || nums[i] != target) {\n        return -1;\n    }\n    // Найти target и вернуть индекс i\n    return i;\n}\n
        binary_search_edge.go
        /* Бинарный поиск самого левого target */\nfunc binarySearchLeftEdge(nums []int, target int) int {\n    // Эквивалентно поиску точки вставки target\n    i := binarySearchInsertion(nums, target)\n    // target не найден, вернуть -1\n    if i == len(nums) || nums[i] != target {\n        return -1\n    }\n    // Найти target и вернуть индекс i\n    return i\n}\n
        binary_search_edge.swift
        /* Бинарный поиск самого левого target */\nfunc binarySearchLeftEdge(nums: [Int], target: Int) -> Int {\n    // Эквивалентно поиску точки вставки target\n    let i = binarySearchInsertion(nums: nums, target: target)\n    // target не найден, вернуть -1\n    if i == nums.endIndex || nums[i] != target {\n        return -1\n    }\n    // Найти target и вернуть индекс i\n    return i\n}\n
        binary_search_edge.js
        /* Бинарный поиск самого левого target */\nfunction binarySearchLeftEdge(nums, target) {\n    // Эквивалентно поиску точки вставки target\n    const i = binarySearchInsertion(nums, target);\n    // target не найден, вернуть -1\n    if (i === nums.length || nums[i] !== target) {\n        return -1;\n    }\n    // Найти target и вернуть индекс i\n    return i;\n}\n
        binary_search_edge.ts
        /* Бинарный поиск самого левого target */\nfunction binarySearchLeftEdge(nums: Array<number>, target: number): number {\n    // Эквивалентно поиску точки вставки target\n    const i = binarySearchInsertion(nums, target);\n    // target не найден, вернуть -1\n    if (i === nums.length || nums[i] !== target) {\n        return -1;\n    }\n    // Найти target и вернуть индекс i\n    return i;\n}\n
        binary_search_edge.dart
        /* Бинарный поиск самого левого target */\nint binarySearchLeftEdge(List<int> nums, int target) {\n  // Эквивалентно поиску точки вставки target\n  int i = binarySearchInsertion(nums, target);\n  // target не найден, вернуть -1\n  if (i == nums.length || nums[i] != target) {\n    return -1;\n  }\n  // Найти target и вернуть индекс i\n  return i;\n}\n
        binary_search_edge.rs
        /* Бинарный поиск самого левого target */\nfn binary_search_left_edge(nums: &[i32], target: i32) -> i32 {\n    // Эквивалентно поиску точки вставки target\n    let i = binary_search_insertion(nums, target);\n    // target не найден, вернуть -1\n    if i == nums.len() as i32 || nums[i as usize] != target {\n        return -1;\n    }\n    // Найти target и вернуть индекс i\n    i\n}\n
        binary_search_edge.c
        /* Бинарный поиск самого левого target */\nint binarySearchLeftEdge(int *nums, int numSize, int target) {\n    // Эквивалентно поиску точки вставки target\n    int i = binarySearchInsertion(nums, numSize, target);\n    // target не найден, вернуть -1\n    if (i == numSize || nums[i] != target) {\n        return -1;\n    }\n    // Найти target и вернуть индекс i\n    return i;\n}\n
        binary_search_edge.kt
        /* Бинарный поиск самого левого target */\nfun binarySearchLeftEdge(nums: IntArray, target: Int): Int {\n    // Эквивалентно поиску точки вставки target\n    val i = binarySearchInsertion(nums, target)\n    // target не найден, вернуть -1\n    if (i == nums.size || nums[i] != target) {\n        return -1\n    }\n    // Найти target и вернуть индекс i\n    return i\n}\n
        binary_search_edge.rb
        ### Бинарный поиск самого левого target ###\ndef binary_search_left_edge(nums, target)\n  # Эквивалентно поиску точки вставки target\n  i = binary_search_insertion(nums, target)\n\n  # target не найден, вернуть -1\n  return -1 if i == nums.length || nums[i] != target\n\n  i # Найти target и вернуть индекс i\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 10. Поиск","10.3   Двоичный поиск границ"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#1032","level":2,"title":"10.3.2   Поиск правой границы","text":"

        Как тогда найти самый правый target ? Самый прямой способ - изменить код, заменив операцию сужения указателя в случае nums[m] == target . Мы не будем приводить этот код, заинтересованные читатели могут реализовать его самостоятельно.

        Ниже представлены два более изящных способа.

        ","path":["Глава 10. Поиск","10.3   Двоичный поиск границ"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#1","level":3,"title":"1.   Повторное использование поиска левой границы","text":"

        На самом деле функцию поиска самого левого элемента можно использовать и для поиска самого правого элемента. Конкретная идея такова: преобразовать поиск самого правого target в поиск самого левого target + 1.

        Как показано на рисунке 10-7, после завершения поиска указатель \\(i\\) указывает на самый левый target + 1 (если он существует), а указатель \\(j\\) указывает на самый правый target , поэтому достаточно вернуть \\(j\\).

        Рисунок 10-7   Преобразование поиска правой границы в поиск левой

        Обратите внимание: функция возвращает точку вставки \\(i\\) , поэтому из нее нужно вычесть \\(1\\) , чтобы получить \\(j\\) :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_edge.py
        def binary_search_right_edge(nums: list[int], target: int) -> int:\n    \"\"\"Бинарный поиск самого правого target\"\"\"\n    # Преобразовать задачу в поиск самого левого target + 1\n    i = binary_search_insertion(nums, target + 1)\n    # j указывает на самый правый target, а i — на первый элемент больше target\n    j = i - 1\n    # target не найден, вернуть -1\n    if j == -1 or nums[j] != target:\n        return -1\n    # Найти target и вернуть индекс j\n    return j\n
        binary_search_edge.cpp
        /* Бинарный поиск самого правого target */\nint binarySearchRightEdge(vector<int> &nums, int target) {\n    // Преобразовать задачу в поиск самого левого target + 1\n    int i = binarySearchInsertion(nums, target + 1);\n    // j указывает на самый правый target, а i — на первый элемент больше target\n    int j = i - 1;\n    // target не найден, вернуть -1\n    if (j == -1 || nums[j] != target) {\n        return -1;\n    }\n    // Найти target и вернуть индекс j\n    return j;\n}\n
        binary_search_edge.java
        /* Бинарный поиск самого правого target */\nint binarySearchRightEdge(int[] nums, int target) {\n    // Преобразовать задачу в поиск самого левого target + 1\n    int i = binary_search_insertion.binarySearchInsertion(nums, target + 1);\n    // j указывает на самый правый target, а i — на первый элемент больше target\n    int j = i - 1;\n    // target не найден, вернуть -1\n    if (j == -1 || nums[j] != target) {\n        return -1;\n    }\n    // Найти target и вернуть индекс j\n    return j;\n}\n
        binary_search_edge.cs
        /* Бинарный поиск самого правого target */\nint BinarySearchRightEdge(int[] nums, int target) {\n    // Преобразовать задачу в поиск самого левого target + 1\n    int i = binary_search_insertion.BinarySearchInsertion(nums, target + 1);\n    // j указывает на самый правый target, а i — на первый элемент больше target\n    int j = i - 1;\n    // target не найден, вернуть -1\n    if (j == -1 || nums[j] != target) {\n        return -1;\n    }\n    // Найти target и вернуть индекс j\n    return j;\n}\n
        binary_search_edge.go
        /* Бинарный поиск самого правого target */\nfunc binarySearchRightEdge(nums []int, target int) int {\n    // Преобразовать задачу в поиск самого левого target + 1\n    i := binarySearchInsertion(nums, target+1)\n    // j указывает на самый правый target, а i — на первый элемент больше target\n    j := i - 1\n    // target не найден, вернуть -1\n    if j == -1 || nums[j] != target {\n        return -1\n    }\n    // Найти target и вернуть индекс j\n    return j\n}\n
        binary_search_edge.swift
        /* Бинарный поиск самого правого target */\nfunc binarySearchRightEdge(nums: [Int], target: Int) -> Int {\n    // Преобразовать задачу в поиск самого левого target + 1\n    let i = binarySearchInsertion(nums: nums, target: target + 1)\n    // j указывает на самый правый target, а i — на первый элемент больше target\n    let j = i - 1\n    // target не найден, вернуть -1\n    if j == -1 || nums[j] != target {\n        return -1\n    }\n    // Найти target и вернуть индекс j\n    return j\n}\n
        binary_search_edge.js
        /* Бинарный поиск самого правого target */\nfunction binarySearchRightEdge(nums, target) {\n    // Преобразовать задачу в поиск самого левого target + 1\n    const i = binarySearchInsertion(nums, target + 1);\n    // j указывает на самый правый target, а i — на первый элемент больше target\n    const j = i - 1;\n    // target не найден, вернуть -1\n    if (j === -1 || nums[j] !== target) {\n        return -1;\n    }\n    // Найти target и вернуть индекс j\n    return j;\n}\n
        binary_search_edge.ts
        /* Бинарный поиск самого правого target */\nfunction binarySearchRightEdge(nums: Array<number>, target: number): number {\n    // Преобразовать задачу в поиск самого левого target + 1\n    const i = binarySearchInsertion(nums, target + 1);\n    // j указывает на самый правый target, а i — на первый элемент больше target\n    const j = i - 1;\n    // target не найден, вернуть -1\n    if (j === -1 || nums[j] !== target) {\n        return -1;\n    }\n    // Найти target и вернуть индекс j\n    return j;\n}\n
        binary_search_edge.dart
        /* Бинарный поиск самого правого target */\nint binarySearchRightEdge(List<int> nums, int target) {\n  // Преобразовать задачу в поиск самого левого target + 1\n  int i = binarySearchInsertion(nums, target + 1);\n  // j указывает на самый правый target, а i — на первый элемент больше target\n  int j = i - 1;\n  // target не найден, вернуть -1\n  if (j == -1 || nums[j] != target) {\n    return -1;\n  }\n  // Найти target и вернуть индекс j\n  return j;\n}\n
        binary_search_edge.rs
        /* Бинарный поиск самого правого target */\nfn binary_search_right_edge(nums: &[i32], target: i32) -> i32 {\n    // Преобразовать задачу в поиск самого левого target + 1\n    let i = binary_search_insertion(nums, target + 1);\n    // j указывает на самый правый target, а i — на первый элемент больше target\n    let j = i - 1;\n    // target не найден, вернуть -1\n    if j == -1 || nums[j as usize] != target {\n        return -1;\n    }\n    // Найти target и вернуть индекс j\n    j\n}\n
        binary_search_edge.c
        /* Бинарный поиск самого правого target */\nint binarySearchRightEdge(int *nums, int numSize, int target) {\n    // Преобразовать задачу в поиск самого левого target + 1\n    int i = binarySearchInsertion(nums, numSize, target + 1);\n    // j указывает на самый правый target, а i — на первый элемент больше target\n    int j = i - 1;\n    // target не найден, вернуть -1\n    if (j == -1 || nums[j] != target) {\n        return -1;\n    }\n    // Найти target и вернуть индекс j\n    return j;\n}\n
        binary_search_edge.kt
        /* Бинарный поиск самого правого target */\nfun binarySearchRightEdge(nums: IntArray, target: Int): Int {\n    // Преобразовать задачу в поиск самого левого target + 1\n    val i = binarySearchInsertion(nums, target + 1)\n    // j указывает на самый правый target, а i — на первый элемент больше target\n    val j = i - 1\n    // target не найден, вернуть -1\n    if (j == -1 || nums[j] != target) {\n        return -1\n    }\n    // Найти target и вернуть индекс j\n    return j\n}\n
        binary_search_edge.rb
        ### Бинарный поиск самого правого target ###\ndef binary_search_right_edge(nums, target)\n  # Преобразовать задачу в поиск самого левого target + 1\n  i = binary_search_insertion(nums, target + 1)\n\n  # j указывает на самый правый target, а i — на первый элемент больше target\n  j = i - 1\n\n  # target не найден, вернуть -1\n  return -1 if j == -1 || nums[j] != target\n\n  j # Найти target и вернуть индекс j\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 10. Поиск","10.3   Двоичный поиск границ"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#2","level":3,"title":"2.   Преобразование в поиск элемента","text":"

        Мы знаем, что если массив не содержит target , то в конце поиска указатели \\(i\\) и \\(j\\) будут указывать соответственно на первый элемент, больший target , и на первый элемент, меньший target .

        Следовательно, как показано на рисунке 10-8, для поиска левой и правой границы можно сконструировать элемент, которого нет в массиве.

        • Поиск самого левого target : можно преобразовать в поиск target - 0.5 и вернуть указатель \\(i\\) .
        • Поиск самого правого target : можно преобразовать в поиск target + 0.5 и вернуть указатель \\(j\\) .

        Рисунок 10-8   Преобразование поиска границ в поиск элемента

        Код здесь опущен, но стоит обратить внимание на два момента.

        • По условию массив не содержит дробных чисел, поэтому нам не нужно беспокоиться о том, как обрабатывать случай равенства другим элементам массива.
        • Поскольку этот метод вводит дробные числа, переменную target в функции нужно изменить на тип с плавающей запятой (в Python менять ничего не требуется).
        ","path":["Глава 10. Поиск","10.3   Двоичный поиск границ"],"tags":[]},{"location":"chapter_searching/binary_search_insertion/","level":1,"title":"10.2   Двоичный поиск точки вставки","text":"

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

        ","path":["Глава 10. Поиск","10.2   Двоичный поиск точки вставки"],"tags":[]},{"location":"chapter_searching/binary_search_insertion/#1021","level":2,"title":"10.2.1   Случай без повторяющихся элементов","text":"

        Question

        Дан упорядоченный массив nums длины \\(n\\) и элемент target , причем в массиве нет повторяющихся элементов. Нужно вставить target в массив nums , сохранив порядок. Если элемент target уже присутствует в массиве, вставьте его слева от него. Верните индекс, который будет иметь target после вставки. Пример показан на рисунке 10-4.

        Рисунок 10-4   Пример данных для точки вставки

        Если мы хотим переиспользовать код двоичного поиска из предыдущего раздела, нужно ответить на два вопроса.

        Вопрос 1: если массив содержит target , будет ли индекс вставки совпадать с индексом этого элемента?

        По условию target нужно вставить слева от равного элемента, а это означает, что новый target занимает место старого target . Иначе говоря, если массив содержит target , то индекс вставки совпадает с индексом этого target.

        Вопрос 2: если массив не содержит target , индекс какого элемента будет точкой вставки?

        Рассмотрим процесс двоичного поиска подробнее: когда nums[m] < target , указатель \\(i\\) сдвигается вправо и тем самым приближается к элементу, который больше либо равен target . Аналогично указатель \\(j\\) постепенно приближается к элементу, который меньше либо равен target .

        Следовательно, после завершения двоичного поиска обязательно выполняется следующее: указатель \\(i\\) указывает на первый элемент, больший target , а указатель \\(j\\) указывает на первый элемент, меньший target . Нетрудно сделать вывод, что если массив не содержит target , то индекс вставки равен \\(i\\) . Код приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_insertion.py
        def binary_search_insertion_simple(nums: list[int], target: int) -> int:\n    \"\"\"Бинарный поиск точки вставки (без повторяющихся элементов)\"\"\"\n    i, j = 0, len(nums) - 1  # Инициализировать двусторонне замкнутый интервал [0, n-1]\n    while i <= j:\n        m = (i + j) // 2  # Вычислить индекс середины m\n        if nums[m] < target:\n            i = m + 1  # target находится в интервале [m+1, j]\n        elif nums[m] > target:\n            j = m - 1  # target находится в интервале [i, m-1]\n        else:\n            return m  # Найти target и вернуть точку вставки m\n    # target не найден, вернуть точку вставки i\n    return i\n
        binary_search_insertion.cpp
        /* Бинарный поиск точки вставки (без повторяющихся элементов) */\nint binarySearchInsertionSimple(vector<int> &nums, int target) {\n    int i = 0, j = nums.size() - 1; // Инициализировать двусторонне замкнутый интервал [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Вычислить индекс середины m\n        if (nums[m] < target) {\n            i = m + 1; // target находится в интервале [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target находится в интервале [i, m-1]\n        } else {\n            return m; // Найти target и вернуть точку вставки m\n        }\n    }\n    // target не найден, вернуть точку вставки i\n    return i;\n}\n
        binary_search_insertion.java
        /* Бинарный поиск точки вставки (без повторяющихся элементов) */\nint binarySearchInsertionSimple(int[] nums, int target) {\n    int i = 0, j = nums.length - 1; // Инициализировать двусторонне замкнутый интервал [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Вычислить индекс середины m\n        if (nums[m] < target) {\n            i = m + 1; // target находится в интервале [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target находится в интервале [i, m-1]\n        } else {\n            return m; // Найти target и вернуть точку вставки m\n        }\n    }\n    // target не найден, вернуть точку вставки i\n    return i;\n}\n
        binary_search_insertion.cs
        /* Бинарный поиск точки вставки (без повторяющихся элементов) */\nint BinarySearchInsertionSimple(int[] nums, int target) {\n    int i = 0, j = nums.Length - 1; // Инициализировать двусторонне замкнутый интервал [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Вычислить индекс середины m\n        if (nums[m] < target) {\n            i = m + 1; // target находится в интервале [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target находится в интервале [i, m-1]\n        } else {\n            return m; // Найти target и вернуть точку вставки m\n        }\n    }\n    // target не найден, вернуть точку вставки i\n    return i;\n}\n
        binary_search_insertion.go
        /* Бинарный поиск точки вставки (без повторяющихся элементов) */\nfunc binarySearchInsertionSimple(nums []int, target int) int {\n    // Инициализировать двусторонне замкнутый интервал [0, n-1]\n    i, j := 0, len(nums)-1\n    for i <= j {\n        // Вычислить индекс середины m\n        m := i + (j-i)/2\n        if nums[m] < target {\n            // target находится в интервале [m+1, j]\n            i = m + 1\n        } else if nums[m] > target {\n            // target находится в интервале [i, m-1]\n            j = m - 1\n        } else {\n            // Найти target и вернуть точку вставки m\n            return m\n        }\n    }\n    // target не найден, вернуть точку вставки i\n    return i\n}\n
        binary_search_insertion.swift
        /* Бинарный поиск точки вставки (без повторяющихся элементов) */\nfunc binarySearchInsertionSimple(nums: [Int], target: Int) -> Int {\n    // Инициализировать двусторонне замкнутый интервал [0, n-1]\n    var i = nums.startIndex\n    var j = nums.endIndex - 1\n    while i <= j {\n        let m = i + (j - i) / 2 // Вычислить индекс середины m\n        if nums[m] < target {\n            i = m + 1 // target находится в интервале [m+1, j]\n        } else if nums[m] > target {\n            j = m - 1 // target находится в интервале [i, m-1]\n        } else {\n            return m // Найти target и вернуть точку вставки m\n        }\n    }\n    // target не найден, вернуть точку вставки i\n    return i\n}\n
        binary_search_insertion.js
        /* Бинарный поиск точки вставки (без повторяющихся элементов) */\nfunction binarySearchInsertionSimple(nums, target) {\n    let i = 0,\n        j = nums.length - 1; // Инициализировать двусторонне замкнутый интервал [0, n-1]\n    while (i <= j) {\n        const m = Math.floor(i + (j - i) / 2); // Вычислить индекс середины m, используя Math.floor() для округления вниз\n        if (nums[m] < target) {\n            i = m + 1; // target находится в интервале [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target находится в интервале [i, m-1]\n        } else {\n            return m; // Найти target и вернуть точку вставки m\n        }\n    }\n    // target не найден, вернуть точку вставки i\n    return i;\n}\n
        binary_search_insertion.ts
        /* Бинарный поиск точки вставки (без повторяющихся элементов) */\nfunction binarySearchInsertionSimple(\n    nums: Array<number>,\n    target: number\n): number {\n    let i = 0,\n        j = nums.length - 1; // Инициализировать двусторонне замкнутый интервал [0, n-1]\n    while (i <= j) {\n        const m = Math.floor(i + (j - i) / 2); // Вычислить индекс середины m, используя Math.floor() для округления вниз\n        if (nums[m] < target) {\n            i = m + 1; // target находится в интервале [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target находится в интервале [i, m-1]\n        } else {\n            return m; // Найти target и вернуть точку вставки m\n        }\n    }\n    // target не найден, вернуть точку вставки i\n    return i;\n}\n
        binary_search_insertion.dart
        /* Бинарный поиск точки вставки (без повторяющихся элементов) */\nint binarySearchInsertionSimple(List<int> nums, int target) {\n  int i = 0, j = nums.length - 1; // Инициализировать двусторонне замкнутый интервал [0, n-1]\n  while (i <= j) {\n    int m = i + (j - i) ~/ 2; // Вычислить индекс середины m\n    if (nums[m] < target) {\n      i = m + 1; // target находится в интервале [m+1, j]\n    } else if (nums[m] > target) {\n      j = m - 1; // target находится в интервале [i, m-1]\n    } else {\n      return m; // Найти target и вернуть точку вставки m\n    }\n  }\n  // target не найден, вернуть точку вставки i\n  return i;\n}\n
        binary_search_insertion.rs
        /* Бинарный поиск точки вставки (без повторяющихся элементов) */\nfn binary_search_insertion_simple(nums: &[i32], target: i32) -> i32 {\n    let (mut i, mut j) = (0, nums.len() as i32 - 1); // Инициализировать двусторонне замкнутый интервал [0, n-1]\n    while i <= j {\n        let m = i + (j - i) / 2; // Вычислить индекс середины m\n        if nums[m as usize] < target {\n            i = m + 1; // target находится в интервале [m+1, j]\n        } else if nums[m as usize] > target {\n            j = m - 1; // target находится в интервале [i, m-1]\n        } else {\n            return m;\n        }\n    }\n    // target не найден, вернуть точку вставки i\n    i\n}\n
        binary_search_insertion.c
        /* Бинарный поиск точки вставки (без повторяющихся элементов) */\nint binarySearchInsertionSimple(int *nums, int numSize, int target) {\n    int i = 0, j = numSize - 1; // Инициализировать двусторонне замкнутый интервал [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Вычислить индекс середины m\n        if (nums[m] < target) {\n            i = m + 1; // target находится в интервале [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target находится в интервале [i, m-1]\n        } else {\n            return m; // Найти target и вернуть точку вставки m\n        }\n    }\n    // target не найден, вернуть точку вставки i\n    return i;\n}\n
        binary_search_insertion.kt
        /* Бинарный поиск точки вставки (без повторяющихся элементов) */\nfun binarySearchInsertionSimple(nums: IntArray, target: Int): Int {\n    var i = 0\n    var j = nums.size - 1 // Инициализировать двусторонне замкнутый интервал [0, n-1]\n    while (i <= j) {\n        val m = i + (j - i) / 2 // Вычислить индекс середины m\n        if (nums[m] < target) {\n            i = m + 1 // target находится в интервале [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1 // target находится в интервале [i, m-1]\n        } else {\n            return m // Найти target и вернуть точку вставки m\n        }\n    }\n    // target не найден, вернуть точку вставки i\n    return i\n}\n
        binary_search_insertion.rb
        ### Бинарный поиск точки вставки (без повторяющихся элементов) ###\ndef binary_search_insertion_simple(nums, target)\n  # Инициализировать двусторонне замкнутый интервал [0, n-1]\n  i, j = 0, nums.length - 1\n\n  while i <= j\n    # Вычислить индекс середины m\n    m = (i + j) / 2\n\n    if nums[m] < target\n      i = m + 1 # target находится в интервале [m+1, j]\n    elsif nums[m] > target\n      j = m - 1 # target находится в интервале [i, m-1]\n    else\n      return m  # Найти target и вернуть точку вставки m\n    end\n  end\n\n  i # target не найден, вернуть точку вставки i\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 10. Поиск","10.2   Двоичный поиск точки вставки"],"tags":[]},{"location":"chapter_searching/binary_search_insertion/#1022","level":2,"title":"10.2.2   Случай с повторяющимися элементами","text":"

        Question

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

        Если в массиве есть несколько элементов target , то обычный двоичный поиск сможет вернуть индекс только одного из них, но не позволит определить, сколько элементов target находится слева и справа от него.

        По условию целевой элемент нужно вставить в самую левую позицию, поэтому нам нужно найти индекс самого левого target в массиве. На первом этапе можно рассмотреть решение, показанное на рисунке 10-5.

        1. Выполнить двоичный поиск и получить индекс любого элемента target , обозначив его как \\(k\\) .
        2. Начиная с индекса \\(k\\) , линейно двигаться влево и вернуть результат, когда будет найден самый левый target .

        Рисунок 10-5   Линейный поиск точки вставки среди повторяющихся элементов

        Этот метод применим на практике, однако в нем есть линейный поиск, поэтому его временная сложность равна \\(O(n)\\) . Когда в массиве имеется много повторяющихся target , такой подход работает неэффективно.

        Теперь рассмотрим расширение кода двоичного поиска. Как показано на рисунке 10-6, общий процесс остается прежним: на каждом шаге мы сначала вычисляем индекс середины \\(m\\) , а затем сравниваем target и nums[m] , после чего возможны следующие случаи.

        • Когда nums[m] < target или nums[m] > target , это означает, что target еще не найден, поэтому используется стандартная операция сужения интервала в двоичном поиске, благодаря чему указатели \\(i\\) и \\(j\\) приближаются к target.
        • Когда nums[m] == target , это означает, что элементы меньше target находятся в интервале \\([i, m - 1]\\) , поэтому мы используем \\(j = m - 1\\) для сужения интервала, тем самым приближая указатель \\(j\\) к элементам, меньшим target.

        После завершения цикла указатель \\(i\\) будет указывать на самый левый target , а указатель \\(j\\) - на первый элемент, меньший target , поэтому индекс \\(i\\) и является точкой вставки.

        <1><2><3><4><5><6><7><8>

        Рисунок 10-6   Шаги поиска точки вставки для повторяющихся элементов

        Если посмотреть на следующий код, то видно, что действия в ветвях nums[m] > target и nums[m] == target совпадают, поэтому эти две ветви можно объединить.

        Даже в этом случае можно оставить условия развернутыми, потому что так логика выглядит более ясной и код легче читать.

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_insertion.py
        def binary_search_insertion(nums: list[int], target: int) -> int:\n    \"\"\"Бинарный поиск точки вставки (с повторяющимися элементами)\"\"\"\n    i, j = 0, len(nums) - 1  # Инициализировать двусторонне замкнутый интервал [0, n-1]\n    while i <= j:\n        m = (i + j) // 2  # Вычислить индекс середины m\n        if nums[m] < target:\n            i = m + 1  # target находится в интервале [m+1, j]\n        elif nums[m] > target:\n            j = m - 1  # target находится в интервале [i, m-1]\n        else:\n            j = m - 1  # Первый элемент меньше target находится в интервале [i, m-1]\n    # Вернуть точку вставки i\n    return i\n
        binary_search_insertion.cpp
        /* Бинарный поиск точки вставки (с повторяющимися элементами) */\nint binarySearchInsertion(vector<int> &nums, int target) {\n    int i = 0, j = nums.size() - 1; // Инициализировать двусторонне замкнутый интервал [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Вычислить индекс середины m\n        if (nums[m] < target) {\n            i = m + 1; // target находится в интервале [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target находится в интервале [i, m-1]\n        } else {\n            j = m - 1; // Первый элемент меньше target находится в интервале [i, m-1]\n        }\n    }\n    // Вернуть точку вставки i\n    return i;\n}\n
        binary_search_insertion.java
        /* Бинарный поиск точки вставки (с повторяющимися элементами) */\nint binarySearchInsertion(int[] nums, int target) {\n    int i = 0, j = nums.length - 1; // Инициализировать двусторонне замкнутый интервал [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Вычислить индекс середины m\n        if (nums[m] < target) {\n            i = m + 1; // target находится в интервале [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target находится в интервале [i, m-1]\n        } else {\n            j = m - 1; // Первый элемент меньше target находится в интервале [i, m-1]\n        }\n    }\n    // Вернуть точку вставки i\n    return i;\n}\n
        binary_search_insertion.cs
        /* Бинарный поиск точки вставки (с повторяющимися элементами) */\nint BinarySearchInsertion(int[] nums, int target) {\n    int i = 0, j = nums.Length - 1; // Инициализировать двусторонне замкнутый интервал [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Вычислить индекс середины m\n        if (nums[m] < target) {\n            i = m + 1; // target находится в интервале [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target находится в интервале [i, m-1]\n        } else {\n            j = m - 1; // Первый элемент меньше target находится в интервале [i, m-1]\n        }\n    }\n    // Вернуть точку вставки i\n    return i;\n}\n
        binary_search_insertion.go
        /* Бинарный поиск точки вставки (с повторяющимися элементами) */\nfunc binarySearchInsertion(nums []int, target int) int {\n    // Инициализировать двусторонне замкнутый интервал [0, n-1]\n    i, j := 0, len(nums)-1\n    for i <= j {\n        // Вычислить индекс середины m\n        m := i + (j-i)/2\n        if nums[m] < target {\n            // target находится в интервале [m+1, j]\n            i = m + 1\n        } else if nums[m] > target {\n            // target находится в интервале [i, m-1]\n            j = m - 1\n        } else {\n            // Первый элемент меньше target находится в интервале [i, m-1]\n            j = m - 1\n        }\n    }\n    // Вернуть точку вставки i\n    return i\n}\n
        binary_search_insertion.swift
        /* Бинарный поиск точки вставки (с повторяющимися элементами) */\nfunc binarySearchInsertion(nums: [Int], target: Int) -> Int {\n    // Инициализировать двусторонне замкнутый интервал [0, n-1]\n    var i = nums.startIndex\n    var j = nums.endIndex - 1\n    while i <= j {\n        let m = i + (j - i) / 2 // Вычислить индекс середины m\n        if nums[m] < target {\n            i = m + 1 // target находится в интервале [m+1, j]\n        } else if nums[m] > target {\n            j = m - 1 // target находится в интервале [i, m-1]\n        } else {\n            j = m - 1 // Первый элемент меньше target находится в интервале [i, m-1]\n        }\n    }\n    // Вернуть точку вставки i\n    return i\n}\n
        binary_search_insertion.js
        /* Бинарный поиск точки вставки (с повторяющимися элементами) */\nfunction binarySearchInsertion(nums, target) {\n    let i = 0,\n        j = nums.length - 1; // Инициализировать двусторонне замкнутый интервал [0, n-1]\n    while (i <= j) {\n        const m = Math.floor(i + (j - i) / 2); // Вычислить индекс середины m, используя Math.floor() для округления вниз\n        if (nums[m] < target) {\n            i = m + 1; // target находится в интервале [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target находится в интервале [i, m-1]\n        } else {\n            j = m - 1; // Первый элемент меньше target находится в интервале [i, m-1]\n        }\n    }\n    // Вернуть точку вставки i\n    return i;\n}\n
        binary_search_insertion.ts
        /* Бинарный поиск точки вставки (с повторяющимися элементами) */\nfunction binarySearchInsertion(nums: Array<number>, target: number): number {\n    let i = 0,\n        j = nums.length - 1; // Инициализировать двусторонне замкнутый интервал [0, n-1]\n    while (i <= j) {\n        const m = Math.floor(i + (j - i) / 2); // Вычислить индекс середины m, используя Math.floor() для округления вниз\n        if (nums[m] < target) {\n            i = m + 1; // target находится в интервале [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target находится в интервале [i, m-1]\n        } else {\n            j = m - 1; // Первый элемент меньше target находится в интервале [i, m-1]\n        }\n    }\n    // Вернуть точку вставки i\n    return i;\n}\n
        binary_search_insertion.dart
        /* Бинарный поиск точки вставки (с повторяющимися элементами) */\nint binarySearchInsertion(List<int> nums, int target) {\n  int i = 0, j = nums.length - 1; // Инициализировать двусторонне замкнутый интервал [0, n-1]\n  while (i <= j) {\n    int m = i + (j - i) ~/ 2; // Вычислить индекс середины m\n    if (nums[m] < target) {\n      i = m + 1; // target находится в интервале [m+1, j]\n    } else if (nums[m] > target) {\n      j = m - 1; // target находится в интервале [i, m-1]\n    } else {\n      j = m - 1; // Первый элемент меньше target находится в интервале [i, m-1]\n    }\n  }\n  // Вернуть точку вставки i\n  return i;\n}\n
        binary_search_insertion.rs
        /* Бинарный поиск точки вставки (с повторяющимися элементами) */\npub fn binary_search_insertion(nums: &[i32], target: i32) -> i32 {\n    let (mut i, mut j) = (0, nums.len() as i32 - 1); // Инициализировать двусторонне замкнутый интервал [0, n-1]\n    while i <= j {\n        let m = i + (j - i) / 2; // Вычислить индекс середины m\n        if nums[m as usize] < target {\n            i = m + 1; // target находится в интервале [m+1, j]\n        } else if nums[m as usize] > target {\n            j = m - 1; // target находится в интервале [i, m-1]\n        } else {\n            j = m - 1; // Первый элемент меньше target находится в интервале [i, m-1]\n        }\n    }\n    // Вернуть точку вставки i\n    i\n}\n
        binary_search_insertion.c
        /* Бинарный поиск точки вставки (с повторяющимися элементами) */\nint binarySearchInsertion(int *nums, int numSize, int target) {\n    int i = 0, j = numSize - 1; // Инициализировать двусторонне замкнутый интервал [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Вычислить индекс середины m\n        if (nums[m] < target) {\n            i = m + 1; // target находится в интервале [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target находится в интервале [i, m-1]\n        } else {\n            j = m - 1; // Первый элемент меньше target находится в интервале [i, m-1]\n        }\n    }\n    // Вернуть точку вставки i\n    return i;\n}\n
        binary_search_insertion.kt
        /* Бинарный поиск точки вставки (с повторяющимися элементами) */\nfun binarySearchInsertion(nums: IntArray, target: Int): Int {\n    var i = 0\n    var j = nums.size - 1 // Инициализировать двусторонне замкнутый интервал [0, n-1]\n    while (i <= j) {\n        val m = i + (j - i) / 2 // Вычислить индекс середины m\n        if (nums[m] < target) {\n            i = m + 1 // target находится в интервале [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1 // target находится в интервале [i, m-1]\n        } else {\n            j = m - 1 // Первый элемент меньше target находится в интервале [i, m-1]\n        }\n    }\n    // Вернуть точку вставки i\n    return i\n}\n
        binary_search_insertion.rb
        ### Бинарный поиск точки вставки (с повторяющимися элементами) ###\ndef binary_search_insertion(nums, target)\n  # Инициализировать двусторонне замкнутый интервал [0, n-1]\n  i, j = 0, nums.length - 1\n\n  while i <= j\n    # Вычислить индекс середины m\n    m = (i + j) / 2\n\n    if nums[m] < target\n      i = m + 1 # target находится в интервале [m+1, j]\n    elsif nums[m] > target\n      j = m - 1 # target находится в интервале [i, m-1]\n    else\n      j = m - 1 # Первый элемент меньше target находится в интервале [i, m-1]\n    end\n  end\n\n  i # Вернуть точку вставки i\nend\n
        Визуализация кода

        Во весь экран >

        Tip

        Код в этом разделе записан в стиле «двойного замкнутого интервала». При желании можно самостоятельно реализовать вариант «слева закрыт, справа открыт».

        Если смотреть в целом, суть двоичного поиска сводится к тому, что для указателей \\(i\\) и \\(j\\) заранее задаются ориентиры поиска. Целью может быть конкретный элемент, например target , а может быть и диапазон элементов, например все элементы, меньшие target .

        В ходе непрерывного двоичного деления указатели \\(i\\) и \\(j\\) постепенно приближаются к заранее заданной цели. В конце они либо успешно находят ответ, либо останавливаются после выхода за границы.

        ","path":["Глава 10. Поиск","10.2   Двоичный поиск точки вставки"],"tags":[]},{"location":"chapter_searching/replace_linear_by_hashing/","level":1,"title":"10.4   Стратегии оптимизации хеширования","text":"

        В алгоритмических задачах мы часто заменяем линейный поиск на хеш-поиск, чтобы уменьшить временную сложность алгоритма. Разберем одну задачу, чтобы лучше понять этот прием.

        Question

        Дан массив целых чисел nums и целевой элемент target . Найдите в массиве два элемента, сумма которых равна target , и верните их индексы. Подойдет любой корректный ответ.

        ","path":["Глава 10. Поиск","10.4   Стратегии оптимизации хеширования"],"tags":[]},{"location":"chapter_searching/replace_linear_by_hashing/#1041","level":2,"title":"10.4.1   Линейный поиск: обмен времени на пространство","text":"

        Рассмотрим прямой перебор всех возможных комбинаций. Как показано на рисунке 10-9, мы запускаем два вложенных цикла и на каждом шаге проверяем, равна ли сумма двух целых чисел target. Если да, то возвращаем их индексы.

        Рисунок 10-9   Линейный поиск для задачи о двух суммах

        Код приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby two_sum.py
        def two_sum_brute_force(nums: list[int], target: int) -> list[int]:\n    \"\"\"Метод 1: полный перебор\"\"\"\n    # Два вложенных цикла, временная сложность O(n^2)\n    for i in range(len(nums) - 1):\n        for j in range(i + 1, len(nums)):\n            if nums[i] + nums[j] == target:\n                return [i, j]\n    return []\n
        two_sum.cpp
        /* Метод 1: полный перебор */\nvector<int> twoSumBruteForce(vector<int> &nums, int target) {\n    int size = nums.size();\n    // Два вложенных цикла, временная сложность O(n^2)\n    for (int i = 0; i < size - 1; i++) {\n        for (int j = i + 1; j < size; j++) {\n            if (nums[i] + nums[j] == target)\n                return {i, j};\n        }\n    }\n    return {};\n}\n
        two_sum.java
        /* Метод 1: полный перебор */\nint[] twoSumBruteForce(int[] nums, int target) {\n    int size = nums.length;\n    // Два вложенных цикла, временная сложность O(n^2)\n    for (int i = 0; i < size - 1; i++) {\n        for (int j = i + 1; j < size; j++) {\n            if (nums[i] + nums[j] == target)\n                return new int[] { i, j };\n        }\n    }\n    return new int[0];\n}\n
        two_sum.cs
        /* Метод 1: полный перебор */\nint[] TwoSumBruteForce(int[] nums, int target) {\n    int size = nums.Length;\n    // Два вложенных цикла, временная сложность O(n^2)\n    for (int i = 0; i < size - 1; i++) {\n        for (int j = i + 1; j < size; j++) {\n            if (nums[i] + nums[j] == target)\n                return [i, j];\n        }\n    }\n    return [];\n}\n
        two_sum.go
        /* Метод 1: полный перебор */\nfunc twoSumBruteForce(nums []int, target int) []int {\n    size := len(nums)\n    // Два вложенных цикла, временная сложность O(n^2)\n    for i := 0; i < size-1; i++ {\n        for j := i + 1; j < size; j++ {\n            if nums[i]+nums[j] == target {\n                return []int{i, j}\n            }\n        }\n    }\n    return nil\n}\n
        two_sum.swift
        /* Метод 1: полный перебор */\nfunc twoSumBruteForce(nums: [Int], target: Int) -> [Int] {\n    // Два вложенных цикла, временная сложность O(n^2)\n    for i in nums.indices.dropLast() {\n        for j in nums.indices.dropFirst(i + 1) {\n            if nums[i] + nums[j] == target {\n                return [i, j]\n            }\n        }\n    }\n    return [0]\n}\n
        two_sum.js
        /* Метод 1: полный перебор */\nfunction twoSumBruteForce(nums, target) {\n    const n = nums.length;\n    // Два вложенных цикла, временная сложность O(n^2)\n    for (let i = 0; i < n; i++) {\n        for (let j = i + 1; j < n; j++) {\n            if (nums[i] + nums[j] === target) {\n                return [i, j];\n            }\n        }\n    }\n    return [];\n}\n
        two_sum.ts
        /* Метод 1: полный перебор */\nfunction twoSumBruteForce(nums: number[], target: number): number[] {\n    const n = nums.length;\n    // Два вложенных цикла, временная сложность O(n^2)\n    for (let i = 0; i < n; i++) {\n        for (let j = i + 1; j < n; j++) {\n            if (nums[i] + nums[j] === target) {\n                return [i, j];\n            }\n        }\n    }\n    return [];\n}\n
        two_sum.dart
        /* Способ 1: полный перебор */\nList<int> twoSumBruteForce(List<int> nums, int target) {\n  int size = nums.length;\n  // Два вложенных цикла, временная сложность O(n^2)\n  for (var i = 0; i < size - 1; i++) {\n    for (var j = i + 1; j < size; j++) {\n      if (nums[i] + nums[j] == target) return [i, j];\n    }\n  }\n  return [0];\n}\n
        two_sum.rs
        /* Метод 1: полный перебор */\npub fn two_sum_brute_force(nums: &Vec<i32>, target: i32) -> Option<Vec<i32>> {\n    let size = nums.len();\n    // Два вложенных цикла, временная сложность O(n^2)\n    for i in 0..size - 1 {\n        for j in i + 1..size {\n            if nums[i] + nums[j] == target {\n                return Some(vec![i as i32, j as i32]);\n            }\n        }\n    }\n    None\n}\n
        two_sum.c
        /* Метод 1: полный перебор */\nint *twoSumBruteForce(int *nums, int numsSize, int target, int *returnSize) {\n    for (int i = 0; i < numsSize; ++i) {\n        for (int j = i + 1; j < numsSize; ++j) {\n            if (nums[i] + nums[j] == target) {\n                int *res = malloc(sizeof(int) * 2);\n                res[0] = i, res[1] = j;\n                *returnSize = 2;\n                return res;\n            }\n        }\n    }\n    *returnSize = 0;\n    return NULL;\n}\n
        two_sum.kt
        /* Метод 1: полный перебор */\nfun twoSumBruteForce(nums: IntArray, target: Int): IntArray {\n    val size = nums.size\n    // Два вложенных цикла, временная сложность O(n^2)\n    for (i in 0..<size - 1) {\n        for (j in i + 1..<size) {\n            if (nums[i] + nums[j] == target) return intArrayOf(i, j)\n        }\n    }\n    return IntArray(0)\n}\n
        two_sum.rb
        ### Метод 1: полный перебор ###\ndef two_sum_brute_force(nums, target)\n  # Два вложенных цикла, временная сложность O(n^2)\n  for i in 0...(nums.length - 1)\n    for j in (i + 1)...nums.length\n      return [i, j] if nums[i] + nums[j] == target\n    end\n  end\n\n  []\nend\n
        Визуализация кода

        Во весь экран >

        Временная сложность этого метода равна \\(O(n^2)\\) , а пространственная сложность равна \\(O(1)\\) , поэтому на больших объемах данных он очень медленный.

        ","path":["Глава 10. Поиск","10.4   Стратегии оптимизации хеширования"],"tags":[]},{"location":"chapter_searching/replace_linear_by_hashing/#1042-","level":2,"title":"10.4.2   Хеш-поиск: обмен пространства на время","text":"

        Рассмотрим вариант с использованием хеш-таблицы, где ключами и значениями будут элементы массива и их индексы. При циклическом обходе массива на каждом шаге выполняются действия, показанные на рисунке 10-10.

        1. Проверить, находится ли число target - nums[i] в хеш-таблице. Если да, то сразу вернуть индексы этих двух элементов.
        2. Добавить в хеш-таблицу пару из ключа nums[i] и индекса i .
        <1><2><3>

        Рисунок 10-10   Вспомогательная хеш-таблица для задачи о двух суммах

        Код реализации показан ниже, и для него достаточно одного цикла:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby two_sum.py
        def two_sum_hash_table(nums: list[int], target: int) -> list[int]:\n    \"\"\"Метод 2: вспомогательная хеш-таблица\"\"\"\n    # Вспомогательная хеш-таблица, пространственная сложность O(n)\n    dic = {}\n    # Один цикл, временная сложность O(n)\n    for i in range(len(nums)):\n        if target - nums[i] in dic:\n            return [dic[target - nums[i]], i]\n        dic[nums[i]] = i\n    return []\n
        two_sum.cpp
        /* Метод 2: вспомогательная хеш-таблица */\nvector<int> twoSumHashTable(vector<int> &nums, int target) {\n    int size = nums.size();\n    // Вспомогательная хеш-таблица, пространственная сложность O(n)\n    unordered_map<int, int> dic;\n    // Один цикл, временная сложность O(n)\n    for (int i = 0; i < size; i++) {\n        if (dic.find(target - nums[i]) != dic.end()) {\n            return {dic[target - nums[i]], i};\n        }\n        dic.emplace(nums[i], i);\n    }\n    return {};\n}\n
        two_sum.java
        /* Метод 2: вспомогательная хеш-таблица */\nint[] twoSumHashTable(int[] nums, int target) {\n    int size = nums.length;\n    // Вспомогательная хеш-таблица, пространственная сложность O(n)\n    Map<Integer, Integer> dic = new HashMap<>();\n    // Один цикл, временная сложность O(n)\n    for (int i = 0; i < size; i++) {\n        if (dic.containsKey(target - nums[i])) {\n            return new int[] { dic.get(target - nums[i]), i };\n        }\n        dic.put(nums[i], i);\n    }\n    return new int[0];\n}\n
        two_sum.cs
        /* Метод 2: вспомогательная хеш-таблица */\nint[] TwoSumHashTable(int[] nums, int target) {\n    int size = nums.Length;\n    // Вспомогательная хеш-таблица, пространственная сложность O(n)\n    Dictionary<int, int> dic = [];\n    // Один цикл, временная сложность O(n)\n    for (int i = 0; i < size; i++) {\n        if (dic.ContainsKey(target - nums[i])) {\n            return [dic[target - nums[i]], i];\n        }\n        dic.Add(nums[i], i);\n    }\n    return [];\n}\n
        two_sum.go
        /* Метод 2: вспомогательная хеш-таблица */\nfunc twoSumHashTable(nums []int, target int) []int {\n    // Вспомогательная хеш-таблица, пространственная сложность O(n)\n    hashTable := map[int]int{}\n    // Один цикл, временная сложность O(n)\n    for idx, val := range nums {\n        if preIdx, ok := hashTable[target-val]; ok {\n            return []int{preIdx, idx}\n        }\n        hashTable[val] = idx\n    }\n    return nil\n}\n
        two_sum.swift
        /* Метод 2: вспомогательная хеш-таблица */\nfunc twoSumHashTable(nums: [Int], target: Int) -> [Int] {\n    // Вспомогательная хеш-таблица, пространственная сложность O(n)\n    var dic: [Int: Int] = [:]\n    // Один цикл, временная сложность O(n)\n    for i in nums.indices {\n        if let j = dic[target - nums[i]] {\n            return [j, i]\n        }\n        dic[nums[i]] = i\n    }\n    return [0]\n}\n
        two_sum.js
        /* Метод 2: вспомогательная хеш-таблица */\nfunction twoSumHashTable(nums, target) {\n    // Вспомогательная хеш-таблица, пространственная сложность O(n)\n    let m = {};\n    // Один цикл, временная сложность O(n)\n    for (let i = 0; i < nums.length; i++) {\n        if (m[target - nums[i]] !== undefined) {\n            return [m[target - nums[i]], i];\n        } else {\n            m[nums[i]] = i;\n        }\n    }\n    return [];\n}\n
        two_sum.ts
        /* Метод 2: вспомогательная хеш-таблица */\nfunction twoSumHashTable(nums: number[], target: number): number[] {\n    // Вспомогательная хеш-таблица, пространственная сложность O(n)\n    let m: Map<number, number> = new Map();\n    // Один цикл, временная сложность O(n)\n    for (let i = 0; i < nums.length; i++) {\n        let index = m.get(target - nums[i]);\n        if (index !== undefined) {\n            return [index, i];\n        } else {\n            m.set(nums[i], i);\n        }\n    }\n    return [];\n}\n
        two_sum.dart
        /* Способ 2: вспомогательная хеш-таблица */\nList<int> twoSumHashTable(List<int> nums, int target) {\n  int size = nums.length;\n  // Вспомогательная хеш-таблица, пространственная сложность O(n)\n  Map<int, int> dic = HashMap();\n  // Один цикл, временная сложность O(n)\n  for (var i = 0; i < size; i++) {\n    if (dic.containsKey(target - nums[i])) {\n      return [dic[target - nums[i]]!, i];\n    }\n    dic.putIfAbsent(nums[i], () => i);\n  }\n  return [0];\n}\n
        two_sum.rs
        /* Метод 2: вспомогательная хеш-таблица */\npub fn two_sum_hash_table(nums: &Vec<i32>, target: i32) -> Option<Vec<i32>> {\n    // Вспомогательная хеш-таблица, пространственная сложность O(n)\n    let mut dic = HashMap::new();\n    // Один цикл, временная сложность O(n)\n    for (i, num) in nums.iter().enumerate() {\n        match dic.get(&(target - num)) {\n            Some(v) => return Some(vec![*v as i32, i as i32]),\n            None => dic.insert(num, i as i32),\n        };\n    }\n    None\n}\n
        two_sum.c
        /* Хеш-таблица */\ntypedef struct {\n    int key;\n    int val;\n    UT_hash_handle hh; // Реализовано на основе uthash.h\n} HashTable;\n\n/* Поиск в хеш-таблице */\nHashTable *find(HashTable *h, int key) {\n    HashTable *tmp;\n    HASH_FIND_INT(h, &key, tmp);\n    return tmp;\n}\n\n/* Вставка элемента в хеш-таблицу */\nvoid insert(HashTable **h, int key, int val) {\n    HashTable *t = find(*h, key);\n    if (t == NULL) {\n        HashTable *tmp = malloc(sizeof(HashTable));\n        tmp->key = key, tmp->val = val;\n        HASH_ADD_INT(*h, key, tmp);\n    } else {\n        t->val = val;\n    }\n}\n\n/* Метод 2: вспомогательная хеш-таблица */\nint *twoSumHashTable(int *nums, int numsSize, int target, int *returnSize) {\n    HashTable *hashtable = NULL;\n    for (int i = 0; i < numsSize; i++) {\n        HashTable *t = find(hashtable, target - nums[i]);\n        if (t != NULL) {\n            int *res = malloc(sizeof(int) * 2);\n            res[0] = t->val, res[1] = i;\n            *returnSize = 2;\n            return res;\n        }\n        insert(&hashtable, nums[i], i);\n    }\n    *returnSize = 0;\n    return NULL;\n}\n
        two_sum.kt
        /* Метод 2: вспомогательная хеш-таблица */\nfun twoSumHashTable(nums: IntArray, target: Int): IntArray {\n    val size = nums.size\n    // Вспомогательная хеш-таблица, пространственная сложность O(n)\n    val dic = HashMap<Int, Int>()\n    // Один цикл, временная сложность O(n)\n    for (i in 0..<size) {\n        if (dic.containsKey(target - nums[i])) {\n            return intArrayOf(dic[target - nums[i]]!!, i)\n        }\n        dic[nums[i]] = i\n    }\n    return IntArray(0)\n}\n
        two_sum.rb
        ### Метод 2: вспомогательная хеш-таблица ###\ndef two_sum_hash_table(nums, target)\n  # Вспомогательная хеш-таблица, пространственная сложность O(n)\n  dic = {}\n  # Один цикл, временная сложность O(n)\n  for i in 0...nums.length\n    return [dic[target - nums[i]], i] if dic.has_key?(target - nums[i])\n\n    dic[nums[i]] = i\n  end\n\n  []\nend\n
        Визуализация кода

        Во весь экран >

        Благодаря хеш-поиску этот метод снижает временную сложность с \\(O(n^2)\\) до \\(O(n)\\) , существенно повышая эффективность работы.

        Поскольку требуется поддерживать дополнительную хеш-таблицу, пространственная сложность составляет \\(O(n)\\) . Несмотря на это, в целом данный метод лучше сбалансирован по времени и памяти, поэтому именно он является оптимальным решением этой задачи.

        ","path":["Глава 10. Поиск","10.4   Стратегии оптимизации хеширования"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/","level":1,"title":"10.5   Переосмысление алгоритмов поиска","text":"

        Алгоритмы поиска (searching algorithm) используются для того, чтобы находить один или несколько элементов, удовлетворяющих определенным условиям, в структурах данных, таких как массивы, списки, деревья или графы.

        Алгоритмы поиска можно разделить на две категории по способу реализации.

        • Поиск целевого элемента путем обхода структуры данных, например обход массива, списка, дерева или графа.
        • Эффективный поиск элементов с использованием структуры организации данных или априорной информации, например двоичный поиск, хеш-поиск и поиск в двоичном дереве поиска.

        Нетрудно заметить, что эти темы уже рассматривались в предыдущих главах, поэтому алгоритмы поиска нам уже знакомы. В этом разделе мы систематизируем полученные ранее знания и еще раз посмотрим на них как на единую группу методов.

        ","path":["Глава 10. Поиск","10.5   Переосмысление алгоритмов поиска"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/#1051","level":2,"title":"10.5.1   Полный перебор","text":"

        Полный перебор заключается в том, что мы обходим каждый элемент структуры данных, чтобы найти целевой элемент.

        • «Линейный поиск» применяется к линейным структурам данных, таким как массивы и списки. Он начинается с одного конца структуры данных и последовательно проверяет элементы, пока не найдет целевой элемент или пока не достигнет другого конца структуры данных.
        • «Обход в ширину» и «обход в глубину» - это две стратегии обхода графов и деревьев. Обход в ширину стартует из начального узла и исследует все узлы текущего уровня, прежде чем переходить к следующему. Обход в глубину стартует из начального узла, проходит один путь до конца, затем возвращается назад и пробует другие пути, пока не будет полностью пройдена вся структура данных.

        Преимущество полного перебора состоит в его простоте и универсальности, поскольку он не требует предварительной обработки данных и использования дополнительных структур данных.

        Однако временная сложность таких алгоритмов равна \\(O(n)\\) , где \\(n\\) - число элементов, поэтому при больших объемах данных их производительность невысока.

        ","path":["Глава 10. Поиск","10.5   Переосмысление алгоритмов поиска"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/#1052","level":2,"title":"10.5.2   Адаптивный поиск","text":"

        Адаптивный поиск использует специфические свойства данных (например, упорядоченность), чтобы оптимизировать процесс поиска и тем самым эффективнее находить целевой элемент.

        • «Двоичный поиск» использует упорядоченность данных для эффективного поиска и применим только к массивам.
        • «Хеш-поиск» использует хеш-таблицу для построения отображения между поисковыми данными и целевыми данными, благодаря чему запросы выполняются эффективно.
        • «Поиск в дереве» ведется в конкретной древовидной структуре (например, в двоичном дереве поиска) и позволяет быстро отсекать узлы на основе сравнения значений, чтобы найти цель.

        Преимущество этих алгоритмов заключается в высокой эффективности: их временная сложность может достигать \\(O(\\log n)\\) и даже \\(O(1)\\) .

        Однако для использования таких алгоритмов обычно требуется предварительная обработка данных. Например, для двоичного поиска нужно заранее отсортировать массив, а хеш-поиск и поиск в дереве требуют дополнительных структур данных, поддержание которых тоже отнимает время и память.

        Tip

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

        ","path":["Глава 10. Поиск","10.5   Переосмысление алгоритмов поиска"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/#1053","level":2,"title":"10.5.3   Выбор метода поиска","text":"

        Для поиска целевого элемента в наборе данных размера \\(n\\) можно использовать линейный поиск, двоичный поиск, поиск в дереве, хеш-поиск и другие методы. Принципы работы этих методов показаны на рисунке 10-11.

        Рисунок 10-11   Различные стратегии поиска

        Эффективность и особенности перечисленных методов приведены в таблице 10-1.

        Таблица 10-1   Сравнение эффективности алгоритмов поиска

        Линейный поиск Двоичный поиск Поиск в дереве Хеш-поиск Поиск элемента \\(O(n)\\) \\(O(\\log n)\\) \\(O(\\log n)\\) \\(O(1)\\) Вставка элемента \\(O(1)\\) \\(O(n)\\) \\(O(\\log n)\\) \\(O(1)\\) Удаление элемента \\(O(n)\\) \\(O(n)\\) \\(O(\\log n)\\) \\(O(1)\\) Дополнительное пространство \\(O(1)\\) \\(O(1)\\) \\(O(n)\\) \\(O(n)\\) Предварительная обработка / Сортировка \\(O(n \\log n)\\) Построение дерева \\(O(n \\log n)\\) Построение хеш-таблицы \\(O(n)\\) Упорядоченность данных Не требуется Требуется Требуется Не требуется

        Выбор алгоритма поиска также зависит от масштаба данных, требований к производительности поиска, а также частоты запросов и обновлений данных.

        Линейный поиск

        • Обладает хорошей универсальностью и не требует никакой предварительной обработки данных. Если нужно выполнить только один запрос, то время предварительной обработки для остальных трех методов окажется больше, чем время линейного поиска.
        • Подходит для небольших объемов данных, потому что в этом случае влияние временной сложности на эффективность невелико.
        • Подходит для сценариев с высокой частотой обновления данных, поскольку этот метод не требует никакого дополнительного обслуживания данных.

        Двоичный поиск

        • Подходит для больших наборов данных и демонстрирует стабильную эффективность. Его худшая временная сложность равна \\(O(\\log n)\\) .
        • Объем данных не должен быть слишком большим, потому что массив требует непрерывного участка памяти.
        • Не подходит для сценариев с частыми вставками и удалениями данных, так как поддержание массива в отсортированном виде требует больших затрат.

        Хеш-поиск

        • Подходит для сценариев, в которых требования к скорости запросов очень высоки. Средняя временная сложность равна \\(O(1)\\) .
        • Не подходит для сценариев, где требуется упорядоченность данных или поиск по диапазону, потому что хеш-таблица не умеет поддерживать порядок данных.
        • Сильно зависит от хеш-функции и стратегии обработки коллизий, поэтому риск деградации производительности сравнительно велик.
        • Не подходит для слишком больших объемов данных, так как хеш-таблице требуется дополнительное пространство, чтобы максимально снизить число коллизий и обеспечить хорошую производительность поиска.

        Поиск в дереве

        • Подходит для очень больших объемов данных, потому что узлы дерева распределены в памяти и не требуют непрерывного хранения.
        • Подходит для сценариев, где нужно поддерживать упорядоченные данные или выполнять поиск по диапазону.
        • В процессе постоянных вставок и удалений узлов двоичное дерево поиска может перекоситься, и тогда временная сложность деградирует до \\(O(n)\\) .
        • Если использовать AVL-дерево или красно-черное дерево, то все операции могут стабильно выполняться за \\(O(\\log n)\\) , но поддержание баланса дерева увеличивает дополнительные накладные расходы.
        ","path":["Глава 10. Поиск","10.5   Переосмысление алгоритмов поиска"],"tags":[]},{"location":"chapter_searching/summary/","level":1,"title":"10.6   Резюме","text":"","path":["Глава 10. Поиск","10.6   Резюме"],"tags":[]},{"location":"chapter_searching/summary/#1","level":3,"title":"1.   Ключевые выводы","text":"
        • Двоичный поиск опирается на упорядоченность данных и выполняет поиск путем циклического сокращения интервала вдвое. Он требует упорядоченных входных данных и подходит только для массивов или структур данных, реализованных на их основе.
        • Полный перебор находит данные путем обхода структуры данных. Линейный поиск подходит для массивов и списков, а обход в ширину и обход в глубину подходят для графов и деревьев. Эти алгоритмы универсальны и не требуют предварительной обработки данных, но их временная сложность \\(O(n)\\) сравнительно велика.
        • Хеш-поиск, поиск в дереве и двоичный поиск относятся к эффективным методам поиска и позволяют быстро находить целевой элемент в конкретных структурах данных. Такие алгоритмы обладают высокой эффективностью, их временная сложность может достигать \\(O(\\log n)\\) и даже \\(O(1)\\) , но обычно им нужны дополнительные структуры данных.
        • На практике нужно анализировать размер данных, требования к производительности поиска, а также частоту запросов и обновлений данных, чтобы выбрать подходящий метод поиска.
        • Линейный поиск подходит для небольших или часто обновляемых наборов данных. Двоичный поиск - для больших отсортированных данных. Хеш-поиск - для сценариев с высокими требованиями к скорости запросов и без необходимости поиска по диапазону. Поиск в дереве - для больших динамических данных, где нужно поддерживать порядок и выполнять диапазонные запросы.
        • Замена линейного поиска на хеш-поиск - это распространенная стратегия ускорения, которая позволяет снизить временную сложность с \\(O(n)\\) до \\(O(1)\\) .
        ","path":["Глава 10. Поиск","10.6   Резюме"],"tags":[]},{"location":"chapter_sorting/","level":1,"title":"Глава 11.   Сортировка","text":"

        Abstract

        Сортировка упорядочивает хаотичные данные и позволяет быстрее находить закономерности.

        За кажущейся простотой скрывается целая группа алгоритмов с разными достоинствами и ограничениями.

        ","path":["Глава 11. Сортировка","Глава 11.   Сортировка"],"tags":[]},{"location":"chapter_sorting/#_1","level":2,"title":"Содержание главы","text":"
        • 11.1   Алгоритмы сортировки
        • 11.2   Сортировка выбором
        • 11.3   Сортировка пузырьком
        • 11.4   Сортировка вставками
        • 11.5   Быстрая сортировка
        • 11.6   Сортировка слиянием
        • 11.7   Пирамидальная сортировка
        • 11.8   Блочная сортировка
        • 11.9   Сортировка подсчетом
        • 11.10   Поразрядная сортировка
        • 11.11   Резюме
        ","path":["Глава 11. Сортировка","Глава 11.   Сортировка"],"tags":[]},{"location":"chapter_sorting/bubble_sort/","level":1,"title":"11.3   Сортировка пузырьком","text":"

        Сортировка пузырьком (bubble sort) реализует сортировку путем последовательного сравнения и обмена соседних элементов. Этот процесс напоминает всплытие пузырьков снизу вверх, откуда и произошло название алгоритма.

        Как показано на рисунке 11-4, процесс «всплытия» можно смоделировать через операцию обмена элементов: начиная от левого края массива и двигаясь вправо, мы последовательно сравниваем соседние элементы и, если «левый элемент > правый элемент», меняем их местами. После завершения прохода максимальный элемент будет перемещен в самый правый конец массива.

        <1><2><3><4><5><6><7>

        Рисунок 11-4   Моделирование пузырька через обмен элементов

        ","path":["Глава 11. Сортировка","11.3   Сортировка пузырьком"],"tags":[]},{"location":"chapter_sorting/bubble_sort/#1131","level":2,"title":"11.3.1   Алгоритм","text":"

        Пусть длина массива равна \\(n\\). Тогда шаги сортировки пузырьком показаны на рисунке 11-5.

        1. Сначала выполнить один проход «всплытия» по \\(n\\) элементам, переместив максимальный элемент массива на правильную позицию.
        2. Затем выполнить «всплытие» по оставшимся \\(n - 1\\) элементам, переместив второй по величине элемент на правильную позицию.
        3. Продолжать по аналогии. После \\(n - 1\\) раундов «всплытия» первые \\(n - 1\\) по величине элементы окажутся на правильных позициях.
        4. Оставшийся единственный элемент обязательно является минимальным, сортировать его уже не нужно, поэтому сортировка завершена.

        Рисунок 11-5   Процесс сортировки пузырьком

        Пример кода:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby bubble_sort.py
        def bubble_sort(nums: list[int]):\n    \"\"\"Пузырьковая сортировка\"\"\"\n    n = len(nums)\n    # Внешний цикл: неотсортированный диапазон [0, i]\n    for i in range(n - 1, 0, -1):\n        # Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for j in range(i):\n            if nums[j] > nums[j + 1]:\n                # Поменять местами nums[j] и nums[j + 1]\n                nums[j], nums[j + 1] = nums[j + 1], nums[j]\n
        bubble_sort.cpp
        /* Пузырьковая сортировка */\nvoid bubbleSort(vector<int> &nums) {\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (int i = nums.size() - 1; i > 0; i--) {\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Поменять местами nums[j] и nums[j + 1]\n                // Здесь используется функция std::swap()\n                swap(nums[j], nums[j + 1]);\n            }\n        }\n    }\n}\n
        bubble_sort.java
        /* Пузырьковая сортировка */\nvoid bubbleSort(int[] nums) {\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (int i = nums.length - 1; i > 0; i--) {\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Поменять местами nums[j] и nums[j + 1]\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n            }\n        }\n    }\n}\n
        bubble_sort.cs
        /* Пузырьковая сортировка */\nvoid BubbleSort(int[] nums) {\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (int i = nums.Length - 1; i > 0; i--) {\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Поменять местами nums[j] и nums[j + 1]\n                (nums[j + 1], nums[j]) = (nums[j], nums[j + 1]);\n            }\n        }\n    }\n}\n
        bubble_sort.go
        /* Пузырьковая сортировка */\nfunc bubbleSort(nums []int) {\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for i := len(nums) - 1; i > 0; i-- {\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for j := 0; j < i; j++ {\n            if nums[j] > nums[j+1] {\n                // Поменять местами nums[j] и nums[j + 1]\n                nums[j], nums[j+1] = nums[j+1], nums[j]\n            }\n        }\n    }\n}\n
        bubble_sort.swift
        /* Пузырьковая сортировка */\nfunc bubbleSort(nums: inout [Int]) {\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for i in nums.indices.dropFirst().reversed() {\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for j in 0 ..< i {\n            if nums[j] > nums[j + 1] {\n                // Поменять местами nums[j] и nums[j + 1]\n                nums.swapAt(j, j + 1)\n            }\n        }\n    }\n}\n
        bubble_sort.js
        /* Пузырьковая сортировка */\nfunction bubbleSort(nums) {\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Поменять местами nums[j] и nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n            }\n        }\n    }\n}\n
        bubble_sort.ts
        /* Пузырьковая сортировка */\nfunction bubbleSort(nums: number[]): void {\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Поменять местами nums[j] и nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n            }\n        }\n    }\n}\n
        bubble_sort.dart
        /* Пузырьковая сортировка */\nvoid bubbleSort(List<int> nums) {\n  // Внешний цикл: неотсортированный диапазон [0, i]\n  for (int i = nums.length - 1; i > 0; i--) {\n    // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n    for (int j = 0; j < i; j++) {\n      if (nums[j] > nums[j + 1]) {\n        // Поменять местами nums[j] и nums[j + 1]\n        int tmp = nums[j];\n        nums[j] = nums[j + 1];\n        nums[j + 1] = tmp;\n      }\n    }\n  }\n}\n
        bubble_sort.rs
        /* Пузырьковая сортировка */\nfn bubble_sort(nums: &mut [i32]) {\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for i in (1..nums.len()).rev() {\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for j in 0..i {\n            if nums[j] > nums[j + 1] {\n                // Поменять местами nums[j] и nums[j + 1]\n                nums.swap(j, j + 1);\n            }\n        }\n    }\n}\n
        bubble_sort.c
        /* Пузырьковая сортировка */\nvoid bubbleSort(int nums[], int size) {\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (int i = size - 1; i > 0; i--) {\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                int temp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = temp;\n            }\n        }\n    }\n}\n
        bubble_sort.kt
        /* Пузырьковая сортировка */\nfun bubbleSort(nums: IntArray) {\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (i in nums.size - 1 downTo 1) {\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (j in 0..<i) {\n            if (nums[j] > nums[j + 1]) {\n                // Поменять местами nums[j] и nums[j + 1]\n                val temp = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = temp\n            }\n        }\n    }\n}\n
        bubble_sort.rb
        ### Пузырьковая сортировка ###\ndef bubble_sort(nums)\n  n = nums.length\n  # Внешний цикл: неотсортированный диапазон [0, i]\n  for i in (n - 1).downto(1)\n    # Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # Поменять местами nums[j] и nums[j + 1]\n        nums[j], nums[j + 1] = nums[j + 1], nums[j]\n      end\n    end\n  end\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 11. Сортировка","11.3   Сортировка пузырьком"],"tags":[]},{"location":"chapter_sorting/bubble_sort/#1132","level":2,"title":"11.3.2   Оптимизация эффективности","text":"

        Если в каком-либо раунде «всплытия» не произошло ни одного обмена, значит, массив уже отсортирован и можно сразу вернуть результат. Поэтому можно добавить флаг flag для отслеживания этой ситуации и немедленного выхода.

        После такой оптимизации худшая и средняя временные сложности сортировки пузырьком по-прежнему равны \\(O(n^2)\\). Однако если входной массив уже полностью упорядочен, достигается лучшая временная сложность \\(O(n)\\) .

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby bubble_sort.py
        def bubble_sort_with_flag(nums: list[int]):\n    \"\"\"Пузырьковая сортировка (оптимизация флагом)\"\"\"\n    n = len(nums)\n    # Внешний цикл: неотсортированный диапазон [0, i]\n    for i in range(n - 1, 0, -1):\n        flag = False  # Инициализировать флаг\n        # Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for j in range(i):\n            if nums[j] > nums[j + 1]:\n                # Поменять местами nums[j] и nums[j + 1]\n                nums[j], nums[j + 1] = nums[j + 1], nums[j]\n                flag = True  # Записать обмен элементов\n        if not flag:\n            break  # На этой итерации «всплытия» не было ни одного обмена, сразу выйти\n
        bubble_sort.cpp
        /* Пузырьковая сортировка (оптимизация флагом) */\nvoid bubbleSortWithFlag(vector<int> &nums) {\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (int i = nums.size() - 1; i > 0; i--) {\n        bool flag = false; // Инициализировать флаг\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Поменять местами nums[j] и nums[j + 1]\n                // Здесь используется функция std::swap()\n                swap(nums[j], nums[j + 1]);\n                flag = true; // Записать обмен элементов\n            }\n        }\n        if (!flag)\n            break; // На этой итерации «всплытия» не было ни одного обмена, сразу выйти\n    }\n}\n
        bubble_sort.java
        /* Пузырьковая сортировка (оптимизация флагом) */\nvoid bubbleSortWithFlag(int[] nums) {\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (int i = nums.length - 1; i > 0; i--) {\n        boolean flag = false; // Инициализировать флаг\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Поменять местами nums[j] и nums[j + 1]\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                flag = true; // Записать обмен элементов\n            }\n        }\n        if (!flag)\n            break; // На этой итерации «всплытия» не было ни одного обмена, сразу выйти\n    }\n}\n
        bubble_sort.cs
        /* Пузырьковая сортировка (оптимизация флагом) */\nvoid BubbleSortWithFlag(int[] nums) {\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (int i = nums.Length - 1; i > 0; i--) {\n        bool flag = false; // Инициализировать флаг\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Поменять местами nums[j] и nums[j + 1]\n                (nums[j + 1], nums[j]) = (nums[j], nums[j + 1]);\n                flag = true;  // Записать обмен элементов\n            }\n        }\n        if (!flag) break;     // На этой итерации «всплытия» не было ни одного обмена, сразу выйти\n    }\n}\n
        bubble_sort.go
        /* Пузырьковая сортировка (оптимизация флагом) */\nfunc bubbleSortWithFlag(nums []int) {\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for i := len(nums) - 1; i > 0; i-- {\n        flag := false // Инициализировать флаг\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for j := 0; j < i; j++ {\n            if nums[j] > nums[j+1] {\n                // Поменять местами nums[j] и nums[j + 1]\n                nums[j], nums[j+1] = nums[j+1], nums[j]\n                flag = true // Записать обмен элементов\n            }\n        }\n        if flag == false { // На этой итерации «всплытия» не было ни одного обмена, сразу выйти\n            break\n        }\n    }\n}\n
        bubble_sort.swift
        /* Пузырьковая сортировка (оптимизация флагом) */\nfunc bubbleSortWithFlag(nums: inout [Int]) {\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for i in nums.indices.dropFirst().reversed() {\n        var flag = false // Инициализировать флаг\n        for j in 0 ..< i {\n            if nums[j] > nums[j + 1] {\n                // Поменять местами nums[j] и nums[j + 1]\n                nums.swapAt(j, j + 1)\n                flag = true // Записать обмен элементов\n            }\n        }\n        if !flag { // На этой итерации «всплытия» не было ни одного обмена, сразу выйти\n            break\n        }\n    }\n}\n
        bubble_sort.js
        /* Пузырьковая сортировка (оптимизация флагом) */\nfunction bubbleSortWithFlag(nums) {\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        let flag = false; // Инициализировать флаг\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Поменять местами nums[j] и nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                flag = true; // Записать обмен элементов\n            }\n        }\n        if (!flag) break; // На этой итерации «всплытия» не было ни одного обмена, сразу выйти\n    }\n}\n
        bubble_sort.ts
        /* Пузырьковая сортировка (оптимизация флагом) */\nfunction bubbleSortWithFlag(nums: number[]): void {\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        let flag = false; // Инициализировать флаг\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Поменять местами nums[j] и nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                flag = true; // Записать обмен элементов\n            }\n        }\n        if (!flag) break; // На этой итерации «всплытия» не было ни одного обмена, сразу выйти\n    }\n}\n
        bubble_sort.dart
        /* Пузырьковая сортировка (оптимизация флагом) */\nvoid bubbleSortWithFlag(List<int> nums) {\n  // Внешний цикл: неотсортированный диапазон [0, i]\n  for (int i = nums.length - 1; i > 0; i--) {\n    bool flag = false; // Инициализировать флаг\n    // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n    for (int j = 0; j < i; j++) {\n      if (nums[j] > nums[j + 1]) {\n        // Поменять местами nums[j] и nums[j + 1]\n        int tmp = nums[j];\n        nums[j] = nums[j + 1];\n        nums[j + 1] = tmp;\n        flag = true; // Записать обмен элементов\n      }\n    }\n    if (!flag) break; // На этой итерации «всплытия» не было ни одного обмена, сразу выйти\n  }\n}\n
        bubble_sort.rs
        /* Пузырьковая сортировка (оптимизация флагом) */\nfn bubble_sort_with_flag(nums: &mut [i32]) {\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for i in (1..nums.len()).rev() {\n        let mut flag = false; // Инициализировать флаг\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for j in 0..i {\n            if nums[j] > nums[j + 1] {\n                // Поменять местами nums[j] и nums[j + 1]\n                nums.swap(j, j + 1);\n                flag = true; // Записать обмен элементов\n            }\n        }\n        if !flag {\n            break; // На этой итерации «всплытия» не было ни одного обмена, сразу выйти\n        };\n    }\n}\n
        bubble_sort.c
        /* Пузырьковая сортировка (оптимизация флагом) */\nvoid bubbleSortWithFlag(int nums[], int size) {\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (int i = size - 1; i > 0; i--) {\n        bool flag = false;\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                int temp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = temp;\n                flag = true;\n            }\n        }\n        if (!flag)\n            break;\n    }\n}\n
        bubble_sort.kt
        /* Пузырьковая сортировка (оптимизация флагом) */\nfun bubbleSortWithFlag(nums: IntArray) {\n    // Внешний цикл: неотсортированный диапазон [0, i]\n    for (i in nums.size - 1 downTo 1) {\n        var flag = false // Инициализировать флаг\n        // Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n        for (j in 0..<i) {\n            if (nums[j] > nums[j + 1]) {\n                // Поменять местами nums[j] и nums[j + 1]\n                val temp = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = temp\n                flag = true // Записать обмен элементов\n            }\n        }\n        if (!flag) break // На этой итерации «всплытия» не было ни одного обмена, сразу выйти\n    }\n}\n
        bubble_sort.rb
        ### Пузырьковая сортировка ###\ndef bubble_sort(nums)\n  n = nums.length\n  # Внешний цикл: неотсортированный диапазон [0, i]\n  for i in (n - 1).downto(1)\n    # Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # Поменять местами nums[j] и nums[j + 1]\n        nums[j], nums[j + 1] = nums[j + 1], nums[j]\n      end\n    end\n  end\nend\n\n# ## Пузырьковая сортировка (оптимизация флагом) ###\ndef bubble_sort_with_flag(nums)\n  n = nums.length\n  # Внешний цикл: неотсортированный диапазон [0, i]\n  for i in (n - 1).downto(1)\n    flag = false # Инициализировать флаг\n\n    # Внутренний цикл: переместить максимальный элемент неотсортированного диапазона [0, i] в его правый конец\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # Поменять местами nums[j] и nums[j + 1]\n        nums[j], nums[j + 1] = nums[j + 1], nums[j]\n        flag = true # Записать обмен элементов\n      end\n    end\n\n    break unless flag # На этой итерации «всплытия» не было ни одного обмена, сразу выйти\n  end\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 11. Сортировка","11.3   Сортировка пузырьком"],"tags":[]},{"location":"chapter_sorting/bubble_sort/#1133","level":2,"title":"11.3.3   Характеристики алгоритма","text":"
        • Временная сложность равна \\(O(n^2)\\), алгоритм адаптивен: длины диапазонов, проходящих «всплытие» в разных раундах, последовательно равны \\(n - 1\\), \\(n - 2\\), \\(\\dots\\), \\(2\\), \\(1\\) , а их сумма равна \\((n - 1) n / 2\\) . После добавления оптимизации с flag лучшая временная сложность может достигать \\(O(n)\\) .
        • Пространственная сложность равна \\(O(1)\\), сортировка выполняется на месте: указатели \\(i\\) и \\(j\\) используют константный объем дополнительной памяти.
        • Стабильная сортировка: поскольку при «всплытии» равные элементы не обмениваются местами.
        ","path":["Глава 11. Сортировка","11.3   Сортировка пузырьком"],"tags":[]},{"location":"chapter_sorting/bucket_sort/","level":1,"title":"11.8   Блочная сортировка","text":"

        Рассмотренные выше алгоритмы сортировки относятся к «сортировкам на основе сравнений»: они упорядочивают данные, сравнивая элементы друг с другом. Временная сложность таких алгоритмов не может быть лучше \\(O(n \\log n)\\) . Далее мы рассмотрим несколько «сортировок без сравнений», чья временная сложность может достигать линейного порядка.

        Блочная сортировка (bucket sort) является типичным применением стратегии «разделяй и властвуй». Она создает набор упорядоченных по величине блоков, где каждый блок соответствует определенному диапазону данных. Затем элементы равномерно распределяются по этим блокам, внутри каждого блока отдельно выполняется сортировка, а в конце результаты объединяются в порядке блоков.

        ","path":["Глава 11. Сортировка","11.8   Блочная сортировка"],"tags":[]},{"location":"chapter_sorting/bucket_sort/#1181","level":2,"title":"11.8.1   Алгоритм","text":"

        Рассмотрим массив длины \\(n\\), элементы которого являются числами с плавающей запятой из диапазона \\([0, 1)\\) . Процесс блочной сортировки показан на рисунке 11-13.

        1. Инициализировать \\(k\\) блоков и распределить \\(n\\) элементов по этим \\(k\\) блокам.
        2. Отсортировать каждый блок по отдельности (здесь используется встроенная функция сортировки языка программирования).
        3. Объединить результаты в порядке следования блоков от меньшего к большему.

        Рисунок 11-13   Процесс блочной сортировки

        Код приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby bucket_sort.py
        def bucket_sort(nums: list[float]):\n    \"\"\"Сортировка корзинами\"\"\"\n    # Инициализировать k = n/2 корзин, предполагая распределение 2 элементов в каждую корзину\n    k = len(nums) // 2\n    buckets = [[] for _ in range(k)]\n    # 1. Распределить элементы массива по корзинам\n    for num in nums:\n        # Входные данные лежат в диапазоне [0, 1); использовать num * k для отображения в диапазон индексов [0, k-1]\n        i = int(num * k)\n        # Добавить num в корзину i\n        buckets[i].append(num)\n    # 2. Выполнить сортировку внутри каждой корзины\n    for bucket in buckets:\n        # Использовать встроенную функцию сортировки; ее также можно заменить другим алгоритмом сортировки\n        bucket.sort()\n    # 3. Обойти корзины и объединить результаты\n    i = 0\n    for bucket in buckets:\n        for num in bucket:\n            nums[i] = num\n            i += 1\n
        bucket_sort.cpp
        /* Сортировка корзинами */\nvoid bucketSort(vector<float> &nums) {\n    // Инициализировать k = n/2 корзин, предполагая распределение 2 элементов в каждую корзину\n    int k = nums.size() / 2;\n    vector<vector<float>> buckets(k);\n    // 1. Распределить элементы массива по корзинам\n    for (float num : nums) {\n        // Входные данные лежат в диапазоне [0, 1); использовать num * k для отображения в диапазон индексов [0, k-1]\n        int i = num * k;\n        // Добавить num в корзину bucket_idx\n        buckets[i].push_back(num);\n    }\n    // 2. Выполнить сортировку внутри каждой корзины\n    for (vector<float> &bucket : buckets) {\n        // Использовать встроенную функцию сортировки; ее также можно заменить другим алгоритмом сортировки\n        sort(bucket.begin(), bucket.end());\n    }\n    // 3. Обойти корзины и объединить результаты\n    int i = 0;\n    for (vector<float> &bucket : buckets) {\n        for (float num : bucket) {\n            nums[i++] = num;\n        }\n    }\n}\n
        bucket_sort.java
        /* Сортировка корзинами */\nvoid bucketSort(float[] nums) {\n    // Инициализировать k = n/2 корзин, предполагая распределение 2 элементов в каждую корзину\n    int k = nums.length / 2;\n    List<List<Float>> buckets = new ArrayList<>();\n    for (int i = 0; i < k; i++) {\n        buckets.add(new ArrayList<>());\n    }\n    // 1. Распределить элементы массива по корзинам\n    for (float num : nums) {\n        // Входные данные лежат в диапазоне [0, 1); использовать num * k для отображения в диапазон индексов [0, k-1]\n        int i = (int) (num * k);\n        // Добавить num в корзину i\n        buckets.get(i).add(num);\n    }\n    // 2. Выполнить сортировку внутри каждой корзины\n    for (List<Float> bucket : buckets) {\n        // Использовать встроенную функцию сортировки; ее также можно заменить другим алгоритмом сортировки\n        Collections.sort(bucket);\n    }\n    // 3. Обойти корзины и объединить результаты\n    int i = 0;\n    for (List<Float> bucket : buckets) {\n        for (float num : bucket) {\n            nums[i++] = num;\n        }\n    }\n}\n
        bucket_sort.cs
        /* Сортировка корзинами */\nvoid BucketSort(float[] nums) {\n    // Инициализировать k = n/2 корзин, предполагая распределение 2 элементов в каждую корзину\n    int k = nums.Length / 2;\n    List<List<float>> buckets = [];\n    for (int i = 0; i < k; i++) {\n        buckets.Add([]);\n    }\n    // 1. Распределить элементы массива по корзинам\n    foreach (float num in nums) {\n        // Входные данные лежат в диапазоне [0, 1); использовать num * k для отображения в диапазон индексов [0, k-1]\n        int i = (int)(num * k);\n        // Добавить num в корзину i\n        buckets[i].Add(num);\n    }\n    // 2. Выполнить сортировку внутри каждой корзины\n    foreach (List<float> bucket in buckets) {\n        // Использовать встроенную функцию сортировки; ее также можно заменить другим алгоритмом сортировки\n        bucket.Sort();\n    }\n    // 3. Обойти корзины и объединить результаты\n    int j = 0;\n    foreach (List<float> bucket in buckets) {\n        foreach (float num in bucket) {\n            nums[j++] = num;\n        }\n    }\n}\n
        bucket_sort.go
        /* Сортировка корзинами */\nfunc bucketSort(nums []float64) {\n    // Инициализировать k = n/2 корзин, предполагая распределение 2 элементов в каждую корзину\n    k := len(nums) / 2\n    buckets := make([][]float64, k)\n    for i := 0; i < k; i++ {\n        buckets[i] = make([]float64, 0)\n    }\n    // 1. Распределить элементы массива по корзинам\n    for _, num := range nums {\n        // Входные данные лежат в диапазоне [0, 1); использовать num * k для отображения в диапазон индексов [0, k-1]\n        i := int(num * float64(k))\n        // Добавить num в корзину i\n        buckets[i] = append(buckets[i], num)\n    }\n    // 2. Выполнить сортировку внутри каждой корзины\n    for i := 0; i < k; i++ {\n        // Использовать встроенную функцию сортировки среза; ее также можно заменить другим алгоритмом сортировки\n        sort.Float64s(buckets[i])\n    }\n    // 3. Обойти корзины и объединить результаты\n    i := 0\n    for _, bucket := range buckets {\n        for _, num := range bucket {\n            nums[i] = num\n            i++\n        }\n    }\n}\n
        bucket_sort.swift
        /* Сортировка корзинами */\nfunc bucketSort(nums: inout [Double]) {\n    // Инициализировать k = n/2 корзин, предполагая распределение 2 элементов в каждую корзину\n    let k = nums.count / 2\n    var buckets = (0 ..< k).map { _ in [Double]() }\n    // 1. Распределить элементы массива по корзинам\n    for num in nums {\n        // Входные данные лежат в диапазоне [0, 1); использовать num * k для отображения в диапазон индексов [0, k-1]\n        let i = Int(num * Double(k))\n        // Добавить num в корзину i\n        buckets[i].append(num)\n    }\n    // 2. Выполнить сортировку внутри каждой корзины\n    for i in buckets.indices {\n        // Использовать встроенную функцию сортировки; ее также можно заменить другим алгоритмом сортировки\n        buckets[i].sort()\n    }\n    // 3. Обойти корзины и объединить результаты\n    var i = nums.startIndex\n    for bucket in buckets {\n        for num in bucket {\n            nums[i] = num\n            i += 1\n        }\n    }\n}\n
        bucket_sort.js
        /* Сортировка корзинами */\nfunction bucketSort(nums) {\n    // Инициализировать k = n/2 корзин, предполагая распределение 2 элементов в каждую корзину\n    const k = nums.length / 2;\n    const buckets = [];\n    for (let i = 0; i < k; i++) {\n        buckets.push([]);\n    }\n    // 1. Распределить элементы массива по корзинам\n    for (const num of nums) {\n        // Входные данные лежат в диапазоне [0, 1); использовать num * k для отображения в диапазон индексов [0, k-1]\n        const i = Math.floor(num * k);\n        // Добавить num в корзину i\n        buckets[i].push(num);\n    }\n    // 2. Выполнить сортировку внутри каждой корзины\n    for (const bucket of buckets) {\n        // Использовать встроенную функцию сортировки; ее также можно заменить другим алгоритмом сортировки\n        bucket.sort((a, b) => a - b);\n    }\n    // 3. Обойти корзины и объединить результаты\n    let i = 0;\n    for (const bucket of buckets) {\n        for (const num of bucket) {\n            nums[i++] = num;\n        }\n    }\n}\n
        bucket_sort.ts
        /* Сортировка корзинами */\nfunction bucketSort(nums: number[]): void {\n    // Инициализировать k = n/2 корзин, предполагая распределение 2 элементов в каждую корзину\n    const k = nums.length / 2;\n    const buckets: number[][] = [];\n    for (let i = 0; i < k; i++) {\n        buckets.push([]);\n    }\n    // 1. Распределить элементы массива по корзинам\n    for (const num of nums) {\n        // Входные данные лежат в диапазоне [0, 1); использовать num * k для отображения в диапазон индексов [0, k-1]\n        const i = Math.floor(num * k);\n        // Добавить num в корзину i\n        buckets[i].push(num);\n    }\n    // 2. Выполнить сортировку внутри каждой корзины\n    for (const bucket of buckets) {\n        // Использовать встроенную функцию сортировки; ее также можно заменить другим алгоритмом сортировки\n        bucket.sort((a, b) => a - b);\n    }\n    // 3. Обойти корзины и объединить результаты\n    let i = 0;\n    for (const bucket of buckets) {\n        for (const num of bucket) {\n            nums[i++] = num;\n        }\n    }\n}\n
        bucket_sort.dart
        /* Сортировка корзинами */\nvoid bucketSort(List<double> nums) {\n  // Инициализировать k = n/2 корзин, предполагая распределение 2 элементов в каждую корзину\n  int k = nums.length ~/ 2;\n  List<List<double>> buckets = List.generate(k, (index) => []);\n\n  // 1. Распределить элементы массива по корзинам\n  for (double _num in nums) {\n    // Входные данные находятся в диапазоне [0, 1), используем _num * k для отображения в диапазон индексов [0, k-1]\n    int i = (_num * k).toInt();\n    // Добавить _num в корзину bucket_idx\n    buckets[i].add(_num);\n  }\n  // 2. Выполнить сортировку внутри каждой корзины\n  for (List<double> bucket in buckets) {\n    bucket.sort();\n  }\n  // 3. Обойти корзины и объединить результаты\n  int i = 0;\n  for (List<double> bucket in buckets) {\n    for (double _num in bucket) {\n      nums[i++] = _num;\n    }\n  }\n}\n
        bucket_sort.rs
        /* Сортировка корзинами */\nfn bucket_sort(nums: &mut [f64]) {\n    // Инициализировать k = n/2 корзин, предполагая распределение 2 элементов в каждую корзину\n    let k = nums.len() / 2;\n    let mut buckets = vec![vec![]; k];\n    // 1. Распределить элементы массива по корзинам\n    for &num in nums.iter() {\n        // Входные данные лежат в диапазоне [0, 1); использовать num * k для отображения в диапазон индексов [0, k-1]\n        let i = (num * k as f64) as usize;\n        // Добавить num в корзину i\n        buckets[i].push(num);\n    }\n    // 2. Выполнить сортировку внутри каждой корзины\n    for bucket in &mut buckets {\n        // Использовать встроенную функцию сортировки; ее также можно заменить другим алгоритмом сортировки\n        bucket.sort_by(|a, b| a.partial_cmp(b).unwrap());\n    }\n    // 3. Обойти корзины и объединить результаты\n    let mut i = 0;\n    for bucket in buckets.iter() {\n        for &num in bucket.iter() {\n            nums[i] = num;\n            i += 1;\n        }\n    }\n}\n
        bucket_sort.c
        /* Сортировка корзинами */\nvoid bucketSort(float nums[], int n) {\n    int k = n / 2;                                 // Инициализировать k = n/2 корзин\n    int *sizes = malloc(k * sizeof(int));          // Записать размер каждой корзины\n    float **buckets = malloc(k * sizeof(float *)); // Массив динамических массивов (корзины)\n    // Предварительно выделить достаточно места для каждой корзины\n    for (int i = 0; i < k; ++i) {\n        buckets[i] = (float *)malloc(n * sizeof(float));\n        sizes[i] = 0;\n    }\n    // 1. Распределить элементы массива по корзинам\n    for (int i = 0; i < n; ++i) {\n        int idx = (int)(nums[i] * k);\n        buckets[idx][sizes[idx]++] = nums[i];\n    }\n    // 2. Выполнить сортировку внутри каждой корзины\n    for (int i = 0; i < k; ++i) {\n        qsort(buckets[i], sizes[i], sizeof(float), compare);\n    }\n    // 3. Объединить отсортированные корзины\n    int idx = 0;\n    for (int i = 0; i < k; ++i) {\n        for (int j = 0; j < sizes[i]; ++j) {\n            nums[idx++] = buckets[i][j];\n        }\n        // Освободить память\n        free(buckets[i]);\n    }\n}\n
        bucket_sort.kt
        /* Сортировка корзинами */\nfun bucketSort(nums: FloatArray) {\n    // Инициализировать k = n/2 корзин, предполагая распределение 2 элементов в каждую корзину\n    val k = nums.size / 2\n    val buckets = mutableListOf<MutableList<Float>>()\n    for (i in 0..<k) {\n        buckets.add(mutableListOf())\n    }\n    // 1. Распределить элементы массива по корзинам\n    for (num in nums) {\n        // Входные данные лежат в диапазоне [0, 1); использовать num * k для отображения в диапазон индексов [0, k-1]\n        val i = (num * k).toInt()\n        // Добавить num в корзину i\n        buckets[i].add(num)\n    }\n    // 2. Выполнить сортировку внутри каждой корзины\n    for (bucket in buckets) {\n        // Использовать встроенную функцию сортировки; ее также можно заменить другим алгоритмом сортировки\n        bucket.sort()\n    }\n    // 3. Обойти корзины и объединить результаты\n    var i = 0\n    for (bucket in buckets) {\n        for (num in bucket) {\n            nums[i++] = num\n        }\n    }\n}\n
        bucket_sort.rb
        ### Сортировка корзинами ###\ndef bucket_sort(nums)\n  # Инициализировать k = n/2 корзин, предполагая распределение 2 элементов в каждую корзину\n  k = nums.length / 2\n  buckets = Array.new(k) { [] }\n\n  # 1. Распределить элементы массива по корзинам\n  nums.each do |num|\n    # Входные данные лежат в диапазоне [0, 1); использовать num * k для отображения в диапазон индексов [0, k-1]\n    i = (num * k).to_i\n    # Добавить num в корзину i\n    buckets[i] << num\n  end\n\n  # 2. Выполнить сортировку внутри каждой корзины\n  buckets.each do |bucket|\n    # Использовать встроенную функцию сортировки; ее также можно заменить другим алгоритмом сортировки\n    bucket.sort!\n  end\n\n  # 3. Обойти корзины и объединить результаты\n  i = 0\n  buckets.each do |bucket|\n    bucket.each do |num|\n      nums[i] = num\n      i += 1\n    end\n  end\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 11. Сортировка","11.8   Блочная сортировка"],"tags":[]},{"location":"chapter_sorting/bucket_sort/#1182","level":2,"title":"11.8.2   Характеристики алгоритма","text":"

        Блочная сортировка подходит для обработки очень больших объемов данных. Например, если вход содержит 1 миллион элементов и из-за ограничений памяти система не может загрузить их все сразу, можно разбить данные на 1000 блоков, отсортировать каждый блок отдельно, а затем объединить результаты.

        • Временная сложность равна \\(O(n + k)\\) : если элементы распределены по блокам равномерно, то в каждом блоке будет \\(\\frac{n}{k}\\) элементов. Если сортировка одного блока требует \\(O(\\frac{n}{k} \\log\\frac{n}{k})\\) времени, то сортировка всех блоков потребует \\(O(n \\log\\frac{n}{k})\\) времени. Когда число блоков \\(k\\) достаточно велико, временная сложность приближается к \\(O(n)\\) . На объединение результатов требуется \\(O(n + k)\\) времени, потому что нужно пройти по всем блокам и элементам. В худшем случае все данные попадут в один блок, и если сортировка этого блока использует \\(O(n^2)\\) времени, общая сложность также станет \\(O(n^2)\\) .
        • Пространственная сложность равна \\(O(n + k)\\), сортировка не выполняется на месте: требуются дополнительные блоки в количестве \\(k\\) и место для всех \\(n\\) элементов внутри них.
        • Является ли блочная сортировка стабильной, зависит от того, стабилен ли алгоритм сортировки внутри каждого блока.
        ","path":["Глава 11. Сортировка","11.8   Блочная сортировка"],"tags":[]},{"location":"chapter_sorting/bucket_sort/#1183","level":2,"title":"11.8.3   Как добиться равномерного распределения","text":"

        Теоретически временная сложность блочной сортировки может достигать \\(O(n)\\). Ключ к этому - как можно более равномерно распределить элементы по блокам. На практике данные часто распределены неравномерно. Например, если нужно распределить все товары на маркетплейсе по 10 ценовым блокам, количество товаров дешевле 100 рублей может быть очень большим, а товаров дороже 1000 рублей - очень маленьким. Если просто разбить диапазон цен на 10 равных частей, число товаров в каждом блоке будет сильно различаться.

        Чтобы добиться более равномерного распределения, можно сначала задать грубую линию раздела и приблизительно распределить данные по 3 блокам. После этого блоки с большим числом товаров можно снова делить на 3 блока и продолжать процесс до тех пор, пока число элементов в каждом блоке не станет примерно одинаковым.

        Как показано на рисунке 11-14, по сути этот метод строит рекурсивное дерево, цель которого - сделать значения в листьях как можно более равномерными. Конечно, совсем не обязательно каждый раз делить данные именно на 3 блока. Конкретную схему разбиения можно выбирать в зависимости от свойств данных.

        Рисунок 11-14   Рекурсивное разбиение по блокам

        Если нам заранее известна вероятностная модель распределения цен товаров, то границы цен для каждого блока можно задавать в соответствии с этим распределением. Важно отметить, что фактическое распределение данных не обязательно специально измерять - его можно приблизить некоторой вероятностной моделью исходя из свойств данных.

        Как показано на рисунке 11-15, если предположить, что цены товаров подчиняются нормальному распределению, то можно разумно задать интервалы цен и тем самым распределить товары по блокам достаточно равномерно.

        Рисунок 11-15   Разбиение блоков по вероятностному распределению

        ","path":["Глава 11. Сортировка","11.8   Блочная сортировка"],"tags":[]},{"location":"chapter_sorting/counting_sort/","level":1,"title":"11.9   Сортировка подсчетом","text":"

        Сортировка подсчетом (counting sort) реализует сортировку за счет подсчета количества вхождений элементов и обычно используется для массивов целых чисел.

        ","path":["Глава 11. Сортировка","11.9   Сортировка подсчетом"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1191","level":2,"title":"11.9.1   Простая реализация","text":"

        Сначала рассмотрим простой пример. Дан массив nums длины \\(n\\) , элементы которого являются «неотрицательными целыми числами». Общий процесс сортировки подсчетом показан на рисунке 11-16.

        1. Пройти по массиву, найти в нем максимальное число, обозначить его как \\(m\\) , а затем создать вспомогательный массив counter длины \\(m + 1\\) .
        2. С помощью counter подсчитать, сколько раз каждое число встречается в nums. При этом counter[num] хранит число вхождений значения num . Делается это просто: достаточно пройти по nums (пусть текущее число равно num ) и на каждом шаге увеличить counter[num] на \\(1\\) .
        3. Поскольку индексы массива counter изначально упорядочены, можно считать, что все числа уже отсортированы. Далее остается пройти по counter и в соответствии с числом вхождений записать значения обратно в nums в порядке возрастания.

        Рисунок 11-16   Процесс сортировки подсчетом

        Код приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby counting_sort.py
        def counting_sort_naive(nums: list[int]):\n    \"\"\"Сортировка подсчетом\"\"\"\n    # Простая реализация, не подходит для сортировки объектов\n    # 1. Найти максимальный элемент массива m\n    m = max(nums)\n    # 2. Подсчитать число появлений каждой цифры\n    # counter[num] обозначает число появлений num\n    counter = [0] * (m + 1)\n    for num in nums:\n        counter[num] += 1\n    # 3. Обойти counter и заполнить исходный массив nums элементами\n    i = 0\n    for num in range(m + 1):\n        for _ in range(counter[num]):\n            nums[i] = num\n            i += 1\n
        counting_sort.cpp
        /* Сортировка подсчетом */\n// Простая реализация, не подходит для сортировки объектов\nvoid countingSortNaive(vector<int> &nums) {\n    // 1. Найти максимальный элемент массива m\n    int m = 0;\n    for (int num : nums) {\n        m = max(m, num);\n    }\n    // 2. Подсчитать число появлений каждой цифры\n    // counter[num] обозначает число появлений num\n    vector<int> counter(m + 1, 0);\n    for (int num : nums) {\n        counter[num]++;\n    }\n    // 3. Обойти counter и заполнить исходный массив nums элементами\n    int i = 0;\n    for (int num = 0; num < m + 1; num++) {\n        for (int j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n}\n
        counting_sort.java
        /* Сортировка подсчетом */\n// Простая реализация, не подходит для сортировки объектов\nvoid countingSortNaive(int[] nums) {\n    // 1. Найти максимальный элемент массива m\n    int m = 0;\n    for (int num : nums) {\n        m = Math.max(m, num);\n    }\n    // 2. Подсчитать число появлений каждой цифры\n    // counter[num] обозначает число появлений num\n    int[] counter = new int[m + 1];\n    for (int num : nums) {\n        counter[num]++;\n    }\n    // 3. Обойти counter и заполнить исходный массив nums элементами\n    int i = 0;\n    for (int num = 0; num < m + 1; num++) {\n        for (int j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n}\n
        counting_sort.cs
        /* Сортировка подсчетом */\n// Простая реализация, не подходит для сортировки объектов\nvoid CountingSortNaive(int[] nums) {\n    // 1. Найти максимальный элемент массива m\n    int m = 0;\n    foreach (int num in nums) {\n        m = Math.Max(m, num);\n    }\n    // 2. Подсчитать число появлений каждой цифры\n    // counter[num] обозначает число появлений num\n    int[] counter = new int[m + 1];\n    foreach (int num in nums) {\n        counter[num]++;\n    }\n    // 3. Обойти counter и заполнить исходный массив nums элементами\n    int i = 0;\n    for (int num = 0; num < m + 1; num++) {\n        for (int j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n}\n
        counting_sort.go
        /* Сортировка подсчетом */\n// Простая реализация, не подходит для сортировки объектов\nfunc countingSortNaive(nums []int) {\n    // 1. Найти максимальный элемент массива m\n    m := 0\n    for _, num := range nums {\n        if num > m {\n            m = num\n        }\n    }\n    // 2. Подсчитать число появлений каждой цифры\n    // counter[num] обозначает число появлений num\n    counter := make([]int, m+1)\n    for _, num := range nums {\n        counter[num]++\n    }\n    // 3. Обойти counter и заполнить исходный массив nums элементами\n    for i, num := 0, 0; num < m+1; num++ {\n        for j := 0; j < counter[num]; j++ {\n            nums[i] = num\n            i++\n        }\n    }\n}\n
        counting_sort.swift
        /* Сортировка подсчетом */\n// Простая реализация, не подходит для сортировки объектов\nfunc countingSortNaive(nums: inout [Int]) {\n    // 1. Найти максимальный элемент массива m\n    let m = nums.max()!\n    // 2. Подсчитать число появлений каждой цифры\n    // counter[num] обозначает число появлений num\n    var counter = Array(repeating: 0, count: m + 1)\n    for num in nums {\n        counter[num] += 1\n    }\n    // 3. Обойти counter и заполнить исходный массив nums элементами\n    var i = 0\n    for num in 0 ..< m + 1 {\n        for _ in 0 ..< counter[num] {\n            nums[i] = num\n            i += 1\n        }\n    }\n}\n
        counting_sort.js
        /* Сортировка подсчетом */\n// Простая реализация, не подходит для сортировки объектов\nfunction countingSortNaive(nums) {\n    // 1. Найти максимальный элемент массива m\n    let m = Math.max(...nums);\n    // 2. Подсчитать число появлений каждой цифры\n    // counter[num] обозначает число появлений num\n    const counter = new Array(m + 1).fill(0);\n    for (const num of nums) {\n        counter[num]++;\n    }\n    // 3. Обойти counter и заполнить исходный массив nums элементами\n    let i = 0;\n    for (let num = 0; num < m + 1; num++) {\n        for (let j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n}\n
        counting_sort.ts
        /* Сортировка подсчетом */\n// Простая реализация, не подходит для сортировки объектов\nfunction countingSortNaive(nums: number[]): void {\n    // 1. Найти максимальный элемент массива m\n    let m: number = Math.max(...nums);\n    // 2. Подсчитать число появлений каждой цифры\n    // counter[num] обозначает число появлений num\n    const counter: number[] = new Array<number>(m + 1).fill(0);\n    for (const num of nums) {\n        counter[num]++;\n    }\n    // 3. Обойти counter и заполнить исходный массив nums элементами\n    let i = 0;\n    for (let num = 0; num < m + 1; num++) {\n        for (let j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n}\n
        counting_sort.dart
        /* Сортировка подсчетом */\n// Простая реализация, не подходит для сортировки объектов\nvoid countingSortNaive(List<int> nums) {\n  // 1. Найти максимальный элемент массива m\n  int m = 0;\n  for (int _num in nums) {\n    m = max(m, _num);\n  }\n  // 2. Подсчитать число появлений каждой цифры\n  // counter[_num] обозначает число появлений _num\n  List<int> counter = List.filled(m + 1, 0);\n  for (int _num in nums) {\n    counter[_num]++;\n  }\n  // 3. Обойти counter и заполнить исходный массив nums элементами\n  int i = 0;\n  for (int _num = 0; _num < m + 1; _num++) {\n    for (int j = 0; j < counter[_num]; j++, i++) {\n      nums[i] = _num;\n    }\n  }\n}\n
        counting_sort.rs
        /* Сортировка подсчетом */\n// Простая реализация, не подходит для сортировки объектов\nfn counting_sort_naive(nums: &mut [i32]) {\n    // 1. Найти максимальный элемент массива m\n    let m = *nums.iter().max().unwrap();\n    // 2. Подсчитать число появлений каждой цифры\n    // counter[num] обозначает число появлений num\n    let mut counter = vec![0; m as usize + 1];\n    for &num in nums.iter() {\n        counter[num as usize] += 1;\n    }\n    // 3. Обойти counter и заполнить исходный массив nums элементами\n    let mut i = 0;\n    for num in 0..m + 1 {\n        for _ in 0..counter[num as usize] {\n            nums[i] = num;\n            i += 1;\n        }\n    }\n}\n
        counting_sort.c
        /* Сортировка подсчетом */\n// Простая реализация, не подходит для сортировки объектов\nvoid countingSortNaive(int nums[], int size) {\n    // 1. Найти максимальный элемент массива m\n    int m = 0;\n    for (int i = 0; i < size; i++) {\n        if (nums[i] > m) {\n            m = nums[i];\n        }\n    }\n    // 2. Подсчитать число появлений каждой цифры\n    // counter[num] обозначает число появлений num\n    int *counter = calloc(m + 1, sizeof(int));\n    for (int i = 0; i < size; i++) {\n        counter[nums[i]]++;\n    }\n    // 3. Обойти counter и заполнить исходный массив nums элементами\n    int i = 0;\n    for (int num = 0; num < m + 1; num++) {\n        for (int j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n    // 4. Освободить память\n    free(counter);\n}\n
        counting_sort.kt
        /* Сортировка подсчетом */\n// Простая реализация, не подходит для сортировки объектов\nfun countingSortNaive(nums: IntArray) {\n    // 1. Найти максимальный элемент массива m\n    var m = 0\n    for (num in nums) {\n        m = max(m, num)\n    }\n    // 2. Подсчитать число появлений каждой цифры\n    // counter[num] обозначает число появлений num\n    val counter = IntArray(m + 1)\n    for (num in nums) {\n        counter[num]++\n    }\n    // 3. Обойти counter и заполнить исходный массив nums элементами\n    var i = 0\n    for (num in 0..<m + 1) {\n        var j = 0\n        while (j < counter[num]) {\n            nums[i] = num\n            j++\n            i++\n        }\n    }\n}\n
        counting_sort.rb
        ### Сортировка подсчетом ###\ndef counting_sort_naive(nums)\n  # Простая реализация, не подходит для сортировки объектов\n  # 1. Найти максимальный элемент массива m\n  m = 0\n  nums.each { |num| m = [m, num].max }\n  # 2. Подсчитать число появлений каждой цифры\n  # counter[num] обозначает число появлений num\n  counter = Array.new(m + 1, 0)\n  nums.each { |num| counter[num] += 1 }\n  # 3. Обойти counter и заполнить исходный массив nums элементами\n  i = 0\n  for num in 0...(m + 1)\n    (0...counter[num]).each do\n      nums[i] = num\n      i += 1\n    end\n  end\nend\n
        Визуализация кода

        Во весь экран >

        Связь между сортировкой подсчетом и блочной сортировкой

        Если посмотреть на сортировку подсчетом с точки зрения блочной сортировки, то каждый индекс массива counter можно рассматривать как отдельный блок, а процесс подсчета - как распределение элементов по соответствующим блокам. Иными словами, сортировка подсчетом является частным случаем блочной сортировки для целочисленных данных.

        ","path":["Глава 11. Сортировка","11.9   Сортировка подсчетом"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1192","level":2,"title":"11.9.2   Полная реализация","text":"

        Внимательный читатель мог заметить, что если входные данные представлены объектами, то описанный выше шаг 3. перестает работать. Например, если входными данными являются объекты товаров и мы хотим отсортировать их по цене (полю класса), то описанный алгоритм сможет выдать только отсортированный ряд цен, но не исходные объекты в нужном порядке.

        Как же получить корректный порядок исходных данных? Сначала вычислим «префиксную сумму» массива counter . Как следует из названия, префиксная сумма в индексе i , обозначаемая как prefix[i] , равна сумме первых i элементов массива:

        \\[ \\text{prefix}[i] = \\sum_{j=0}^i \\text{counter[j]} \\]

        Префиксная сумма имеет четкий смысл: prefix[num] - 1 обозначает индекс последнего вхождения элемента num в результирующем массиве res. Это очень важная информация, потому что она указывает, в какую позицию результирующего массива должен попасть каждый элемент. Далее мы проходим исходный массив nums в обратном порядке и на каждой итерации для очередного элемента num выполняем два действия.

        1. Записать num в массив res по индексу prefix[num] - 1 .
        2. Уменьшить префиксную сумму prefix[num] на \\(1\\) , чтобы получить индекс следующего размещения элемента num .

        После завершения прохода массив res будет содержать отсортированный результат. Остается только переписать res обратно в nums . Полный процесс сортировки подсчетом показан на рисунке 11-17.

        <1><2><3><4><5><6><7><8>

        Рисунок 11-17   Шаги сортировки подсчетом

        Код реализации сортировки подсчетом приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby counting_sort.py
        def counting_sort(nums: list[int]):\n    \"\"\"Сортировка подсчетом\"\"\"\n    # Полная реализация, позволяет сортировать объекты и является стабильной сортировкой\n    # 1. Найти максимальный элемент массива m\n    m = max(nums)\n    # 2. Подсчитать число появлений каждой цифры\n    # counter[num] обозначает число появлений num\n    counter = [0] * (m + 1)\n    for num in nums:\n        counter[num] += 1\n    # 3. Вычислить префиксные суммы counter и преобразовать «число появлений» в «конечный индекс»\n    # То есть counter[num]-1 — это индекс последнего появления num в res\n    for i in range(m):\n        counter[i + 1] += counter[i]\n    # 4. Обойти nums в обратном порядке и поместить элементы в результирующий массив res\n    # Инициализировать массив res для хранения результата\n    n = len(nums)\n    res = [0] * n\n    for i in range(n - 1, -1, -1):\n        num = nums[i]\n        res[counter[num] - 1] = num  # Поместить num по соответствующему индексу\n        counter[num] -= 1  # Уменьшить префиксную сумму на 1, чтобы получить индекс следующего размещения num\n    # Перезаписать исходный массив nums массивом результата res\n    for i in range(n):\n        nums[i] = res[i]\n
        counting_sort.cpp
        /* Сортировка подсчетом */\n// Полная реализация, позволяет сортировать объекты и является стабильной сортировкой\nvoid countingSort(vector<int> &nums) {\n    // 1. Найти максимальный элемент массива m\n    int m = 0;\n    for (int num : nums) {\n        m = max(m, num);\n    }\n    // 2. Подсчитать число появлений каждой цифры\n    // counter[num] обозначает число появлений num\n    vector<int> counter(m + 1, 0);\n    for (int num : nums) {\n        counter[num]++;\n    }\n    // 3. Вычислить префиксные суммы counter и преобразовать «число появлений» в «конечный индекс»\n    // То есть counter[num]-1 — это индекс последнего появления num в res\n    for (int i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Обойти nums в обратном порядке и поместить элементы в результирующий массив res\n    // Инициализировать массив res для хранения результата\n    int n = nums.size();\n    vector<int> res(n);\n    for (int i = n - 1; i >= 0; i--) {\n        int num = nums[i];\n        res[counter[num] - 1] = num; // Поместить num по соответствующему индексу\n        counter[num]--;              // Уменьшить префиксную сумму на 1, чтобы получить индекс следующего размещения num\n    }\n    // Перезаписать исходный массив nums массивом результата res\n    nums = res;\n}\n
        counting_sort.java
        /* Сортировка подсчетом */\n// Полная реализация, позволяет сортировать объекты и является стабильной сортировкой\nvoid countingSort(int[] nums) {\n    // 1. Найти максимальный элемент массива m\n    int m = 0;\n    for (int num : nums) {\n        m = Math.max(m, num);\n    }\n    // 2. Подсчитать число появлений каждой цифры\n    // counter[num] обозначает число появлений num\n    int[] counter = new int[m + 1];\n    for (int num : nums) {\n        counter[num]++;\n    }\n    // 3. Вычислить префиксные суммы counter и преобразовать «число появлений» в «конечный индекс»\n    // То есть counter[num]-1 — это индекс последнего появления num в res\n    for (int i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Обойти nums в обратном порядке и поместить элементы в результирующий массив res\n    // Инициализировать массив res для хранения результата\n    int n = nums.length;\n    int[] res = new int[n];\n    for (int i = n - 1; i >= 0; i--) {\n        int num = nums[i];\n        res[counter[num] - 1] = num; // Поместить num по соответствующему индексу\n        counter[num]--; // Уменьшить префиксную сумму на 1, чтобы получить индекс следующего размещения num\n    }\n    // Перезаписать исходный массив nums массивом результата res\n    for (int i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n
        counting_sort.cs
        /* Сортировка подсчетом */\n// Полная реализация, позволяет сортировать объекты и является стабильной сортировкой\nvoid CountingSort(int[] nums) {\n    // 1. Найти максимальный элемент массива m\n    int m = 0;\n    foreach (int num in nums) {\n        m = Math.Max(m, num);\n    }\n    // 2. Подсчитать число появлений каждой цифры\n    // counter[num] обозначает число появлений num\n    int[] counter = new int[m + 1];\n    foreach (int num in nums) {\n        counter[num]++;\n    }\n    // 3. Вычислить префиксные суммы counter и преобразовать «число появлений» в «конечный индекс»\n    // То есть counter[num]-1 — это индекс последнего появления num в res\n    for (int i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Обойти nums в обратном порядке и поместить элементы в результирующий массив res\n    // Инициализировать массив res для хранения результата\n    int n = nums.Length;\n    int[] res = new int[n];\n    for (int i = n - 1; i >= 0; i--) {\n        int num = nums[i];\n        res[counter[num] - 1] = num; // Поместить num по соответствующему индексу\n        counter[num]--; // Уменьшить префиксную сумму на 1, чтобы получить индекс следующего размещения num\n    }\n    // Перезаписать исходный массив nums массивом результата res\n    for (int i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n
        counting_sort.go
        /* Сортировка подсчетом */\n// Полная реализация, позволяет сортировать объекты и является стабильной сортировкой\nfunc countingSort(nums []int) {\n    // 1. Найти максимальный элемент массива m\n    m := 0\n    for _, num := range nums {\n        if num > m {\n            m = num\n        }\n    }\n    // 2. Подсчитать число появлений каждой цифры\n    // counter[num] обозначает число появлений num\n    counter := make([]int, m+1)\n    for _, num := range nums {\n        counter[num]++\n    }\n    // 3. Вычислить префиксные суммы counter и преобразовать «число появлений» в «конечный индекс»\n    // То есть counter[num]-1 — это индекс последнего появления num в res\n    for i := 0; i < m; i++ {\n        counter[i+1] += counter[i]\n    }\n    // 4. Обойти nums в обратном порядке и поместить элементы в результирующий массив res\n    // Инициализировать массив res для хранения результата\n    n := len(nums)\n    res := make([]int, n)\n    for i := n - 1; i >= 0; i-- {\n        num := nums[i]\n        // Поместить num по соответствующему индексу\n        res[counter[num]-1] = num\n        // Уменьшить префиксную сумму на 1, чтобы получить индекс следующего размещения num\n        counter[num]--\n    }\n    // Перезаписать исходный массив nums массивом результата res\n    copy(nums, res)\n}\n
        counting_sort.swift
        /* Сортировка подсчетом */\n// Полная реализация, позволяет сортировать объекты и является стабильной сортировкой\nfunc countingSort(nums: inout [Int]) {\n    // 1. Найти максимальный элемент массива m\n    let m = nums.max()!\n    // 2. Подсчитать число появлений каждой цифры\n    // counter[num] обозначает число появлений num\n    var counter = Array(repeating: 0, count: m + 1)\n    for num in nums {\n        counter[num] += 1\n    }\n    // 3. Вычислить префиксные суммы counter и преобразовать «число появлений» в «конечный индекс»\n    // То есть counter[num]-1 — это индекс последнего появления num в res\n    for i in 0 ..< m {\n        counter[i + 1] += counter[i]\n    }\n    // 4. Обойти nums в обратном порядке и поместить элементы в результирующий массив res\n    // Инициализировать массив res для хранения результата\n    var res = Array(repeating: 0, count: nums.count)\n    for i in nums.indices.reversed() {\n        let num = nums[i]\n        res[counter[num] - 1] = num // Поместить num по соответствующему индексу\n        counter[num] -= 1 // Уменьшить префиксную сумму на 1, чтобы получить индекс следующего размещения num\n    }\n    // Перезаписать исходный массив nums массивом результата res\n    for i in nums.indices {\n        nums[i] = res[i]\n    }\n}\n
        counting_sort.js
        /* Сортировка подсчетом */\n// Полная реализация, позволяет сортировать объекты и является стабильной сортировкой\nfunction countingSort(nums) {\n    // 1. Найти максимальный элемент массива m\n    let m = Math.max(...nums);\n    // 2. Подсчитать число появлений каждой цифры\n    // counter[num] обозначает число появлений num\n    const counter = new Array(m + 1).fill(0);\n    for (const num of nums) {\n        counter[num]++;\n    }\n    // 3. Вычислить префиксные суммы counter и преобразовать «число появлений» в «конечный индекс»\n    // То есть counter[num]-1 — это индекс последнего появления num в res\n    for (let i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Обойти nums в обратном порядке и поместить элементы в результирующий массив res\n    // Инициализировать массив res для хранения результата\n    const n = nums.length;\n    const res = new Array(n);\n    for (let i = n - 1; i >= 0; i--) {\n        const num = nums[i];\n        res[counter[num] - 1] = num; // Поместить num по соответствующему индексу\n        counter[num]--; // Уменьшить префиксную сумму на 1, чтобы получить индекс следующего размещения num\n    }\n    // Перезаписать исходный массив nums массивом результата res\n    for (let i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n
        counting_sort.ts
        /* Сортировка подсчетом */\n// Полная реализация, позволяет сортировать объекты и является стабильной сортировкой\nfunction countingSort(nums: number[]): void {\n    // 1. Найти максимальный элемент массива m\n    let m: number = Math.max(...nums);\n    // 2. Подсчитать число появлений каждой цифры\n    // counter[num] обозначает число появлений num\n    const counter: number[] = new Array<number>(m + 1).fill(0);\n    for (const num of nums) {\n        counter[num]++;\n    }\n    // 3. Вычислить префиксные суммы counter и преобразовать «число появлений» в «конечный индекс»\n    // То есть counter[num]-1 — это индекс последнего появления num в res\n    for (let i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Обойти nums в обратном порядке и поместить элементы в результирующий массив res\n    // Инициализировать массив res для хранения результата\n    const n = nums.length;\n    const res: number[] = new Array<number>(n);\n    for (let i = n - 1; i >= 0; i--) {\n        const num = nums[i];\n        res[counter[num] - 1] = num; // Поместить num по соответствующему индексу\n        counter[num]--; // Уменьшить префиксную сумму на 1, чтобы получить индекс следующего размещения num\n    }\n    // Перезаписать исходный массив nums массивом результата res\n    for (let i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n
        counting_sort.dart
        /* Сортировка подсчетом */\n// Полная реализация, позволяет сортировать объекты и является стабильной сортировкой\nvoid countingSort(List<int> nums) {\n  // 1. Найти максимальный элемент массива m\n  int m = 0;\n  for (int _num in nums) {\n    m = max(m, _num);\n  }\n  // 2. Подсчитать число появлений каждой цифры\n  // counter[_num] обозначает число появлений _num\n  List<int> counter = List.filled(m + 1, 0);\n  for (int _num in nums) {\n    counter[_num]++;\n  }\n  // 3. Вычислить префиксные суммы counter и преобразовать «число появлений» в «конечный индекс»\n  // То есть counter[_num]-1 — это индекс последнего появления _num в res\n  for (int i = 0; i < m; i++) {\n    counter[i + 1] += counter[i];\n  }\n  // 4. Обойти nums в обратном порядке и поместить элементы в результирующий массив res\n  // Инициализировать массив res для хранения результата\n  int n = nums.length;\n  List<int> res = List.filled(n, 0);\n  for (int i = n - 1; i >= 0; i--) {\n    int _num = nums[i];\n    res[counter[_num] - 1] = _num; // Поместить _num по соответствующему индексу\n    counter[_num]--; // Уменьшить префиксную сумму на 1, чтобы получить индекс следующего размещения _num\n  }\n  // Перезаписать исходный массив nums массивом результата res\n  nums.setAll(0, res);\n}\n
        counting_sort.rs
        /* Сортировка подсчетом */\n// Полная реализация, позволяет сортировать объекты и является стабильной сортировкой\nfn counting_sort(nums: &mut [i32]) {\n    // 1. Найти максимальный элемент массива m\n    let m = *nums.iter().max().unwrap() as usize;\n    // 2. Подсчитать число появлений каждой цифры\n    // counter[num] обозначает число появлений num\n    let mut counter = vec![0; m + 1];\n    for &num in nums.iter() {\n        counter[num as usize] += 1;\n    }\n    // 3. Вычислить префиксные суммы counter и преобразовать «число появлений» в «конечный индекс»\n    // То есть counter[num]-1 — это индекс последнего появления num в res\n    for i in 0..m {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Обойти nums в обратном порядке и поместить элементы в результирующий массив res\n    // Инициализировать массив res для хранения результата\n    let n = nums.len();\n    let mut res = vec![0; n];\n    for i in (0..n).rev() {\n        let num = nums[i];\n        res[counter[num as usize] - 1] = num; // Поместить num по соответствующему индексу\n        counter[num as usize] -= 1; // Уменьшить префиксную сумму на 1, чтобы получить индекс следующего размещения num\n    }\n    // Перезаписать исходный массив nums массивом результата res\n    nums.copy_from_slice(&res)\n}\n
        counting_sort.c
        /* Сортировка подсчетом */\n// Полная реализация, позволяет сортировать объекты и является стабильной сортировкой\nvoid countingSort(int nums[], int size) {\n    // 1. Найти максимальный элемент массива m\n    int m = 0;\n    for (int i = 0; i < size; i++) {\n        if (nums[i] > m) {\n            m = nums[i];\n        }\n    }\n    // 2. Подсчитать число появлений каждой цифры\n    // counter[num] обозначает число появлений num\n    int *counter = calloc(m, sizeof(int));\n    for (int i = 0; i < size; i++) {\n        counter[nums[i]]++;\n    }\n    // 3. Вычислить префиксные суммы counter и преобразовать «число появлений» в «конечный индекс»\n    // То есть counter[num]-1 — это индекс последнего появления num в res\n    for (int i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Обойти nums в обратном порядке и поместить элементы в результирующий массив res\n    // Инициализировать массив res для хранения результата\n    int *res = malloc(sizeof(int) * size);\n    for (int i = size - 1; i >= 0; i--) {\n        int num = nums[i];\n        res[counter[num] - 1] = num; // Поместить num по соответствующему индексу\n        counter[num]--;              // Уменьшить префиксную сумму на 1, чтобы получить индекс следующего размещения num\n    }\n    // Перезаписать исходный массив nums массивом результата res\n    memcpy(nums, res, size * sizeof(int));\n    // 5. Освободить память\n    free(res);\n    free(counter);\n}\n
        counting_sort.kt
        /* Сортировка подсчетом */\n// Полная реализация, позволяет сортировать объекты и является стабильной сортировкой\nfun countingSort(nums: IntArray) {\n    // 1. Найти максимальный элемент массива m\n    var m = 0\n    for (num in nums) {\n        m = max(m, num)\n    }\n    // 2. Подсчитать число появлений каждой цифры\n    // counter[num] обозначает число появлений num\n    val counter = IntArray(m + 1)\n    for (num in nums) {\n        counter[num]++\n    }\n    // 3. Вычислить префиксные суммы counter и преобразовать «число появлений» в «конечный индекс»\n    // То есть counter[num]-1 — это индекс последнего появления num в res\n    for (i in 0..<m) {\n        counter[i + 1] += counter[i]\n    }\n    // 4. Обойти nums в обратном порядке и поместить элементы в результирующий массив res\n    // Инициализировать массив res для хранения результата\n    val n = nums.size\n    val res = IntArray(n)\n    for (i in n - 1 downTo 0) {\n        val num = nums[i]\n        res[counter[num] - 1] = num // Поместить num по соответствующему индексу\n        counter[num]-- // Уменьшить префиксную сумму на 1, чтобы получить индекс следующего размещения num\n    }\n    // Перезаписать исходный массив nums массивом результата res\n    for (i in 0..<n) {\n        nums[i] = res[i]\n    }\n}\n
        counting_sort.rb
        ### Сортировка подсчетом ###\ndef counting_sort(nums)\n  # Полная реализация, позволяет сортировать объекты и является стабильной сортировкой\n  # 1. Найти максимальный элемент массива m\n  m = nums.max\n  # 2. Подсчитать число появлений каждой цифры\n  # counter[num] обозначает число появлений num\n  counter = Array.new(m + 1, 0)\n  nums.each { |num| counter[num] += 1 }\n  # 3. Вычислить префиксные суммы counter и преобразовать «число появлений» в «конечный индекс»\n  # То есть counter[num]-1 — это индекс последнего появления num в res\n  (0...m).each { |i| counter[i + 1] += counter[i] }\n  # 4. Обойти nums в обратном порядке и поместить элементы в результирующий массив res\n  # Инициализировать массив res для хранения результата\n  n = nums.length\n  res = Array.new(n, 0)\n  (n - 1).downto(0).each do |i|\n    num = nums[i]\n    res[counter[num] - 1] = num # Поместить num по соответствующему индексу\n    counter[num] -= 1 # Уменьшить префиксную сумму на 1, чтобы получить индекс следующего размещения num\n  end\n  # Перезаписать исходный массив nums массивом результата res\n  (0...n).each { |i| nums[i] = res[i] }\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 11. Сортировка","11.9   Сортировка подсчетом"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1193","level":2,"title":"11.9.3   Характеристики алгоритма","text":"
        • Временная сложность равна \\(O(n + m)\\), алгоритм не является адаптивным : необходимо пройти по nums и по counter , а оба этих прохода занимают линейное время. Обычно выполняется \\(n \\gg m\\) , поэтому временная сложность стремится к \\(O(n)\\) .
        • Пространственная сложность равна \\(O(n + m)\\), сортировка не выполняется на месте: используются массивы res и counter длины \\(n\\) и \\(m\\) соответственно.
        • Стабильная сортировка: порядок заполнения res идет «справа налево», поэтому обратный проход по nums позволяет сохранить относительный порядок равных элементов и тем самым реализовать стабильную сортировку. Вообще говоря, прямой проход по nums тоже даст правильный результат сортировки, но он будет нестабильным.
        ","path":["Глава 11. Сортировка","11.9   Сортировка подсчетом"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1194","level":2,"title":"11.9.4   Ограничения","text":"

        На этом этапе сортировка подсчетом может показаться очень изящной: она позволяет эффективно сортировать данные, опираясь только на подсчет числа вхождений. Однако условия ее применения довольно строгие.

        Сортировка подсчетом применима только к неотрицательным целым числам. Чтобы использовать ее для других типов данных, нужно убедиться, что эти данные можно преобразовать в неотрицательные целые числа и что при преобразовании относительный порядок элементов не изменится. Например, для массива целых чисел с отрицательными значениями можно сначала прибавить ко всем числам константу, превратив их в положительные, затем выполнить сортировку и после этого преобразовать значения обратно.

        Сортировка подсчетом подходит для случаев, когда объем данных велик, но диапазон значений невелик. Например, в приведенном выше примере \\(m\\) не должно быть слишком большим, иначе будет занято слишком много памяти. А когда \\(n \\ll m\\) , сортировка подсчетом использует \\(O(m)\\) времени и может оказаться медленнее, чем алгоритмы сортировки с \\(O(n \\log n)\\) .

        ","path":["Глава 11. Сортировка","11.9   Сортировка подсчетом"],"tags":[]},{"location":"chapter_sorting/heap_sort/","level":1,"title":"11.7   Пирамидальная сортировка","text":"

        Tip

        Перед чтением этого раздела убедитесь, что вы уже изучили главу «Куча».

        Пирамидальная сортировка (heap sort) - это эффективный алгоритм сортировки, основанный на структуре данных «куча». Для его реализации можно использовать уже изученные нами «построение кучи» и «извлечение элементов из кучи».

        1. Подать на вход массив и построить из него мин-кучу. В этот момент минимальный элемент будет находиться в вершине кучи.
        2. Непрерывно выполнять извлечение из кучи и по порядку записывать извлеченные элементы - так получится последовательность, отсортированная по возрастанию.

        Хотя этот метод и работоспособен, он требует дополнительного массива для хранения извлеченных элементов и потому расходует лишнюю память. На практике обычно используют более изящную реализацию.

        ","path":["Глава 11. Сортировка","11.7   Пирамидальная сортировка"],"tags":[]},{"location":"chapter_sorting/heap_sort/#1171","level":2,"title":"11.7.1   Алгоритм","text":"

        Пусть длина массива равна \\(n\\). Тогда процесс пирамидальной сортировки показан на рисунке 11-12.

        1. Подать на вход массив и построить из него макс-кучу. После этого максимальный элемент окажется в вершине кучи.
        2. Обменять элемент в вершине кучи (первый элемент) с элементом внизу кучи (последний элемент). После обмена длина кучи уменьшается на \\(1\\) , а число уже отсортированных элементов увеличивается на \\(1\\) .
        3. Начиная с вершины, выполнить операцию просеивания сверху вниз. После этого свойство кучи будет восстановлено.
        4. Циклически повторять шаг 2. и шаг 3. . После \\(n - 1\\) раундов массив будет полностью отсортирован.

        Tip

        На самом деле операция извлечения из кучи тоже включает шаг 2. и шаг 3. , только дополнительно содержит действие по удалению элемента.

        <1><2><3><4><5><6><7><8><9><10><11><12>

        Рисунок 11-12   Шаги пирамидальной сортировки

        В коде используется та же функция просеивания сверху вниз sift_down(), что и в главе «Куча». Важно помнить, что длина кучи уменьшается по мере извлечения максимального элемента, поэтому функции sift_down() нужно передавать параметр длины \\(n\\) , чтобы указать текущую действительную длину кучи. Код приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby heap_sort.py
        def sift_down(nums: list[int], n: int, i: int):\n    \"\"\"Длина кучи равна n; начиная с узла i, выполнить просеивание сверху вниз\"\"\"\n    while True:\n        # Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n        l = 2 * i + 1\n        r = 2 * i + 2\n        ma = i\n        if l < n and nums[l] > nums[ma]:\n            ma = l\n        if r < n and nums[r] > nums[ma]:\n            ma = r\n        # Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if ma == i:\n            break\n        # Поменять два узла местами\n        nums[i], nums[ma] = nums[ma], nums[i]\n        # Циклическое просеивание вниз\n        i = ma\n\ndef heap_sort(nums: list[int]):\n    \"\"\"Сортировка кучей\"\"\"\n    # Построение кучи: выполнить heapify для всех узлов, кроме листовых\n    for i in range(len(nums) // 2 - 1, -1, -1):\n        sift_down(nums, len(nums), i)\n    # Извлекать максимальный элемент из кучи в течение n-1 итераций\n    for i in range(len(nums) - 1, 0, -1):\n        # Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n        nums[0], nums[i] = nums[i], nums[0]\n        # Начиная с корневого узла, выполнить просеивание сверху вниз\n        sift_down(nums, i, 0)\n
        heap_sort.cpp
        /* Длина кучи равна n; начиная с узла i, выполнить просеивание сверху вниз */\nvoid siftDown(vector<int> &nums, int n, int i) {\n    while (true) {\n        // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n        int l = 2 * i + 1;\n        int r = 2 * i + 2;\n        int ma = i;\n        if (l < n && nums[l] > nums[ma])\n            ma = l;\n        if (r < n && nums[r] > nums[ma])\n            ma = r;\n        // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if (ma == i) {\n            break;\n        }\n        // Поменять два узла местами\n        swap(nums[i], nums[ma]);\n        // Циклическое просеивание вниз\n        i = ma;\n    }\n}\n\n/* Сортировка кучей */\nvoid heapSort(vector<int> &nums) {\n    // Построение кучи: выполнить heapify для всех узлов, кроме листовых\n    for (int i = nums.size() / 2 - 1; i >= 0; --i) {\n        siftDown(nums, nums.size(), i);\n    }\n    // Извлекать максимальный элемент из кучи в течение n-1 итераций\n    for (int i = nums.size() - 1; i > 0; --i) {\n        // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n        swap(nums[0], nums[i]);\n        // Начиная с корневого узла, выполнить просеивание сверху вниз\n        siftDown(nums, i, 0);\n    }\n}\n
        heap_sort.java
        /* Длина кучи равна n; начиная с узла i, выполнить просеивание сверху вниз */\nvoid siftDown(int[] nums, int n, int i) {\n    while (true) {\n        // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n        int l = 2 * i + 1;\n        int r = 2 * i + 2;\n        int ma = i;\n        if (l < n && nums[l] > nums[ma])\n            ma = l;\n        if (r < n && nums[r] > nums[ma])\n            ma = r;\n        // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if (ma == i)\n            break;\n        // Поменять два узла местами\n        int temp = nums[i];\n        nums[i] = nums[ma];\n        nums[ma] = temp;\n        // Циклическое просеивание вниз\n        i = ma;\n    }\n}\n\n/* Сортировка кучей */\nvoid heapSort(int[] nums) {\n    // Построение кучи: выполнить heapify для всех узлов, кроме листовых\n    for (int i = nums.length / 2 - 1; i >= 0; i--) {\n        siftDown(nums, nums.length, i);\n    }\n    // Извлекать максимальный элемент из кучи в течение n-1 итераций\n    for (int i = nums.length - 1; i > 0; i--) {\n        // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n        int tmp = nums[0];\n        nums[0] = nums[i];\n        nums[i] = tmp;\n        // Начиная с корневого узла, выполнить просеивание сверху вниз\n        siftDown(nums, i, 0);\n    }\n}\n
        heap_sort.cs
        /* Длина кучи равна n; начиная с узла i, выполнить просеивание сверху вниз */\nvoid SiftDown(int[] nums, int n, int i) {\n    while (true) {\n        // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n        int l = 2 * i + 1;\n        int r = 2 * i + 2;\n        int ma = i;\n        if (l < n && nums[l] > nums[ma])\n            ma = l;\n        if (r < n && nums[r] > nums[ma])\n            ma = r;\n        // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if (ma == i)\n            break;\n        // Поменять два узла местами\n        (nums[ma], nums[i]) = (nums[i], nums[ma]);\n        // Циклическое просеивание вниз\n        i = ma;\n    }\n}\n\n/* Сортировка кучей */\nvoid HeapSort(int[] nums) {\n    // Построение кучи: выполнить heapify для всех узлов, кроме листовых\n    for (int i = nums.Length / 2 - 1; i >= 0; i--) {\n        SiftDown(nums, nums.Length, i);\n    }\n    // Извлекать максимальный элемент из кучи в течение n-1 итераций\n    for (int i = nums.Length - 1; i > 0; i--) {\n        // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n        (nums[i], nums[0]) = (nums[0], nums[i]);\n        // Начиная с корневого узла, выполнить просеивание сверху вниз\n        SiftDown(nums, i, 0);\n    }\n}\n
        heap_sort.go
        /* Длина кучи равна n; начиная с узла i, выполнить просеивание сверху вниз */\nfunc siftDown(nums *[]int, n, i int) {\n    for true {\n        // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n        l := 2*i + 1\n        r := 2*i + 2\n        ma := i\n        if l < n && (*nums)[l] > (*nums)[ma] {\n            ma = l\n        }\n        if r < n && (*nums)[r] > (*nums)[ma] {\n            ma = r\n        }\n        // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if ma == i {\n            break\n        }\n        // Поменять два узла местами\n        (*nums)[i], (*nums)[ma] = (*nums)[ma], (*nums)[i]\n        // Циклическое просеивание вниз\n        i = ma\n    }\n}\n\n/* Сортировка кучей */\nfunc heapSort(nums *[]int) {\n    // Построение кучи: выполнить heapify для всех узлов, кроме листовых\n    for i := len(*nums)/2 - 1; i >= 0; i-- {\n        siftDown(nums, len(*nums), i)\n    }\n    // Извлекать максимальный элемент из кучи в течение n-1 итераций\n    for i := len(*nums) - 1; i > 0; i-- {\n        // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n        (*nums)[0], (*nums)[i] = (*nums)[i], (*nums)[0]\n        // Начиная с корневого узла, выполнить просеивание сверху вниз\n        siftDown(nums, i, 0)\n    }\n}\n
        heap_sort.swift
        /* Длина кучи равна n; начиная с узла i, выполнить просеивание сверху вниз */\nfunc siftDown(nums: inout [Int], n: Int, i: Int) {\n    var i = i\n    while true {\n        // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n        let l = 2 * i + 1\n        let r = 2 * i + 2\n        var ma = i\n        if l < n, nums[l] > nums[ma] {\n            ma = l\n        }\n        if r < n, nums[r] > nums[ma] {\n            ma = r\n        }\n        // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if ma == i {\n            break\n        }\n        // Поменять два узла местами\n        nums.swapAt(i, ma)\n        // Циклическое просеивание вниз\n        i = ma\n    }\n}\n\n/* Сортировка кучей */\nfunc heapSort(nums: inout [Int]) {\n    // Построение кучи: выполнить heapify для всех узлов, кроме листовых\n    for i in stride(from: nums.count / 2 - 1, through: 0, by: -1) {\n        siftDown(nums: &nums, n: nums.count, i: i)\n    }\n    // Извлекать максимальный элемент из кучи в течение n-1 итераций\n    for i in nums.indices.dropFirst().reversed() {\n        // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n        nums.swapAt(0, i)\n        // Начиная с корневого узла, выполнить просеивание сверху вниз\n        siftDown(nums: &nums, n: i, i: 0)\n    }\n}\n
        heap_sort.js
        /* Длина кучи равна n; начиная с узла i, выполнить просеивание сверху вниз */\nfunction siftDown(nums, n, i) {\n    while (true) {\n        // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n        let l = 2 * i + 1;\n        let r = 2 * i + 2;\n        let ma = i;\n        if (l < n && nums[l] > nums[ma]) {\n            ma = l;\n        }\n        if (r < n && nums[r] > nums[ma]) {\n            ma = r;\n        }\n        // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if (ma === i) {\n            break;\n        }\n        // Поменять два узла местами\n        [nums[i], nums[ma]] = [nums[ma], nums[i]];\n        // Циклическое просеивание вниз\n        i = ma;\n    }\n}\n\n/* Сортировка кучей */\nfunction heapSort(nums) {\n    // Построение кучи: выполнить heapify для всех узлов, кроме листовых\n    for (let i = Math.floor(nums.length / 2) - 1; i >= 0; i--) {\n        siftDown(nums, nums.length, i);\n    }\n    // Извлекать максимальный элемент из кучи в течение n-1 итераций\n    for (let i = nums.length - 1; i > 0; i--) {\n        // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n        [nums[0], nums[i]] = [nums[i], nums[0]];\n        // Начиная с корневого узла, выполнить просеивание сверху вниз\n        siftDown(nums, i, 0);\n    }\n}\n
        heap_sort.ts
        /* Длина кучи равна n; начиная с узла i, выполнить просеивание сверху вниз */\nfunction siftDown(nums: number[], n: number, i: number): void {\n    while (true) {\n        // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n        let l = 2 * i + 1;\n        let r = 2 * i + 2;\n        let ma = i;\n        if (l < n && nums[l] > nums[ma]) {\n            ma = l;\n        }\n        if (r < n && nums[r] > nums[ma]) {\n            ma = r;\n        }\n        // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if (ma === i) {\n            break;\n        }\n        // Поменять два узла местами\n        [nums[i], nums[ma]] = [nums[ma], nums[i]];\n        // Циклическое просеивание вниз\n        i = ma;\n    }\n}\n\n/* Сортировка кучей */\nfunction heapSort(nums: number[]): void {\n    // Построение кучи: выполнить heapify для всех узлов, кроме листовых\n    for (let i = Math.floor(nums.length / 2) - 1; i >= 0; i--) {\n        siftDown(nums, nums.length, i);\n    }\n    // Извлекать максимальный элемент из кучи в течение n-1 итераций\n    for (let i = nums.length - 1; i > 0; i--) {\n        // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n        [nums[0], nums[i]] = [nums[i], nums[0]];\n        // Начиная с корневого узла, выполнить просеивание сверху вниз\n        siftDown(nums, i, 0);\n    }\n}\n
        heap_sort.dart
        /* Длина кучи равна n; начиная с узла i, выполнить просеивание сверху вниз */\nvoid siftDown(List<int> nums, int n, int i) {\n  while (true) {\n    // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n    int l = 2 * i + 1;\n    int r = 2 * i + 2;\n    int ma = i;\n    if (l < n && nums[l] > nums[ma]) ma = l;\n    if (r < n && nums[r] > nums[ma]) ma = r;\n    // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n    if (ma == i) break;\n    // Поменять два узла местами\n    int temp = nums[i];\n    nums[i] = nums[ma];\n    nums[ma] = temp;\n    // Циклическое просеивание вниз\n    i = ma;\n  }\n}\n\n/* Сортировка кучей */\nvoid heapSort(List<int> nums) {\n  // Построение кучи: выполнить heapify для всех узлов, кроме листовых\n  for (int i = nums.length ~/ 2 - 1; i >= 0; i--) {\n    siftDown(nums, nums.length, i);\n  }\n  // Извлекать максимальный элемент из кучи в течение n-1 итераций\n  for (int i = nums.length - 1; i > 0; i--) {\n    // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n    int tmp = nums[0];\n    nums[0] = nums[i];\n    nums[i] = tmp;\n    // Начиная с корневого узла, выполнить просеивание сверху вниз\n    siftDown(nums, i, 0);\n  }\n}\n
        heap_sort.rs
        /* Длина кучи равна n; начиная с узла i, выполнить просеивание сверху вниз */\nfn sift_down(nums: &mut [i32], n: usize, mut i: usize) {\n    loop {\n        // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n        let l = 2 * i + 1;\n        let r = 2 * i + 2;\n        let mut ma = i;\n        if l < n && nums[l] > nums[ma] {\n            ma = l;\n        }\n        if r < n && nums[r] > nums[ma] {\n            ma = r;\n        }\n        // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if ma == i {\n            break;\n        }\n        // Поменять два узла местами\n        nums.swap(i, ma);\n        // Циклическое просеивание вниз\n        i = ma;\n    }\n}\n\n/* Сортировка кучей */\nfn heap_sort(nums: &mut [i32]) {\n    // Построение кучи: выполнить heapify для всех узлов, кроме листовых\n    for i in (0..nums.len() / 2).rev() {\n        sift_down(nums, nums.len(), i);\n    }\n    // Извлекать максимальный элемент из кучи в течение n-1 итераций\n    for i in (1..nums.len()).rev() {\n        // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n        nums.swap(0, i);\n        // Начиная с корневого узла, выполнить просеивание сверху вниз\n        sift_down(nums, i, 0);\n    }\n}\n
        heap_sort.c
        /* Длина кучи равна n; начиная с узла i, выполнить просеивание сверху вниз */\nvoid siftDown(int nums[], int n, int i) {\n    while (1) {\n        // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n        int l = 2 * i + 1;\n        int r = 2 * i + 2;\n        int ma = i;\n        if (l < n && nums[l] > nums[ma])\n            ma = l;\n        if (r < n && nums[r] > nums[ma])\n            ma = r;\n        // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if (ma == i) {\n            break;\n        }\n        // Поменять два узла местами\n        int temp = nums[i];\n        nums[i] = nums[ma];\n        nums[ma] = temp;\n        // Циклическое просеивание вниз\n        i = ma;\n    }\n}\n\n/* Сортировка кучей */\nvoid heapSort(int nums[], int n) {\n    // Построение кучи: выполнить heapify для всех узлов, кроме листовых\n    for (int i = n / 2 - 1; i >= 0; --i) {\n        siftDown(nums, n, i);\n    }\n    // Извлекать максимальный элемент из кучи в течение n-1 итераций\n    for (int i = n - 1; i > 0; --i) {\n        // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n        int tmp = nums[0];\n        nums[0] = nums[i];\n        nums[i] = tmp;\n        // Начиная с корневого узла, выполнить просеивание сверху вниз\n        siftDown(nums, i, 0);\n    }\n}\n
        heap_sort.kt
        /* Длина кучи равна n; начиная с узла i, выполнить просеивание сверху вниз */\nfun siftDown(nums: IntArray, n: Int, li: Int) {\n    var i = li\n    while (true) {\n        // Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n        val l = 2 * i + 1\n        val r = 2 * i + 2\n        var ma = i\n        if (l < n && nums[l] > nums[ma]) \n            ma = l\n        if (r < n && nums[r] > nums[ma]) \n            ma = r\n        // Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n        if (ma == i) \n            break\n        // Поменять два узла местами\n        val temp = nums[i]\n        nums[i] = nums[ma]\n        nums[ma] = temp\n        // Циклическое просеивание вниз\n        i = ma\n    }\n}\n\n/* Сортировка кучей */\nfun heapSort(nums: IntArray) {\n    // Построение кучи: выполнить heapify для всех узлов, кроме листовых\n    for (i in nums.size / 2 - 1 downTo 0) {\n        siftDown(nums, nums.size, i)\n    }\n    // Извлекать максимальный элемент из кучи в течение n-1 итераций\n    for (i in nums.size - 1 downTo 1) {\n        // Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n        val temp = nums[0]\n        nums[0] = nums[i]\n        nums[i] = temp\n        // Начиная с корневого узла, выполнить просеивание сверху вниз\n        siftDown(nums, i, 0)\n    }\n}\n
        heap_sort.rb
        ### Длина кучи равна n; начиная с узла i, выполнить просеивание сверху вниз ###\ndef sift_down(nums, n, i)\n  while true\n    # Определить узел с максимальным значением среди i, l и r и обозначить его как ma\n    l = 2 * i + 1\n    r = 2 * i + 2\n    ma = i\n    ma = l if l < n && nums[l] > nums[ma]\n    ma = r if r < n && nums[r] > nums[ma]\n    # Если узел i уже максимален или индексы l и r вне границ, дальнейшее просеивание не требуется, выйти\n    break if ma == i\n    # Поменять два узла местами\n    nums[i], nums[ma] = nums[ma], nums[i]\n    # Циклическое просеивание вниз\n    i = ma\n  end\nend\n\n### Сортировка кучей ###\ndef heap_sort(nums)\n  # Построение кучи: выполнить heapify для всех узлов, кроме листовых\n  (nums.length / 2 - 1).downto(0) do |i|\n    sift_down(nums, nums.length, i)\n  end\n  # Извлекать максимальный элемент из кучи в течение n-1 итераций\n  (nums.length - 1).downto(1) do |i|\n    # Поменять корневой узел с самым правым листом местами (поменять первый и последний элементы)\n    nums[0], nums[i] = nums[i], nums[0]\n    # Начиная с корневого узла, выполнить просеивание сверху вниз\n    sift_down(nums, i, 0)\n  end\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 11. Сортировка","11.7   Пирамидальная сортировка"],"tags":[]},{"location":"chapter_sorting/heap_sort/#1172","level":2,"title":"11.7.2   Характеристики алгоритма","text":"
        • Временная сложность равна \\(O(n \\log n)\\), алгоритм не является адаптивным: построение кучи занимает \\(O(n)\\) времени. Извлечение максимального элемента из кучи имеет временную сложность \\(O(\\log n)\\) и выполняется \\(n - 1\\) раз.
        • Пространственная сложность равна \\(O(1)\\), сортировка выполняется на месте: несколько переменных-указателей используют \\(O(1)\\) памяти. Обмен элементов и операции просеивания выполняются прямо в исходном массиве.
        • Нестабильная сортировка: при обмене вершины кучи и нижнего элемента относительный порядок равных элементов может измениться.
        ","path":["Глава 11. Сортировка","11.7   Пирамидальная сортировка"],"tags":[]},{"location":"chapter_sorting/insertion_sort/","level":1,"title":"11.4   Сортировка вставками","text":"

        Сортировка вставками (insertion sort) - это простой алгоритм сортировки, принцип которого очень похож на ручную сортировку карт в колоде.

        Точнее говоря, в неотсортированном диапазоне выбирается опорный элемент, после чего он сравнивается с элементами слева в уже отсортированном диапазоне и вставляется в правильную позицию.

        На рисунке 11-6 показан процесс вставки элемента в массив. Пусть опорный элемент обозначен как base. Нам нужно сдвинуть все элементы от целевого индекса до base на одну позицию вправо, а затем записать base в целевой индекс.

        Рисунок 11-6   Одна операция вставки

        ","path":["Глава 11. Сортировка","11.4   Сортировка вставками"],"tags":[]},{"location":"chapter_sorting/insertion_sort/#1141","level":2,"title":"11.4.1   Алгоритм","text":"

        Общий процесс сортировки вставками показан на рисунке 11-7.

        1. В начальном состоянии отсортирован только первый элемент массива.
        2. Выбрать второй элемент массива как base. После вставки в правильную позицию первые два элемента массива окажутся отсортированными.
        3. Выбрать третий элемент как base. После вставки в правильную позицию первые три элемента массива окажутся отсортированными.
        4. Продолжать по аналогии. В последнем раунде в качестве base берется последний элемент, и после его вставки все элементы массива будут отсортированы.

        Рисунок 11-7   Процесс сортировки вставками

        Пример кода:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby insertion_sort.py
        def insertion_sort(nums: list[int]):\n    \"\"\"Сортировка вставками\"\"\"\n    # Внешний цикл: отсортированный диапазон [0, i-1]\n    for i in range(1, len(nums)):\n        base = nums[i]\n        j = i - 1\n        # Внутренний цикл: вставить base в правильную позицию отсортированного диапазона [0, i-1]\n        while j >= 0 and nums[j] > base:\n            nums[j + 1] = nums[j]  # Сдвинуть nums[j] на одну позицию вправо\n            j -= 1\n        nums[j + 1] = base  # Поместить base в правильную позицию\n
        insertion_sort.cpp
        /* Сортировка вставками */\nvoid insertionSort(vector<int> &nums) {\n    // Внешний цикл: отсортированный диапазон [0, i-1]\n    for (int i = 1; i < nums.size(); i++) {\n        int base = nums[i], j = i - 1;\n        // Внутренний цикл: вставить base в правильную позицию отсортированного диапазона [0, i-1]\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j]; // Сдвинуть nums[j] на одну позицию вправо\n            j--;\n        }\n        nums[j + 1] = base; // Поместить base в правильную позицию\n    }\n}\n
        insertion_sort.java
        /* Сортировка вставками */\nvoid insertionSort(int[] nums) {\n    // Внешний цикл: отсортированный диапазон [0, i-1]\n    for (int i = 1; i < nums.length; i++) {\n        int base = nums[i], j = i - 1;\n        // Внутренний цикл: вставить base в правильную позицию отсортированного диапазона [0, i-1]\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j]; // Сдвинуть nums[j] на одну позицию вправо\n            j--;\n        }\n        nums[j + 1] = base;        // Поместить base в правильную позицию\n    }\n}\n
        insertion_sort.cs
        /* Сортировка вставками */\nvoid InsertionSort(int[] nums) {\n    // Внешний цикл: отсортированный диапазон [0, i-1]\n    for (int i = 1; i < nums.Length; i++) {\n        int bas = nums[i], j = i - 1;\n        // Внутренний цикл: вставить base в правильную позицию отсортированного диапазона [0, i-1]\n        while (j >= 0 && nums[j] > bas) {\n            nums[j + 1] = nums[j]; // Сдвинуть nums[j] на одну позицию вправо\n            j--;\n        }\n        nums[j + 1] = bas;         // Поместить base в правильную позицию\n    }\n}\n
        insertion_sort.go
        /* Сортировка вставками */\nfunc insertionSort(nums []int) {\n    // Внешний цикл: отсортированный диапазон [0, i-1]\n    for i := 1; i < len(nums); i++ {\n        base := nums[i]\n        j := i - 1\n        // Внутренний цикл: вставить base в правильную позицию отсортированного диапазона [0, i-1]\n        for j >= 0 && nums[j] > base {\n            nums[j+1] = nums[j] // Сдвинуть nums[j] на одну позицию вправо\n            j--\n        }\n        nums[j+1] = base // Поместить base в правильную позицию\n    }\n}\n
        insertion_sort.swift
        /* Сортировка вставками */\nfunc insertionSort(nums: inout [Int]) {\n    // Внешний цикл: отсортированный диапазон [0, i-1]\n    for i in nums.indices.dropFirst() {\n        let base = nums[i]\n        var j = i - 1\n        // Внутренний цикл: вставить base в правильную позицию отсортированного диапазона [0, i-1]\n        while j >= 0, nums[j] > base {\n            nums[j + 1] = nums[j] // Сдвинуть nums[j] на одну позицию вправо\n            j -= 1\n        }\n        nums[j + 1] = base // Поместить base в правильную позицию\n    }\n}\n
        insertion_sort.js
        /* Сортировка вставками */\nfunction insertionSort(nums) {\n    // Внешний цикл: отсортированный диапазон [0, i-1]\n    for (let i = 1; i < nums.length; i++) {\n        let base = nums[i],\n            j = i - 1;\n        // Внутренний цикл: вставить base в правильную позицию отсортированного диапазона [0, i-1]\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j]; // Сдвинуть nums[j] на одну позицию вправо\n            j--;\n        }\n        nums[j + 1] = base; // Поместить base в правильную позицию\n    }\n}\n
        insertion_sort.ts
        /* Сортировка вставками */\nfunction insertionSort(nums: number[]): void {\n    // Внешний цикл: отсортированный диапазон [0, i-1]\n    for (let i = 1; i < nums.length; i++) {\n        const base = nums[i];\n        let j = i - 1;\n        // Внутренний цикл: вставить base в правильную позицию отсортированного диапазона [0, i-1]\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j]; // Сдвинуть nums[j] на одну позицию вправо\n            j--;\n        }\n        nums[j + 1] = base; // Поместить base в правильную позицию\n    }\n}\n
        insertion_sort.dart
        /* Сортировка вставками */\nvoid insertionSort(List<int> nums) {\n  // Внешний цикл: отсортированный диапазон [0, i-1]\n  for (int i = 1; i < nums.length; i++) {\n    int base = nums[i], j = i - 1;\n    // Внутренний цикл: вставить base в правильную позицию отсортированного диапазона [0, i-1]\n    while (j >= 0 && nums[j] > base) {\n      nums[j + 1] = nums[j]; // Сдвинуть nums[j] на одну позицию вправо\n      j--;\n    }\n    nums[j + 1] = base; // Поместить base в правильную позицию\n  }\n}\n
        insertion_sort.rs
        /* Сортировка вставками */\nfn insertion_sort(nums: &mut [i32]) {\n    // Внешний цикл: отсортированный диапазон [0, i-1]\n    for i in 1..nums.len() {\n        let (base, mut j) = (nums[i], (i - 1) as i32);\n        // Внутренний цикл: вставить base в правильную позицию отсортированного диапазона [0, i-1]\n        while j >= 0 && nums[j as usize] > base {\n            nums[(j + 1) as usize] = nums[j as usize]; // Сдвинуть nums[j] на одну позицию вправо\n            j -= 1;\n        }\n        nums[(j + 1) as usize] = base; // Поместить base в правильную позицию\n    }\n}\n
        insertion_sort.c
        /* Сортировка вставками */\nvoid insertionSort(int nums[], int size) {\n    // Внешний цикл: отсортированный диапазон [0, i-1]\n    for (int i = 1; i < size; i++) {\n        int base = nums[i], j = i - 1;\n        // Внутренний цикл: вставить base в правильную позицию отсортированного диапазона [0, i-1]\n        while (j >= 0 && nums[j] > base) {\n            // Сдвинуть nums[j] на одну позицию вправо\n            nums[j + 1] = nums[j];\n            j--;\n        }\n        // Поместить base в правильную позицию\n        nums[j + 1] = base;\n    }\n}\n
        insertion_sort.kt
        /* Сортировка вставками */\nfun insertionSort(nums: IntArray) {\n    // Внешний цикл: отсортированные элементы равны 1, 2, ..., n\n    for (i in nums.indices) {\n        val base = nums[i]\n        var j = i - 1\n        // Внутренний цикл: вставить base в правильную позицию отсортированного диапазона [0, i-1]\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j] // Сдвинуть nums[j] на одну позицию вправо\n            j--\n        }\n        nums[j + 1] = base        // Поместить base в правильную позицию\n    }\n}\n
        insertion_sort.rb
        ### Сортировка вставками ###\ndef insertion_sort(nums)\n  n = nums.length\n  # Внешний цикл: отсортированный диапазон [0, i-1]\n  for i in 1...n\n    base = nums[i]\n    j = i - 1\n    # Внутренний цикл: вставить base в правильную позицию отсортированного диапазона [0, i-1]\n    while j >= 0 && nums[j] > base\n      nums[j + 1] = nums[j] # Сдвинуть nums[j] на одну позицию вправо\n      j -= 1\n    end\n    nums[j + 1] = base # Поместить base в правильную позицию\n  end\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 11. Сортировка","11.4   Сортировка вставками"],"tags":[]},{"location":"chapter_sorting/insertion_sort/#1142","level":2,"title":"11.4.2   Характеристики алгоритма","text":"
        • Временная сложность равна \\(O(n^2)\\), алгоритм адаптивен: в худшем случае каждой операции вставки требуется соответственно \\(n - 1\\), \\(n-2\\), \\(\\dots\\), \\(2\\), \\(1\\) итераций, а их сумма равна \\((n - 1) n / 2\\) , поэтому временная сложность равна \\(O(n^2)\\) . Если входные данные уже упорядочены, операция вставки завершается раньше. Когда входной массив полностью отсортирован, сортировка вставками достигает лучшей временной сложности \\(O(n)\\) .
        • Пространственная сложность равна \\(O(1)\\), сортировка выполняется на месте: указатели \\(i\\) и \\(j\\) используют константный объем дополнительной памяти.
        • Стабильная сортировка: в процессе вставки элементы помещаются справа от равных им элементов, поэтому их относительный порядок не меняется.
        ","path":["Глава 11. Сортировка","11.4   Сортировка вставками"],"tags":[]},{"location":"chapter_sorting/insertion_sort/#1143","level":2,"title":"11.4.3   Преимущества сортировки вставками","text":"

        Временная сложность сортировки вставками равна \\(O(n^2)\\) , а у быстрой сортировки, которую мы скоро изучим, временная сложность равна \\(O(n \\log n)\\) . Несмотря на более высокую асимптотическую сложность, на малых объемах данных сортировка вставками обычно работает быстрее.

        Этот вывод похож на сравнение линейного и двоичного поиска. Алгоритмы уровня \\(O(n \\log n)\\) , такие как быстрая сортировка, относятся к алгоритмам на основе стратегии «разделяй и властвуй» и обычно включают больше элементарных вычислений. Когда объем данных мал, значения \\(n^2\\) и \\(n \\log n\\) близки друг к другу, поэтому асимптотика не доминирует, а решающим становится число элементарных операций в каждом раунде.

        На практике встроенные функции сортировки во многих языках программирования (например, в Java) используют сортировку вставками. Общая идея такова: для длинных массивов применять алгоритмы сортировки на основе стратегии «разделяй и властвуй», например быструю сортировку. Для коротких массивов сразу использовать сортировку вставками.

        Хотя сортировка пузырьком, выбором и вставками имеют одинаковую временную сложность \\(O(n^2)\\) , в реальных задачах сортировка вставками используется заметно чаще, чем сортировка пузырьком и сортировка выбором. Основные причины таковы.

        • Сортировка пузырьком основана на обмене элементов, для чего нужна временная переменная и суммарно выполняются 3 элементарные операции. Сортировка вставками основана на присваивании элементов и требует всего 1 элементарной операции. Поэтому вычислительные затраты сортировки пузырьком обычно выше, чем у сортировки вставками.
        • Временная сложность сортировки выбором в любом случае равна \\(O(n^2)\\) . Если входные данные уже частично упорядочены, сортировка вставками обычно эффективнее сортировки выбором.
        • Сортировка выбором нестабильна, поэтому ее нельзя использовать для многоуровневой сортировки.
        ","path":["Глава 11. Сортировка","11.4   Сортировка вставками"],"tags":[]},{"location":"chapter_sorting/merge_sort/","level":1,"title":"11.6   Сортировка слиянием","text":"

        Сортировка слиянием (merge sort) - это алгоритм сортировки, основанный на стратегии «разделяй и властвуй», который включает этапы «разделения» и «слияния», показанные на рисунке 11-10.

        1. Этап разделения: массив рекурсивно делится пополам, и задача сортировки длинного массива превращается в задачи сортировки более коротких массивов.
        2. Этап слияния: когда длина подмассива становится равной 1, разделение завершается и начинается слияние. Два коротких упорядоченных массива непрерывно объединяются в один более длинный упорядоченный массив, пока процесс не завершится.

        Рисунок 11-10   Этапы разделения и слияния в сортировке слиянием

        ","path":["Глава 11. Сортировка","11.6   Сортировка слиянием"],"tags":[]},{"location":"chapter_sorting/merge_sort/#1161","level":2,"title":"11.6.1   Алгоритм","text":"

        Как показано на рисунке 11-11, на этапе «разделения» массив рекурсивно разбивается сверху вниз по середине на два подмассива.

        1. Вычислить середину массива mid и рекурсивно разделить левый подмассив (интервал [left, mid] ) и правый подмассив (интервал [mid + 1, right] ).
        2. Рекурсивно повторять шаг 1. , пока длина подмассива не станет равной 1.

        Этап «слияния» снизу вверх объединяет левый и правый подмассивы в один упорядоченный массив. Следует заметить, что начиная с подмассивов длины 1, каждый подмассив в фазе слияния уже является упорядоченным.

        <1><2><3><4><5><6><7><8><9><10>

        Рисунок 11-11   Шаги сортировки слиянием

        Нетрудно заметить, что порядок рекурсии в сортировке слиянием совпадает с порядком обхода в глубину двоичного дерева.

        • Обход в глубину: сначала рекурсивно обходится левое поддерево, затем правое поддерево, а в конце обрабатывается корневой узел.
        • Сортировка слиянием: сначала рекурсивно разделяется левый подмассив, затем правый подмассив, а в конце выполняется слияние.

        Реализация сортировки слиянием показана в коде ниже. Обратите внимание: в nums объединяемый интервал равен [left, right] , а соответствующий интервал в tmp равен [0, right - left] .

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby merge_sort.py
        def merge(nums: list[int], left: int, mid: int, right: int):\n    \"\"\"Объединить левый и правый подмассивы\"\"\"\n    # Диапазон левого подмассива: [left, mid], диапазон правого подмассива: [mid+1, right]\n    # Создать временный массив tmp для хранения результата слияния\n    tmp = [0] * (right - left + 1)\n    # Инициализировать начальные индексы левого и правого подмассивов\n    i, j, k = left, mid + 1, 0\n    # Пока в левом и правом подмассивах еще есть элементы, сравнивать их и копировать меньший во временный массив\n    while i <= mid and j <= right:\n        if nums[i] <= nums[j]:\n            tmp[k] = nums[i]\n            i += 1\n        else:\n            tmp[k] = nums[j]\n            j += 1\n        k += 1\n    # Скопировать оставшиеся элементы левого и правого подмассивов во временный массив\n    while i <= mid:\n        tmp[k] = nums[i]\n        i += 1\n        k += 1\n    while j <= right:\n        tmp[k] = nums[j]\n        j += 1\n        k += 1\n    # Скопировать элементы временного массива tmp обратно в соответствующий диапазон исходного массива nums\n    for k in range(0, len(tmp)):\n        nums[left + k] = tmp[k]\n\ndef merge_sort(nums: list[int], left: int, right: int):\n    \"\"\"Сортировка слиянием\"\"\"\n    # Условие завершения\n    if left >= right:\n        return  # Завершить рекурсию, когда длина подмассива равна 1\n    # Этап разбиения\n    mid = (left + right) // 2 # Вычислить середину\n    merge_sort(nums, left, mid)  # Рекурсивно обработать левый подмассив\n    merge_sort(nums, mid + 1, right)  # Рекурсивно обработать правый подмассив\n    # Этап слияния\n    merge(nums, left, mid, right)\n
        merge_sort.cpp
        /* Объединить левый и правый подмассивы */\nvoid merge(vector<int> &nums, int left, int mid, int right) {\n    // Диапазон левого подмассива: [left, mid], диапазон правого подмассива: [mid+1, right]\n    // Создать временный массив tmp для хранения результата слияния\n    vector<int> tmp(right - left + 1);\n    // Инициализировать начальные индексы левого и правого подмассивов\n    int i = left, j = mid + 1, k = 0;\n    // Пока в левом и правом подмассивах еще есть элементы, сравнивать их и копировать меньший во временный массив\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j])\n            tmp[k++] = nums[i++];\n        else\n            tmp[k++] = nums[j++];\n    }\n    // Скопировать оставшиеся элементы левого и правого подмассивов во временный массив\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // Скопировать элементы временного массива tmp обратно в соответствующий диапазон исходного массива nums\n    for (k = 0; k < tmp.size(); k++) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* Сортировка слиянием */\nvoid mergeSort(vector<int> &nums, int left, int right) {\n    // Условие завершения\n    if (left >= right)\n        return; // Завершить рекурсию, когда длина подмассива равна 1\n    // Этап разбиения\n    int mid = left + (right - left) / 2;    // Вычислить середину\n    mergeSort(nums, left, mid);      // Рекурсивно обработать левый подмассив\n    mergeSort(nums, mid + 1, right); // Рекурсивно обработать правый подмассив\n    // Этап слияния\n    merge(nums, left, mid, right);\n}\n
        merge_sort.java
        /* Объединить левый и правый подмассивы */\nvoid merge(int[] nums, int left, int mid, int right) {\n    // Диапазон левого подмассива: [left, mid], диапазон правого подмассива: [mid+1, right]\n    // Создать временный массив tmp для хранения результата слияния\n    int[] tmp = new int[right - left + 1];\n    // Инициализировать начальные индексы левого и правого подмассивов\n    int i = left, j = mid + 1, k = 0;\n    // Пока в левом и правом подмассивах еще есть элементы, сравнивать их и копировать меньший во временный массив\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j])\n            tmp[k++] = nums[i++];\n        else\n            tmp[k++] = nums[j++];\n    }\n    // Скопировать оставшиеся элементы левого и правого подмассивов во временный массив\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // Скопировать элементы временного массива tmp обратно в соответствующий диапазон исходного массива nums\n    for (k = 0; k < tmp.length; k++) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* Сортировка слиянием */\nvoid mergeSort(int[] nums, int left, int right) {\n    // Условие завершения\n    if (left >= right)\n        return; // Завершить рекурсию, когда длина подмассива равна 1\n    // Этап разбиения\n    int mid = left + (right - left) / 2; // Вычислить середину\n    mergeSort(nums, left, mid); // Рекурсивно обработать левый подмассив\n    mergeSort(nums, mid + 1, right); // Рекурсивно обработать правый подмассив\n    // Этап слияния\n    merge(nums, left, mid, right);\n}\n
        merge_sort.cs
        /* Объединить левый и правый подмассивы */\nvoid Merge(int[] nums, int left, int mid, int right) {\n    // Диапазон левого подмассива: [left, mid], диапазон правого подмассива: [mid+1, right]\n    // Создать временный массив tmp для хранения результата слияния\n    int[] tmp = new int[right - left + 1];\n    // Инициализировать начальные индексы левого и правого подмассивов\n    int i = left, j = mid + 1, k = 0;\n    // Пока в левом и правом подмассивах еще есть элементы, сравнивать их и копировать меньший во временный массив\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j])\n            tmp[k++] = nums[i++];\n        else\n            tmp[k++] = nums[j++];\n    }\n    // Скопировать оставшиеся элементы левого и правого подмассивов во временный массив\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // Скопировать элементы временного массива tmp обратно в соответствующий диапазон исходного массива nums\n    for (k = 0; k < tmp.Length; ++k) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* Сортировка слиянием */\nvoid MergeSort(int[] nums, int left, int right) {\n    // Условие завершения\n    if (left >= right) return;       // Завершить рекурсию, когда длина подмассива равна 1\n    // Этап разбиения\n    int mid = left + (right - left) / 2;    // Вычислить середину\n    MergeSort(nums, left, mid);      // Рекурсивно обработать левый подмассив\n    MergeSort(nums, mid + 1, right); // Рекурсивно обработать правый подмассив\n    // Этап слияния\n    Merge(nums, left, mid, right);\n}\n
        merge_sort.go
        /* Объединить левый и правый подмассивы */\nfunc merge(nums []int, left, mid, right int) {\n    // Диапазон левого подмассива: [left, mid], диапазон правого подмассива: [mid+1, right]\n    // Создать временный массив tmp для хранения результата слияния\n    tmp := make([]int, right-left+1)\n    // Инициализировать начальные индексы левого и правого подмассивов\n    i, j, k := left, mid+1, 0\n    // Пока в левом и правом подмассивах еще есть элементы, сравнивать их и копировать меньший во временный массив\n    for i <= mid && j <= right {\n        if nums[i] <= nums[j] {\n            tmp[k] = nums[i]\n            i++\n        } else {\n            tmp[k] = nums[j]\n            j++\n        }\n        k++\n    }\n    // Скопировать оставшиеся элементы левого и правого подмассивов во временный массив\n    for i <= mid {\n        tmp[k] = nums[i]\n        i++\n        k++\n    }\n    for j <= right {\n        tmp[k] = nums[j]\n        j++\n        k++\n    }\n    // Скопировать элементы временного массива tmp обратно в соответствующий диапазон исходного массива nums\n    for k := 0; k < len(tmp); k++ {\n        nums[left+k] = tmp[k]\n    }\n}\n\n/* Сортировка слиянием */\nfunc mergeSort(nums []int, left, right int) {\n    // Условие завершения\n    if left >= right {\n        return\n    }\n    // Этап разбиения\n    mid := left + (right - left) / 2\n    mergeSort(nums, left, mid)\n    mergeSort(nums, mid+1, right)\n    // Этап слияния\n    merge(nums, left, mid, right)\n}\n
        merge_sort.swift
        /* Объединить левый и правый подмассивы */\nfunc merge(nums: inout [Int], left: Int, mid: Int, right: Int) {\n    // Диапазон левого подмассива: [left, mid], диапазон правого подмассива: [mid+1, right]\n    // Создать временный массив tmp для хранения результата слияния\n    var tmp = Array(repeating: 0, count: right - left + 1)\n    // Инициализировать начальные индексы левого и правого подмассивов\n    var i = left, j = mid + 1, k = 0\n    // Пока в левом и правом подмассивах еще есть элементы, сравнивать их и копировать меньший во временный массив\n    while i <= mid, j <= right {\n        if nums[i] <= nums[j] {\n            tmp[k] = nums[i]\n            i += 1\n        } else {\n            tmp[k] = nums[j]\n            j += 1\n        }\n        k += 1\n    }\n    // Скопировать оставшиеся элементы левого и правого подмассивов во временный массив\n    while i <= mid {\n        tmp[k] = nums[i]\n        i += 1\n        k += 1\n    }\n    while j <= right {\n        tmp[k] = nums[j]\n        j += 1\n        k += 1\n    }\n    // Скопировать элементы временного массива tmp обратно в соответствующий диапазон исходного массива nums\n    for k in tmp.indices {\n        nums[left + k] = tmp[k]\n    }\n}\n\n/* Сортировка слиянием */\nfunc mergeSort(nums: inout [Int], left: Int, right: Int) {\n    // Условие завершения\n    if left >= right { // Завершить рекурсию, когда длина подмассива равна 1\n        return\n    }\n    // Этап разбиения\n    let mid = left + (right - left) / 2 // Вычислить середину\n    mergeSort(nums: &nums, left: left, right: mid) // Рекурсивно обработать левый подмассив\n    mergeSort(nums: &nums, left: mid + 1, right: right) // Рекурсивно обработать правый подмассив\n    // Этап слияния\n    merge(nums: &nums, left: left, mid: mid, right: right)\n}\n
        merge_sort.js
        /* Объединить левый и правый подмассивы */\nfunction merge(nums, left, mid, right) {\n    // Диапазон левого подмассива: [left, mid], диапазон правого подмассива: [mid+1, right]\n    // Создать временный массив tmp для хранения результата слияния\n    const tmp = new Array(right - left + 1);\n    // Инициализировать начальные индексы левого и правого подмассивов\n    let i = left,\n        j = mid + 1,\n        k = 0;\n    // Пока в левом и правом подмассивах еще есть элементы, сравнивать их и копировать меньший во временный массив\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j]) {\n            tmp[k++] = nums[i++];\n        } else {\n            tmp[k++] = nums[j++];\n        }\n    }\n    // Скопировать оставшиеся элементы левого и правого подмассивов во временный массив\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // Скопировать элементы временного массива tmp обратно в соответствующий диапазон исходного массива nums\n    for (k = 0; k < tmp.length; k++) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* Сортировка слиянием */\nfunction mergeSort(nums, left, right) {\n    // Условие завершения\n    if (left >= right) return; // Завершить рекурсию, когда длина подмассива равна 1\n    // Этап разбиения\n    let mid = Math.floor(left + (right - left) / 2); // Вычислить середину\n    mergeSort(nums, left, mid); // Рекурсивно обработать левый подмассив\n    mergeSort(nums, mid + 1, right); // Рекурсивно обработать правый подмассив\n    // Этап слияния\n    merge(nums, left, mid, right);\n}\n
        merge_sort.ts
        /* Объединить левый и правый подмассивы */\nfunction merge(nums: number[], left: number, mid: number, right: number): void {\n    // Диапазон левого подмассива: [left, mid], диапазон правого подмассива: [mid+1, right]\n    // Создать временный массив tmp для хранения результата слияния\n    const tmp = new Array(right - left + 1);\n    // Инициализировать начальные индексы левого и правого подмассивов\n    let i = left,\n        j = mid + 1,\n        k = 0;\n    // Пока в левом и правом подмассивах еще есть элементы, сравнивать их и копировать меньший во временный массив\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j]) {\n            tmp[k++] = nums[i++];\n        } else {\n            tmp[k++] = nums[j++];\n        }\n    }\n    // Скопировать оставшиеся элементы левого и правого подмассивов во временный массив\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // Скопировать элементы временного массива tmp обратно в соответствующий диапазон исходного массива nums\n    for (k = 0; k < tmp.length; k++) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* Сортировка слиянием */\nfunction mergeSort(nums: number[], left: number, right: number): void {\n    // Условие завершения\n    if (left >= right) return; // Завершить рекурсию, когда длина подмассива равна 1\n    // Этап разбиения\n    let mid = Math.floor(left + (right - left) / 2); // Вычислить середину\n    mergeSort(nums, left, mid); // Рекурсивно обработать левый подмассив\n    mergeSort(nums, mid + 1, right); // Рекурсивно обработать правый подмассив\n    // Этап слияния\n    merge(nums, left, mid, right);\n}\n
        merge_sort.dart
        /* Объединить левый и правый подмассивы */\nvoid merge(List<int> nums, int left, int mid, int right) {\n  // Диапазон левого подмассива: [left, mid], диапазон правого подмассива: [mid+1, right]\n  // Создать временный массив tmp для хранения результата слияния\n  List<int> tmp = List.filled(right - left + 1, 0);\n  // Инициализировать начальные индексы левого и правого подмассивов\n  int i = left, j = mid + 1, k = 0;\n  // Пока в левом и правом подмассивах еще есть элементы, сравнивать их и копировать меньший во временный массив\n  while (i <= mid && j <= right) {\n    if (nums[i] <= nums[j])\n      tmp[k++] = nums[i++];\n    else\n      tmp[k++] = nums[j++];\n  }\n  // Скопировать оставшиеся элементы левого и правого подмассивов во временный массив\n  while (i <= mid) {\n    tmp[k++] = nums[i++];\n  }\n  while (j <= right) {\n    tmp[k++] = nums[j++];\n  }\n  // Скопировать элементы временного массива tmp обратно в соответствующий диапазон исходного массива nums\n  for (k = 0; k < tmp.length; k++) {\n    nums[left + k] = tmp[k];\n  }\n}\n\n/* Сортировка слиянием */\nvoid mergeSort(List<int> nums, int left, int right) {\n  // Условие завершения\n  if (left >= right) return; // Завершить рекурсию, когда длина подмассива равна 1\n  // Этап разбиения\n  int mid = left + (right - left) ~/ 2; // Вычислить середину\n  mergeSort(nums, left, mid); // Рекурсивно обработать левый подмассив\n  mergeSort(nums, mid + 1, right); // Рекурсивно обработать правый подмассив\n  // Этап слияния\n  merge(nums, left, mid, right);\n}\n
        merge_sort.rs
        /* Объединить левый и правый подмассивы */\nfn merge(nums: &mut [i32], left: usize, mid: usize, right: usize) {\n    // Диапазон левого подмассива: [left, mid], диапазон правого подмассива: [mid+1, right]\n    // Создать временный массив tmp для хранения результата слияния\n    let tmp_size = right - left + 1;\n    let mut tmp = vec![0; tmp_size];\n    // Инициализировать начальные индексы левого и правого подмассивов\n    let (mut i, mut j, mut k) = (left, mid + 1, 0);\n    // Пока в левом и правом подмассивах еще есть элементы, сравнивать их и копировать меньший во временный массив\n    while i <= mid && j <= right {\n        if nums[i] <= nums[j] {\n            tmp[k] = nums[i];\n            i += 1;\n        } else {\n            tmp[k] = nums[j];\n            j += 1;\n        }\n        k += 1;\n    }\n    // Скопировать оставшиеся элементы левого и правого подмассивов во временный массив\n    while i <= mid {\n        tmp[k] = nums[i];\n        k += 1;\n        i += 1;\n    }\n    while j <= right {\n        tmp[k] = nums[j];\n        k += 1;\n        j += 1;\n    }\n    // Скопировать элементы временного массива tmp обратно в соответствующий диапазон исходного массива nums\n    for k in 0..tmp_size {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* Сортировка слиянием */\nfn merge_sort(nums: &mut [i32], left: usize, right: usize) {\n    // Условие завершения\n    if left >= right {\n        return; // Завершить рекурсию, когда длина подмассива равна 1\n    }\n\n    // Этап разбиения\n    let mid = left + (right - left) / 2; // Вычислить середину\n    merge_sort(nums, left, mid); // Рекурсивно обработать левый подмассив\n    merge_sort(nums, mid + 1, right); // Рекурсивно обработать правый подмассив\n\n    // Этап слияния\n    merge(nums, left, mid, right);\n}\n
        merge_sort.c
        /* Объединить левый и правый подмассивы */\nvoid merge(int *nums, int left, int mid, int right) {\n    // Диапазон левого подмассива: [left, mid], диапазон правого подмассива: [mid+1, right]\n    // Создать временный массив tmp для хранения результата слияния\n    int tmpSize = right - left + 1;\n    int *tmp = (int *)malloc(tmpSize * sizeof(int));\n    // Инициализировать начальные индексы левого и правого подмассивов\n    int i = left, j = mid + 1, k = 0;\n    // Пока в левом и правом подмассивах еще есть элементы, сравнивать их и копировать меньший во временный массив\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j]) {\n            tmp[k++] = nums[i++];\n        } else {\n            tmp[k++] = nums[j++];\n        }\n    }\n    // Скопировать оставшиеся элементы левого и правого подмассивов во временный массив\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // Скопировать элементы временного массива tmp обратно в соответствующий диапазон исходного массива nums\n    for (k = 0; k < tmpSize; ++k) {\n        nums[left + k] = tmp[k];\n    }\n    // Освободить память\n    free(tmp);\n}\n\n/* Сортировка слиянием */\nvoid mergeSort(int *nums, int left, int right) {\n    // Условие завершения\n    if (left >= right)\n        return; // Завершить рекурсию, когда длина подмассива равна 1\n    // Этап разбиения\n    int mid = left + (right - left) / 2;    // Вычислить середину\n    mergeSort(nums, left, mid);      // Рекурсивно обработать левый подмассив\n    mergeSort(nums, mid + 1, right); // Рекурсивно обработать правый подмассив\n    // Этап слияния\n    merge(nums, left, mid, right);\n}\n
        merge_sort.kt
        /* Объединить левый и правый подмассивы */\nfun merge(nums: IntArray, left: Int, mid: Int, right: Int) {\n    // Диапазон левого подмассива: [left, mid], диапазон правого подмассива: [mid+1, right]\n    // Создать временный массив tmp для хранения результата слияния\n    val tmp = IntArray(right - left + 1)\n    // Инициализировать начальные индексы левого и правого подмассивов\n    var i = left\n    var j = mid + 1\n    var k = 0\n    // Пока в левом и правом подмассивах еще есть элементы, сравнивать их и копировать меньший во временный массив\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j])\n            tmp[k++] = nums[i++]\n        else\n            tmp[k++] = nums[j++]\n    }\n    // Скопировать оставшиеся элементы левого и правого подмассивов во временный массив\n    while (i <= mid) {\n        tmp[k++] = nums[i++]\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++]\n    }\n    // Скопировать элементы временного массива tmp обратно в соответствующий диапазон исходного массива nums\n    for (l in tmp.indices) {\n        nums[left + l] = tmp[l]\n    }\n}\n\n/* Сортировка слиянием */\nfun mergeSort(nums: IntArray, left: Int, right: Int) {\n    // Условие завершения\n    if (left >= right) return  // Завершить рекурсию, когда длина подмассива равна 1\n    // Этап разбиения\n    val mid = left + (right - left) / 2 // Вычислить середину\n    mergeSort(nums, left, mid) // Рекурсивно обработать левый подмассив\n    mergeSort(nums, mid + 1, right) // Рекурсивно обработать правый подмассив\n    // Этап слияния\n    merge(nums, left, mid, right)\n}\n
        merge_sort.rb
        ### Слияние левого и правого подмассивов ###\ndef merge(nums, left, mid, right)\n  # Интервал левого подмассива: [left, mid], правого подмассива: [mid+1, right]\n  # Создать временный массив tmp для хранения результата слияния\n  tmp = Array.new(right - left + 1, 0)\n  # Инициализировать начальные индексы левого и правого подмассивов\n  i, j, k = left, mid + 1, 0\n  # Пока в левом и правом подмассивах еще есть элементы, сравнивать их и копировать меньший во временный массив\n  while i <= mid && j <= right\n    if nums[i] <= nums[j]\n      tmp[k] = nums[i]\n      i += 1\n    else\n      tmp[k] = nums[j]\n      j += 1\n    end\n    k += 1\n  end\n  # Скопировать оставшиеся элементы левого и правого подмассивов во временный массив\n  while i <= mid\n    tmp[k] = nums[i]\n    i += 1\n    k += 1\n  end\n  while j <= right\n    tmp[k] = nums[j]\n    j += 1\n    k += 1\n  end\n  # Скопировать элементы временного массива tmp обратно в соответствующий диапазон исходного массива nums\n  (0...tmp.length).each do |k|\n    nums[left + k] = tmp[k]\n  end\nend\n\n### Сортировка слиянием ###\ndef merge_sort(nums, left, right)\n  # Условие завершения\n  # Когда длина подмассива равна 1, рекурсия завершается\n  return if left >= right\n  # Этап разбиения\n  mid = left + (right - left) / 2 # Вычислить середину\n  merge_sort(nums, left, mid) # Рекурсивно обработать левый подмассив\n  merge_sort(nums, mid + 1, right) # Рекурсивно обработать правый подмассив\n  # Этап слияния\n  merge(nums, left, mid, right)\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 11. Сортировка","11.6   Сортировка слиянием"],"tags":[]},{"location":"chapter_sorting/merge_sort/#1162","level":2,"title":"11.6.2   Характеристики алгоритма","text":"
        • Временная сложность равна \\(O(n \\log n)\\), алгоритм не является адаптивным: этап разделения создает дерево рекурсии высоты \\(\\log n\\) , а суммарное число операций слияния на каждом уровне равно \\(n\\) , поэтому общая временная сложность составляет \\(O(n \\log n)\\) .
        • Пространственная сложность равна \\(O(n)\\), сортировка не выполняется на месте: глубина рекурсии равна \\(\\log n\\) , из-за чего требуется \\(O(\\log n)\\) памяти под стек вызовов. Для этапа слияния нужен вспомогательный массив, поэтому дополнительно используется \\(O(n)\\) памяти.
        • Стабильная сортировка: в процессе слияния относительный порядок равных элементов не меняется.
        ","path":["Глава 11. Сортировка","11.6   Сортировка слиянием"],"tags":[]},{"location":"chapter_sorting/merge_sort/#1163","level":2,"title":"11.6.3   Сортировка связного списка","text":"

        Для связных списков сортировка слиянием имеет заметное преимущество перед другими алгоритмами сортировки: пространственную сложность задачи сортировки списка можно оптимизировать до \\(O(1)\\).

        • Этап разделения: работу по разбиению списка можно реализовать с помощью «итерации» вместо «рекурсии», тем самым устранив расход памяти на стек вызовов.
        • Этап слияния: в связном списке добавление и удаление узлов требует только изменения ссылок (указателей), поэтому при слиянии двух коротких упорядоченных списков в один длинный упорядоченный список не нужно создавать дополнительный список.

        Детали реализации достаточно сложны. Заинтересованные читатели могут обратиться к соответствующим материалам самостоятельно.

        ","path":["Глава 11. Сортировка","11.6   Сортировка слиянием"],"tags":[]},{"location":"chapter_sorting/quick_sort/","level":1,"title":"11.5   Быстрая сортировка","text":"

        Быстрая сортировка (quick sort) - это алгоритм сортировки, основанный на стратегии «разделяй и властвуй». Он работает эффективно и применяется очень широко.

        Ключевая операция быстрой сортировки - это «разделение с опорным элементом». Ее цель такова: выбрать некоторый элемент массива в качестве «опорного» и переместить все элементы меньше опорного влево от него, а все элементы больше опорного - вправо. Конкретный процесс показан на рисунке 11-8.

        1. Выбрать самый левый элемент массива как опорный и инициализировать два указателя i и j , направленные на левую и правую границы массива.
        2. Запустить цикл, в котором i и j ищут соответственно первый элемент, больший опорного, и первый элемент, меньший опорного, после чего эти два элемента меняются местами.
        3. Повторять шаг 2. , пока указатели i и j не встретятся, а затем обменять опорный элемент с элементом на границе двух подмассивов.
        <1><2><3><4><5><6><7><8><9>

        Рисунок 11-8   Шаги разделения с опорным элементом

        После завершения разделения исходный массив разбивается на три части: левый подмассив, опорный элемент и правый подмассив. При этом выполняется условие «любой элемент левого подмассива \\(\\leq\\) опорный элемент \\(\\leq\\) любой элемент правого подмассива». Следовательно, далее нам нужно лишь отсортировать эти два подмассива.

        Стратегия разделяй и властвуй в быстрой сортировке

        Иными словами, разделение с опорным элементом сводит задачу сортировки длинного массива к двум задачам сортировки более коротких массивов.

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py
        def partition(self, nums: list[int], left: int, right: int) -> int:\n    \"\"\"Разбиение с опорными указателями\"\"\"\n    # Взять nums[left] в качестве опорного элемента\n    i, j = left, right\n    while i < j:\n        while i < j and nums[j] >= nums[left]:\n            j -= 1  # Идти справа налево в поисках первого элемента меньше опорного\n        while i < j and nums[i] <= nums[left]:\n            i += 1  # Идти слева направо в поисках первого элемента больше опорного\n        # Обмен элементов\n        nums[i], nums[j] = nums[j], nums[i]\n    # Переместить опорный элемент на границу двух подмассивов\n    nums[i], nums[left] = nums[left], nums[i]\n    return i  # Вернуть индекс опорного элемента\n
        quick_sort.cpp
        /* Разбиение с опорными указателями */\nint partition(vector<int> &nums, int left, int right) {\n    // Взять nums[left] в качестве опорного элемента\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;                // Идти справа налево в поисках первого элемента меньше опорного\n        while (i < j && nums[i] <= nums[left])\n            i++;                // Идти слева направо в поисках первого элемента больше опорного\n        swap(nums[i], nums[j]); // Поменять эти два элемента местами\n    }\n    swap(nums[i], nums[left]);  // Переместить опорный элемент на границу двух подмассивов\n    return i;                   // Вернуть индекс опорного элемента\n}\n
        quick_sort.java
        /* Обмен элементов */\nvoid swap(int[] nums, int i, int j) {\n    int tmp = nums[i];\n    nums[i] = nums[j];\n    nums[j] = tmp;\n}\n\n/* Разбиение с опорными указателями */\nint partition(int[] nums, int left, int right) {\n    // Взять nums[left] в качестве опорного элемента\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;          // Идти справа налево в поисках первого элемента меньше опорного\n        while (i < j && nums[i] <= nums[left])\n            i++;          // Идти слева направо в поисках первого элемента больше опорного\n        swap(nums, i, j); // Поменять эти два элемента местами\n    }\n    swap(nums, i, left);  // Переместить опорный элемент на границу двух подмассивов\n    return i;             // Вернуть индекс опорного элемента\n}\n
        quick_sort.cs
        /* Обмен элементов */\nvoid Swap(int[] nums, int i, int j) {\n    (nums[j], nums[i]) = (nums[i], nums[j]);\n}\n\n/* Разбиение с опорными указателями */\nint Partition(int[] nums, int left, int right) {\n    // Взять nums[left] в качестве опорного элемента\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;          // Идти справа налево в поисках первого элемента меньше опорного\n        while (i < j && nums[i] <= nums[left])\n            i++;          // Идти слева направо в поисках первого элемента больше опорного\n        Swap(nums, i, j); // Поменять эти два элемента местами\n    }\n    Swap(nums, i, left);  // Переместить опорный элемент на границу двух подмассивов\n    return i;             // Вернуть индекс опорного элемента\n}\n
        quick_sort.go
        /* Разбиение с опорными указателями */\nfunc (q *quickSort) partition(nums []int, left, right int) int {\n    // Взять nums[left] в качестве опорного элемента\n    i, j := left, right\n    for i < j {\n        for i < j && nums[j] >= nums[left] {\n            j-- // Идти справа налево в поисках первого элемента меньше опорного\n        }\n        for i < j && nums[i] <= nums[left] {\n            i++ // Идти слева направо в поисках первого элемента больше опорного\n        }\n        // Обмен элементов\n        nums[i], nums[j] = nums[j], nums[i]\n    }\n    // Переместить опорный элемент на границу двух подмассивов\n    nums[i], nums[left] = nums[left], nums[i]\n    return i // Вернуть индекс опорного элемента\n}\n
        quick_sort.swift
        /* Разбиение с опорными указателями */\nfunc partition(nums: inout [Int], left: Int, right: Int) -> Int {\n    // Взять nums[left] в качестве опорного элемента\n    var i = left\n    var j = right\n    while i < j {\n        while i < j, nums[j] >= nums[left] {\n            j -= 1 // Идти справа налево в поисках первого элемента меньше опорного\n        }\n        while i < j, nums[i] <= nums[left] {\n            i += 1 // Идти слева направо в поисках первого элемента больше опорного\n        }\n        nums.swapAt(i, j) // Поменять эти два элемента местами\n    }\n    nums.swapAt(i, left) // Переместить опорный элемент на границу двух подмассивов\n    return i // Вернуть индекс опорного элемента\n}\n
        quick_sort.js
        /* Обмен элементов */\nswap(nums, i, j) {\n    let tmp = nums[i];\n    nums[i] = nums[j];\n    nums[j] = tmp;\n}\n\n/* Разбиение с опорными указателями */\npartition(nums, left, right) {\n    // Взять nums[left] в качестве опорного элемента\n    let i = left,\n        j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) {\n            j -= 1; // Идти справа налево в поисках первого элемента меньше опорного\n        }\n        while (i < j && nums[i] <= nums[left]) {\n            i += 1; // Идти слева направо в поисках первого элемента больше опорного\n        }\n        // Обмен элементов\n        this.swap(nums, i, j); // Поменять эти два элемента местами\n    }\n    this.swap(nums, i, left); // Переместить опорный элемент на границу двух подмассивов\n    return i; // Вернуть индекс опорного элемента\n}\n
        quick_sort.ts
        /* Обмен элементов */\nswap(nums: number[], i: number, j: number): void {\n    let tmp = nums[i];\n    nums[i] = nums[j];\n    nums[j] = tmp;\n}\n\n/* Разбиение с опорными указателями */\npartition(nums: number[], left: number, right: number): number {\n    // Взять nums[left] в качестве опорного элемента\n    let i = left,\n        j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) {\n            j -= 1; // Идти справа налево в поисках первого элемента меньше опорного\n        }\n        while (i < j && nums[i] <= nums[left]) {\n            i += 1; // Идти слева направо в поисках первого элемента больше опорного\n        }\n        // Обмен элементов\n        this.swap(nums, i, j); // Поменять эти два элемента местами\n    }\n    this.swap(nums, i, left); // Переместить опорный элемент на границу двух подмассивов\n    return i; // Вернуть индекс опорного элемента\n}\n
        quick_sort.dart
        /* Обмен элементов */\nvoid _swap(List<int> nums, int i, int j) {\n  int tmp = nums[i];\n  nums[i] = nums[j];\n  nums[j] = tmp;\n}\n\n/* Разбиение с опорными указателями */\nint _partition(List<int> nums, int left, int right) {\n  // Взять nums[left] в качестве опорного элемента\n  int i = left, j = right;\n  while (i < j) {\n    while (i < j && nums[j] >= nums[left]) j--; // Идти справа налево в поисках первого элемента меньше опорного\n    while (i < j && nums[i] <= nums[left]) i++; // Идти слева направо в поисках первого элемента больше опорного\n    _swap(nums, i, j); // Поменять эти два элемента местами\n  }\n  _swap(nums, i, left); // Переместить опорный элемент на границу двух подмассивов\n  return i; // Вернуть индекс опорного элемента\n}\n
        quick_sort.rs
        /* Разбиение с опорными указателями */\nfn partition(nums: &mut [i32], left: usize, right: usize) -> usize {\n    // Взять nums[left] в качестве опорного элемента\n    let (mut i, mut j) = (left, right);\n    while i < j {\n        while i < j && nums[j] >= nums[left] {\n            j -= 1; // Идти справа налево в поисках первого элемента меньше опорного\n        }\n        while i < j && nums[i] <= nums[left] {\n            i += 1; // Идти слева направо в поисках первого элемента больше опорного\n        }\n        nums.swap(i, j); // Поменять эти два элемента местами\n    }\n    nums.swap(i, left); // Переместить опорный элемент на границу двух подмассивов\n    i // Вернуть индекс опорного элемента\n}\n
        quick_sort.c
        /* Обмен элементов */\nvoid swap(int nums[], int i, int j) {\n    int tmp = nums[i];\n    nums[i] = nums[j];\n    nums[j] = tmp;\n}\n\n/* Разбиение с опорными указателями */\nint partition(int nums[], int left, int right) {\n    // Взять nums[left] в качестве опорного элемента\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) {\n            j--; // Идти справа налево в поисках первого элемента меньше опорного\n        }\n        while (i < j && nums[i] <= nums[left]) {\n            i++; // Идти слева направо в поисках первого элемента больше опорного\n        }\n        // Поменять эти два элемента местами\n        swap(nums, i, j);\n    }\n    // Переместить опорный элемент на границу двух подмассивов\n    swap(nums, i, left);\n    // Вернуть индекс опорного элемента\n    return i;\n}\n
        quick_sort.kt
        /* Обмен элементов */\nfun swap(nums: IntArray, i: Int, j: Int) {\n    val temp = nums[i]\n    nums[i] = nums[j]\n    nums[j] = temp\n}\n\n/* Разбиение с опорными указателями */\nfun partition(nums: IntArray, left: Int, right: Int): Int {\n    // Взять nums[left] в качестве опорного элемента\n    var i = left\n    var j = right\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--           // Идти справа налево в поисках первого элемента меньше опорного\n        while (i < j && nums[i] <= nums[left])\n            i++           // Идти слева направо в поисках первого элемента больше опорного\n        swap(nums, i, j)  // Поменять эти два элемента местами\n    }\n    swap(nums, i, left)   // Переместить опорный элемент на границу двух подмассивов\n    return i              // Вернуть индекс опорного элемента\n}\n
        quick_sort.rb
        ### Разбиение с опорными указателями ###\ndef partition(nums, left, right)\n  # Взять nums[left] в качестве опорного элемента\n  i, j = left, right\n  while i < j\n    while i < j && nums[j] >= nums[left]\n      j -= 1 # Идти справа налево в поисках первого элемента меньше опорного\n    end\n    while i < j && nums[i] <= nums[left]\n      i += 1 # Идти слева направо в поисках первого элемента больше опорного\n    end\n    # Обмен элементов\n    nums[i], nums[j] = nums[j], nums[i]\n  end\n  # Переместить опорный элемент на границу двух подмассивов\n  nums[i], nums[left] = nums[left], nums[i]\n  i # Вернуть индекс опорного элемента\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 11. Сортировка","11.5   Быстрая сортировка"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1151","level":2,"title":"11.5.1   Алгоритм","text":"

        Общий процесс быстрой сортировки показан на рисунке 11-9.

        1. Сначала выполнить «разделение с опорным элементом» для исходного массива и получить неотсортированные левый и правый подмассивы.
        2. Затем рекурсивно выполнить «разделение с опорным элементом» для левого и правого подмассивов.
        3. Продолжать рекурсию до тех пор, пока длина подмассива не станет равной 1. После этого сортировка всего массива будет завершена.

        Рисунок 11-9   Процесс быстрой сортировки

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py
        def quick_sort(self, nums: list[int], left: int, right: int):\n    \"\"\"Быстрая сортировка\"\"\"\n    # Завершить рекурсию, когда длина подмассива равна 1\n    if left >= right:\n        return\n    # Разбиение с опорными указателями\n    pivot = self.partition(nums, left, right)\n    # Рекурсивно обработать левый и правый подмассивы\n    self.quick_sort(nums, left, pivot - 1)\n    self.quick_sort(nums, pivot + 1, right)\n
        quick_sort.cpp
        /* Быстрая сортировка */\nvoid quickSort(vector<int> &nums, int left, int right) {\n    // Завершить рекурсию, когда длина подмассива равна 1\n    if (left >= right)\n        return;\n    // Разбиение с опорными указателями\n    int pivot = partition(nums, left, right);\n    // Рекурсивно обработать левый и правый подмассивы\n    quickSort(nums, left, pivot - 1);\n    quickSort(nums, pivot + 1, right);\n}\n
        quick_sort.java
        /* Быстрая сортировка */\nvoid quickSort(int[] nums, int left, int right) {\n    // Завершить рекурсию, когда длина подмассива равна 1\n    if (left >= right)\n        return;\n    // Разбиение с опорными указателями\n    int pivot = partition(nums, left, right);\n    // Рекурсивно обработать левый и правый подмассивы\n    quickSort(nums, left, pivot - 1);\n    quickSort(nums, pivot + 1, right);\n}\n
        quick_sort.cs
        /* Быстрая сортировка */\nvoid QuickSort(int[] nums, int left, int right) {\n    // Завершить рекурсию, когда длина подмассива равна 1\n    if (left >= right)\n        return;\n    // Разбиение с опорными указателями\n    int pivot = Partition(nums, left, right);\n    // Рекурсивно обработать левый и правый подмассивы\n    QuickSort(nums, left, pivot - 1);\n    QuickSort(nums, pivot + 1, right);\n}\n
        quick_sort.go
        /* Быстрая сортировка */\nfunc (q *quickSort) quickSort(nums []int, left, right int) {\n    // Завершить рекурсию, когда длина подмассива равна 1\n    if left >= right {\n        return\n    }\n    // Разбиение с опорными указателями\n    pivot := q.partition(nums, left, right)\n    // Рекурсивно обработать левый и правый подмассивы\n    q.quickSort(nums, left, pivot-1)\n    q.quickSort(nums, pivot+1, right)\n}\n
        quick_sort.swift
        /* Быстрая сортировка */\nfunc quickSort(nums: inout [Int], left: Int, right: Int) {\n    // Завершить рекурсию, когда длина подмассива равна 1\n    if left >= right {\n        return\n    }\n    // Разбиение с опорными указателями\n    let pivot = partition(nums: &nums, left: left, right: right)\n    // Рекурсивно обработать левый и правый подмассивы\n    quickSort(nums: &nums, left: left, right: pivot - 1)\n    quickSort(nums: &nums, left: pivot + 1, right: right)\n}\n
        quick_sort.js
        /* Быстрая сортировка */\nquickSort(nums, left, right) {\n    // Завершить рекурсию, когда длина подмассива равна 1\n    if (left >= right) return;\n    // Разбиение с опорными указателями\n    const pivot = this.partition(nums, left, right);\n    // Рекурсивно обработать левый и правый подмассивы\n    this.quickSort(nums, left, pivot - 1);\n    this.quickSort(nums, pivot + 1, right);\n}\n
        quick_sort.ts
        /* Быстрая сортировка */\nquickSort(nums: number[], left: number, right: number): void {\n    // Завершить рекурсию, когда длина подмассива равна 1\n    if (left >= right) {\n        return;\n    }\n    // Разбиение с опорными указателями\n    const pivot = this.partition(nums, left, right);\n    // Рекурсивно обработать левый и правый подмассивы\n    this.quickSort(nums, left, pivot - 1);\n    this.quickSort(nums, pivot + 1, right);\n}\n
        quick_sort.dart
        /* Быстрая сортировка */\nvoid quickSort(List<int> nums, int left, int right) {\n  // Завершить рекурсию, когда длина подмассива равна 1\n  if (left >= right) return;\n  // Разбиение с опорными указателями\n  int pivot = _partition(nums, left, right);\n  // Рекурсивно обработать левый и правый подмассивы\n  quickSort(nums, left, pivot - 1);\n  quickSort(nums, pivot + 1, right);\n}\n
        quick_sort.rs
        /* Быстрая сортировка */\npub fn quick_sort(left: i32, right: i32, nums: &mut [i32]) {\n    // Завершить рекурсию, когда длина подмассива равна 1\n    if left >= right {\n        return;\n    }\n    // Разбиение с опорными указателями\n    let pivot = Self::partition(nums, left as usize, right as usize) as i32;\n    // Рекурсивно обработать левый и правый подмассивы\n    Self::quick_sort(left, pivot - 1, nums);\n    Self::quick_sort(pivot + 1, right, nums);\n}\n
        quick_sort.c
        /* Быстрая сортировка */\nvoid quickSort(int nums[], int left, int right) {\n    // Завершить рекурсию, когда длина подмассива равна 1\n    if (left >= right) {\n        return;\n    }\n    // Разбиение с опорными указателями\n    int pivot = partition(nums, left, right);\n    // Рекурсивно обработать левый и правый подмассивы\n    quickSort(nums, left, pivot - 1);\n    quickSort(nums, pivot + 1, right);\n}\n
        quick_sort.kt
        /* Быстрая сортировка */\nfun quickSort(nums: IntArray, left: Int, right: Int) {\n    // Завершить рекурсию, когда длина подмассива равна 1\n    if (left >= right) return\n    // Разбиение с опорными указателями\n    val pivot = partition(nums, left, right)\n    // Рекурсивно обработать левый и правый подмассивы\n    quickSort(nums, left, pivot - 1)\n    quickSort(nums, pivot + 1, right)\n}\n
        quick_sort.rb
        ### Класс быстрой сортировки ###\ndef quick_sort(nums, left, right)\n  # Рекурсивно обрабатывать, пока длина подмассива не станет равной 1\n  if left < right\n    # Разбиение с опорными указателями\n    pivot = partition(nums, left, right)\n    # Рекурсивно обработать левый и правый подмассивы\n    quick_sort(nums, left, pivot - 1)\n    quick_sort(nums, pivot + 1, right)\n  end\n  nums\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 11. Сортировка","11.5   Быстрая сортировка"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1152","level":2,"title":"11.5.2   Характеристики алгоритма","text":"
        • Временная сложность равна \\(O(n \\log n)\\), алгоритм не является адаптивным: в среднем глубина рекурсии при разделении равна \\(\\log n\\) , а суммарное число циклов на каждом уровне равно \\(n\\) , поэтому общая сложность составляет \\(O(n \\log n)\\) . В худшем случае каждое разделение делит массив длины \\(n\\) на подмассивы длины \\(0\\) и \\(n - 1\\). Тогда глубина рекурсии достигает \\(n\\) , на каждом уровне выполняется \\(n\\) операций, и общая временная сложность вырождается в \\(O(n^2)\\) .
        • Пространственная сложность равна \\(O(n)\\), сортировка выполняется на месте: если входной массив полностью отсортирован в обратном порядке, глубина рекурсии достигает худшего случая \\(n\\) , что требует \\(O(n)\\) памяти под стек вызовов. При этом сама сортировка выполняется в исходном массиве без дополнительного массива.
        • Нестабильная сортировка: на последнем шаге разделения опорный элемент может быть обменян вправо от равного ему элемента.
        ","path":["Глава 11. Сортировка","11.5   Быстрая сортировка"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1153","level":2,"title":"11.5.3   Почему быстрая сортировка быстрая","text":"

        Уже по названию понятно, что быстрая сортировка должна иметь преимущества по эффективности. Хотя ее средняя временная сложность совпадает со сложностью «сортировки слиянием» и «пирамидальной сортировки», на практике быстрая сортировка обычно работает быстрее. Основные причины таковы.

        • Вероятность худшего случая очень мала: хотя худшая временная сложность быстрой сортировки равна \\(O(n^2)\\) и она не так стабильна, как сортировка слиянием, в подавляющем большинстве случаев она работает за \\(O(n \\log n)\\) .
        • Высокая эффективность использования кэша: при выполнении разделения система может загрузить весь подмассив в кэш, поэтому доступ к элементам оказывается быстрым. Алгоритмы вроде «пирамидальной сортировки» требуют скачкообразного доступа к элементам и таким свойством не обладают.
        • Небольшой константный множитель в сложности: среди трех перечисленных алгоритмов у быстрой сортировки обычно меньше всего сравнений, присваиваний и обменов. Это похоже на причину, по которой «сортировка вставками» часто быстрее «сортировки пузырьком».
        ","path":["Глава 11. Сортировка","11.5   Быстрая сортировка"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1154","level":2,"title":"11.5.4   Оптимизация выбора опорного элемента","text":"

        На некоторых входных данных временная эффективность быстрой сортировки может ухудшаться. Рассмотрим крайний случай: входной массив полностью отсортирован в обратном порядке. Поскольку в качестве опорного мы выбираем самый левый элемент, после разделения он будет обменян в самый правый конец массива, из-за чего длина левого подмассива станет \\(n - 1\\) , а длина правого - \\(0\\) . Если рекурсия будет продолжаться таким образом, то после каждого разделения один из подмассивов будет иметь длину \\(0\\) , стратегия «разделяй и властвуй» потеряет смысл, а быстрая сортировка выродится в нечто близкое к «сортировке пузырьком».

        Чтобы по возможности избежать такого сценария, можно улучшить стратегию выбора опорного элемента в процедуре разделения. Например, можно выбирать случайный элемент массива как опорный. Однако если не повезет и каждый раз будет выбираться неудачный опорный элемент, производительность все равно останется неудовлетворительной.

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

        Чтобы улучшить ситуацию, можно взять три кандидата (обычно первый, последний и средний элементы массива) и использовать медиану этих трех значений как опорный элемент. Благодаря этому вероятность того, что опорный элемент окажется «не слишком маленьким и не слишком большим», заметно возрастает. Конечно, можно брать и большее число кандидатов, чтобы еще сильнее повысить устойчивость алгоритма. После этого вероятность деградации временной сложности до \\(O(n^2)\\) существенно уменьшается.

        Пример кода:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py
        def median_three(self, nums: list[int], left: int, mid: int, right: int) -> int:\n    \"\"\"Выбрать медиану из трех кандидатов\"\"\"\n    l, m, r = nums[left], nums[mid], nums[right]\n    if (l <= m <= r) or (r <= m <= l):\n        return mid  # m находится между l и r\n    if (m <= l <= r) or (r <= l <= m):\n        return left  # l находится между m и r\n    return right\n\ndef partition(self, nums: list[int], left: int, right: int) -> int:\n    \"\"\"Разбиение с опорными указателями (медиана трех)\"\"\"\n    # Взять nums[left] в качестве опорного элемента\n    med = self.median_three(nums, left, (left + right) // 2, right)\n    # Переместить медиану в крайний левый элемент массива\n    nums[left], nums[med] = nums[med], nums[left]\n    # Взять nums[left] в качестве опорного элемента\n    i, j = left, right\n    while i < j:\n        while i < j and nums[j] >= nums[left]:\n            j -= 1  # Идти справа налево в поисках первого элемента меньше опорного\n        while i < j and nums[i] <= nums[left]:\n            i += 1  # Идти слева направо в поисках первого элемента больше опорного\n        # Обмен элементов\n        nums[i], nums[j] = nums[j], nums[i]\n    # Переместить опорный элемент на границу двух подмассивов\n    nums[i], nums[left] = nums[left], nums[i]\n    return i  # Вернуть индекс опорного элемента\n
        quick_sort.cpp
        /* Выбрать медиану из трех кандидатов */\nint medianThree(vector<int> &nums, int left, int mid, int right) {\n    int l = nums[left], m = nums[mid], r = nums[right];\n    if ((l <= m && m <= r) || (r <= m && m <= l))\n        return mid; // m находится между l и r\n    if ((m <= l && l <= r) || (r <= l && l <= m))\n        return left; // l находится между m и r\n    return right;\n}\n\n/* Разбиение с опорными указателями (медиана трех) */\nint partition(vector<int> &nums, int left, int right) {\n    // Выбрать медиану из трех кандидатов\n    int med = medianThree(nums, left, (left + right) / 2, right);\n    // Переместить медиану в крайний левый элемент массива\n    swap(nums[left], nums[med]);\n    // Взять nums[left] в качестве опорного элемента\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;                // Идти справа налево в поисках первого элемента меньше опорного\n        while (i < j && nums[i] <= nums[left])\n            i++;                // Идти слева направо в поисках первого элемента больше опорного\n        swap(nums[i], nums[j]); // Поменять эти два элемента местами\n    }\n    swap(nums[i], nums[left]);  // Переместить опорный элемент на границу двух подмассивов\n    return i;                   // Вернуть индекс опорного элемента\n}\n
        quick_sort.java
        /* Выбрать медиану из трех кандидатов */\nint medianThree(int[] nums, int left, int mid, int right) {\n    int l = nums[left], m = nums[mid], r = nums[right];\n    if ((l <= m && m <= r) || (r <= m && m <= l))\n        return mid; // m находится между l и r\n    if ((m <= l && l <= r) || (r <= l && l <= m))\n        return left; // l находится между m и r\n    return right;\n}\n\n/* Разбиение с опорными указателями (медиана трех) */\nint partition(int[] nums, int left, int right) {\n    // Выбрать медиану из трех кандидатов\n    int med = medianThree(nums, left, (left + right) / 2, right);\n    // Переместить медиану в крайний левый элемент массива\n    swap(nums, left, med);\n    // Взять nums[left] в качестве опорного элемента\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;          // Идти справа налево в поисках первого элемента меньше опорного\n        while (i < j && nums[i] <= nums[left])\n            i++;          // Идти слева направо в поисках первого элемента больше опорного\n        swap(nums, i, j); // Поменять эти два элемента местами\n    }\n    swap(nums, i, left);  // Переместить опорный элемент на границу двух подмассивов\n    return i;             // Вернуть индекс опорного элемента\n}\n
        quick_sort.cs
        /* Выбрать медиану из трех кандидатов */\nint MedianThree(int[] nums, int left, int mid, int right) {\n    int l = nums[left], m = nums[mid], r = nums[right];\n    if ((l <= m && m <= r) || (r <= m && m <= l))\n        return mid; // m находится между l и r\n    if ((m <= l && l <= r) || (r <= l && l <= m))\n        return left; // l находится между m и r\n    return right;\n}\n\n/* Разбиение с опорными указателями (медиана трех) */\nint Partition(int[] nums, int left, int right) {\n    // Выбрать медиану из трех кандидатов\n    int med = MedianThree(nums, left, (left + right) / 2, right);\n    // Переместить медиану в крайний левый элемент массива\n    Swap(nums, left, med);\n    // Взять nums[left] в качестве опорного элемента\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;          // Идти справа налево в поисках первого элемента меньше опорного\n        while (i < j && nums[i] <= nums[left])\n            i++;          // Идти слева направо в поисках первого элемента больше опорного\n        Swap(nums, i, j); // Поменять эти два элемента местами\n    }\n    Swap(nums, i, left);  // Переместить опорный элемент на границу двух подмассивов\n    return i;             // Вернуть индекс опорного элемента\n}\n
        quick_sort.go
        /* Выбрать медиану из трех кандидатов */\nfunc (q *quickSortMedian) medianThree(nums []int, left, mid, right int) int {\n    l, m, r := nums[left], nums[mid], nums[right]\n    if (l <= m && m <= r) || (r <= m && m <= l) {\n        return mid // m находится между l и r\n    }\n    if (m <= l && l <= r) || (r <= l && l <= m) {\n        return left // l находится между m и r\n    }\n    return right\n}\n\n/* Разбиение с опорными указателями (медиана трех) */\nfunc (q *quickSortMedian) partition(nums []int, left, right int) int {\n    // Взять nums[left] в качестве опорного элемента\n    med := q.medianThree(nums, left, (left+right)/2, right)\n    // Переместить медиану в крайний левый элемент массива\n    nums[left], nums[med] = nums[med], nums[left]\n    // Взять nums[left] в качестве опорного элемента\n    i, j := left, right\n    for i < j {\n        for i < j && nums[j] >= nums[left] {\n            j-- // Идти справа налево в поисках первого элемента меньше опорного\n        }\n        for i < j && nums[i] <= nums[left] {\n            i++ // Идти слева направо в поисках первого элемента больше опорного\n        }\n        // Обмен элементов\n        nums[i], nums[j] = nums[j], nums[i]\n    }\n    // Переместить опорный элемент на границу двух подмассивов\n    nums[i], nums[left] = nums[left], nums[i]\n    return i // Вернуть индекс опорного элемента\n}\n
        quick_sort.swift
        /* Выбрать медиану из трех кандидатов */\nfunc medianThree(nums: [Int], left: Int, mid: Int, right: Int) -> Int {\n    let l = nums[left]\n    let m = nums[mid]\n    let r = nums[right]\n    if (l <= m && m <= r) || (r <= m && m <= l) {\n        return mid // m находится между l и r\n    }\n    if (m <= l && l <= r) || (r <= l && l <= m) {\n        return left // l находится между m и r\n    }\n    return right\n}\n\n/* Разбиение с опорными указателями (медиана трех) */\nfunc partitionMedian(nums: inout [Int], left: Int, right: Int) -> Int {\n    // Выбрать медиану из трех кандидатов\n    let med = medianThree(nums: nums, left: left, mid: left + (right - left) / 2, right: right)\n    // Переместить медиану в крайний левый элемент массива\n    nums.swapAt(left, med)\n    return partition(nums: &nums, left: left, right: right)\n}\n
        quick_sort.js
        /* Выбрать медиану из трех кандидатов */\nmedianThree(nums, left, mid, right) {\n    let l = nums[left],\n        m = nums[mid],\n        r = nums[right];\n    // m находится между l и r\n    if ((l <= m && m <= r) || (r <= m && m <= l)) return mid;\n    // l находится между m и r\n    if ((m <= l && l <= r) || (r <= l && l <= m)) return left;\n    return right;\n}\n\n/* Разбиение с опорными указателями (медиана трех) */\npartition(nums, left, right) {\n    // Выбрать медиану из трех кандидатов\n    let med = this.medianThree(\n        nums,\n        left,\n        Math.floor((left + right) / 2),\n        right\n    );\n    // Переместить медиану в крайний левый элемент массива\n    this.swap(nums, left, med);\n    // Взять nums[left] в качестве опорного элемента\n    let i = left,\n        j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) j--; // Идти справа налево в поисках первого элемента меньше опорного\n        while (i < j && nums[i] <= nums[left]) i++; // Идти слева направо в поисках первого элемента больше опорного\n        this.swap(nums, i, j); // Поменять эти два элемента местами\n    }\n    this.swap(nums, i, left); // Переместить опорный элемент на границу двух подмассивов\n    return i; // Вернуть индекс опорного элемента\n}\n
        quick_sort.ts
        /* Выбрать медиану из трех кандидатов */\nmedianThree(\n    nums: number[],\n    left: number,\n    mid: number,\n    right: number\n): number {\n    let l = nums[left],\n        m = nums[mid],\n        r = nums[right];\n    // m находится между l и r\n    if ((l <= m && m <= r) || (r <= m && m <= l)) return mid;\n    // l находится между m и r\n    if ((m <= l && l <= r) || (r <= l && l <= m)) return left;\n    return right;\n}\n\n/* Разбиение с опорными указателями (медиана трех) */\npartition(nums: number[], left: number, right: number): number {\n    // Выбрать медиану из трех кандидатов\n    let med = this.medianThree(\n        nums,\n        left,\n        Math.floor((left + right) / 2),\n        right\n    );\n    // Переместить медиану в крайний левый элемент массива\n    this.swap(nums, left, med);\n    // Взять nums[left] в качестве опорного элемента\n    let i = left,\n        j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) {\n            j--; // Идти справа налево в поисках первого элемента меньше опорного\n        }\n        while (i < j && nums[i] <= nums[left]) {\n            i++; // Идти слева направо в поисках первого элемента больше опорного\n        }\n        this.swap(nums, i, j); // Поменять эти два элемента местами\n    }\n    this.swap(nums, i, left); // Переместить опорный элемент на границу двух подмассивов\n    return i; // Вернуть индекс опорного элемента\n}\n
        quick_sort.dart
        /* Выбрать медиану из трех кандидатов */\nint _medianThree(List<int> nums, int left, int mid, int right) {\n  int l = nums[left], m = nums[mid], r = nums[right];\n  if ((l <= m && m <= r) || (r <= m && m <= l))\n    return mid; // m находится между l и r\n  if ((m <= l && l <= r) || (r <= l && l <= m))\n    return left; // l находится между m и r\n  return right;\n}\n\n/* Разбиение с опорными указателями (медиана трех) */\nint _partition(List<int> nums, int left, int right) {\n  // Выбрать медиану из трех кандидатов\n  int med = _medianThree(nums, left, (left + right) ~/ 2, right);\n  // Переместить медиану в крайний левый элемент массива\n  _swap(nums, left, med);\n  // Взять nums[left] в качестве опорного элемента\n  int i = left, j = right;\n  while (i < j) {\n    while (i < j && nums[j] >= nums[left]) j--; // Идти справа налево в поисках первого элемента меньше опорного\n    while (i < j && nums[i] <= nums[left]) i++; // Идти слева направо в поисках первого элемента больше опорного\n    _swap(nums, i, j); // Поменять эти два элемента местами\n  }\n  _swap(nums, i, left); // Переместить опорный элемент на границу двух подмассивов\n  return i; // Вернуть индекс опорного элемента\n}\n
        quick_sort.rs
        /* Выбрать медиану из трех кандидатов */\nfn median_three(nums: &mut [i32], left: usize, mid: usize, right: usize) -> usize {\n    let (l, m, r) = (nums[left], nums[mid], nums[right]);\n    if (l <= m && m <= r) || (r <= m && m <= l) {\n        return mid; // m находится между l и r\n    }\n    if (m <= l && l <= r) || (r <= l && l <= m) {\n        return left; // l находится между m и r\n    }\n    right\n}\n\n/* Разбиение с опорными указателями (медиана трех) */\nfn partition(nums: &mut [i32], left: usize, right: usize) -> usize {\n    // Выбрать медиану из трех кандидатов\n    let med = Self::median_three(nums, left, (left + right) / 2, right);\n    // Переместить медиану в крайний левый элемент массива\n    nums.swap(left, med);\n    // Взять nums[left] в качестве опорного элемента\n    let (mut i, mut j) = (left, right);\n    while i < j {\n        while i < j && nums[j] >= nums[left] {\n            j -= 1; // Идти справа налево в поисках первого элемента меньше опорного\n        }\n        while i < j && nums[i] <= nums[left] {\n            i += 1; // Идти слева направо в поисках первого элемента больше опорного\n        }\n        nums.swap(i, j); // Поменять эти два элемента местами\n    }\n    nums.swap(i, left); // Переместить опорный элемент на границу двух подмассивов\n    i // Вернуть индекс опорного элемента\n}\n
        quick_sort.c
        /* Выбрать медиану из трех кандидатов */\nint medianThree(int nums[], int left, int mid, int right) {\n    int l = nums[left], m = nums[mid], r = nums[right];\n    if ((l <= m && m <= r) || (r <= m && m <= l))\n        return mid; // m находится между l и r\n    if ((m <= l && l <= r) || (r <= l && l <= m))\n        return left; // l находится между m и r\n    return right;\n}\n\n/* Разбиение с опорными указателями (медиана трех) */\nint partitionMedian(int nums[], int left, int right) {\n    // Выбрать медиану из трех кандидатов\n    int med = medianThree(nums, left, (left + right) / 2, right);\n    // Переместить медиану в крайний левый элемент массива\n    swap(nums, left, med);\n    // Взять nums[left] в качестве опорного элемента\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--; // Идти справа налево в поисках первого элемента меньше опорного\n        while (i < j && nums[i] <= nums[left])\n            i++;          // Идти слева направо в поисках первого элемента больше опорного\n        swap(nums, i, j); // Поменять эти два элемента местами\n    }\n    swap(nums, i, left); // Переместить опорный элемент на границу двух подмассивов\n    return i;            // Вернуть индекс опорного элемента\n}\n
        quick_sort.kt
        /* Выбрать медиану из трех кандидатов */\nfun medianThree(nums: IntArray, left: Int, mid: Int, right: Int): Int {\n    val l = nums[left]\n    val m = nums[mid]\n    val r = nums[right]\n    if ((m in l..r) || (m in r..l))\n        return mid  // m находится между l и r\n    if ((l in m..r) || (l in r..m))\n        return left // l находится между m и r\n    return right\n}\n\n/* Разбиение с опорными указателями (медиана трех) */\nfun partitionMedian(nums: IntArray, left: Int, right: Int): Int {\n    // Выбрать медиану из трех кандидатов\n    val med = medianThree(nums, left, (left + right) / 2, right)\n    // Переместить медиану в крайний левый элемент массива\n    swap(nums, left, med)\n    // Взять nums[left] в качестве опорного элемента\n    var i = left\n    var j = right\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--                      // Идти справа налево в поисках первого элемента меньше опорного\n        while (i < j && nums[i] <= nums[left])\n            i++                      // Идти слева направо в поисках первого элемента больше опорного\n        swap(nums, i, j)             // Поменять эти два элемента местами\n    }\n    swap(nums, i, left)              // Переместить опорный элемент на границу двух подмассивов\n    return i                         // Вернуть индекс опорного элемента\n}\n
        quick_sort.rb
        ### Выбрать медиану из трех кандидатов ###\ndef median_three(nums, left, mid, right)\n  # Выбрать медиану из трех кандидатов\n  _l, _m, _r = nums[left], nums[mid], nums[right]\n  # m находится между l и r\n  return mid if (_l <= _m && _m <= _r) || (_r <= _m && _m <= _l)\n  # l находится между m и r\n  return left if (_m <= _l && _l <= _r) || (_r <= _l && _l <= _m)\n  return right\nend\n\n### Выбрать медиану из трех кандидатов ###\ndef median_three(nums, left, mid, right)\n  # Выбрать медиану из трех кандидатов\n  _l, _m, _r = nums[left], nums[mid], nums[right]\n  # m находится между l и r\n  return mid if (_l <= _m && _m <= _r) || (_r <= _m && _m <= _l)\n  # l находится между m и r\n  return left if (_m <= _l && _l <= _r) || (_r <= _l && _l <= _m)\n  return right\nend\n\n# ## Разбиение с опорными указателями (медиана трех) ###\ndef partition(nums, left, right)\n  # ## Использовать nums[left] как опорный элемент\n  med = median_three(nums, left, (left + right) / 2, right)\n  # Переместить медиану в крайний левый элемент массива\n  nums[left], nums[med] = nums[med], nums[left]\n  i, j = left, right\n  while i < j\n    while i < j && nums[j] >= nums[left]\n      j -= 1 # Идти справа налево в поисках первого элемента меньше опорного\n    end\n    while i < j && nums[i] <= nums[left]\n      i += 1 # Идти слева направо в поисках первого элемента больше опорного\n    end\n    # Обмен элементов\n    nums[i], nums[j] = nums[j], nums[i]\n  end\n  # Переместить опорный элемент на границу двух подмассивов\n  nums[i], nums[left] = nums[left], nums[i]\n  i # Вернуть индекс опорного элемента\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 11. Сортировка","11.5   Быстрая сортировка"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1155","level":2,"title":"11.5.5   Оптимизация глубины рекурсии","text":"

        На некоторых входных данных быстрая сортировка может занимать слишком много памяти. Рассмотрим полностью отсортированный входной массив. Пусть длина текущего подмассива в рекурсии равна \\(m\\). Тогда после каждого разделения будут получаться левый подмассив длины \\(0\\) и правый подмассив длины \\(m - 1\\) . Это означает, что на каждом уровне размер задачи уменьшается совсем немного (лишь на один элемент), а высота дерева рекурсии достигает \\(n - 1\\) , поэтому требуется \\(O(n)\\) памяти под стек вызовов.

        Чтобы избежать накопления стековых кадров, после каждого разделения можно сравнивать длины двух подмассивов и рекурсивно обрабатывать только более короткий из них. Поскольку длина короткого подмассива не превысит \\(n / 2\\) , такой подход гарантирует, что глубина рекурсии не превысит \\(\\log n\\) , а худшая пространственная сложность будет оптимизирована до \\(O(\\log n)\\) . Код приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py
        def quick_sort(self, nums: list[int], left: int, right: int):\n    \"\"\"Быстрая сортировка (оптимизация глубины рекурсии)\"\"\"\n    # Завершить, когда длина подмассива равна 1\n    while left < right:\n        # Операция разбиения с опорными указателями\n        pivot = self.partition(nums, left, right)\n        # Выполнить быструю сортировку для более короткого из двух подмассивов\n        if pivot - left < right - pivot:\n            self.quick_sort(nums, left, pivot - 1)  # Рекурсивно отсортировать левый подмассив\n            left = pivot + 1  # Оставшийся неотсортированный диапазон: [pivot + 1, right]\n        else:\n            self.quick_sort(nums, pivot + 1, right)  # Рекурсивно отсортировать правый подмассив\n            right = pivot - 1  # Оставшийся неотсортированный диапазон: [left, pivot - 1]\n
        quick_sort.cpp
        /* Быстрая сортировка (оптимизация глубины рекурсии) */\nvoid quickSort(vector<int> &nums, int left, int right) {\n    // Завершить, когда длина подмассива равна 1\n    while (left < right) {\n        // Операция разбиения с опорными указателями\n        int pivot = partition(nums, left, right);\n        // Выполнить быструю сортировку для более короткого из двух подмассивов\n        if (pivot - left < right - pivot) {\n            quickSort(nums, left, pivot - 1); // Рекурсивно отсортировать левый подмассив\n            left = pivot + 1;                 // Оставшийся неотсортированный диапазон: [pivot + 1, right]\n        } else {\n            quickSort(nums, pivot + 1, right); // Рекурсивно отсортировать правый подмассив\n            right = pivot - 1;                 // Оставшийся неотсортированный диапазон: [left, pivot - 1]\n        }\n    }\n}\n
        quick_sort.java
        /* Быстрая сортировка (оптимизация глубины рекурсии) */\nvoid quickSort(int[] nums, int left, int right) {\n    // Завершить, когда длина подмассива равна 1\n    while (left < right) {\n        // Операция разбиения с опорными указателями\n        int pivot = partition(nums, left, right);\n        // Выполнить быструю сортировку для более короткого из двух подмассивов\n        if (pivot - left < right - pivot) {\n            quickSort(nums, left, pivot - 1); // Рекурсивно отсортировать левый подмассив\n            left = pivot + 1; // Оставшийся неотсортированный диапазон: [pivot + 1, right]\n        } else {\n            quickSort(nums, pivot + 1, right); // Рекурсивно отсортировать правый подмассив\n            right = pivot - 1; // Оставшийся неотсортированный диапазон: [left, pivot - 1]\n        }\n    }\n}\n
        quick_sort.cs
        /* Быстрая сортировка (оптимизация глубины рекурсии) */\nvoid QuickSort(int[] nums, int left, int right) {\n    // Завершить, когда длина подмассива равна 1\n    while (left < right) {\n        // Операция разбиения с опорными указателями\n        int pivot = Partition(nums, left, right);\n        // Выполнить быструю сортировку для более короткого из двух подмассивов\n        if (pivot - left < right - pivot) {\n            QuickSort(nums, left, pivot - 1);  // Рекурсивно отсортировать левый подмассив\n            left = pivot + 1;  // Оставшийся неотсортированный диапазон: [pivot + 1, right]\n        } else {\n            QuickSort(nums, pivot + 1, right); // Рекурсивно отсортировать правый подмассив\n            right = pivot - 1; // Оставшийся неотсортированный диапазон: [left, pivot - 1]\n        }\n    }\n}\n
        quick_sort.go
        /* Быстрая сортировка (оптимизация глубины рекурсии) */\nfunc (q *quickSortTailCall) quickSort(nums []int, left, right int) {\n    // Завершить, когда длина подмассива равна 1\n    for left < right {\n        // Операция разбиения с опорными указателями\n        pivot := q.partition(nums, left, right)\n        // Выполнить быструю сортировку для более короткого из двух подмассивов\n        if pivot-left < right-pivot {\n            q.quickSort(nums, left, pivot-1) // Рекурсивно отсортировать левый подмассив\n            left = pivot + 1                 // Оставшийся неотсортированный диапазон: [pivot + 1, right]\n        } else {\n            q.quickSort(nums, pivot+1, right) // Рекурсивно отсортировать правый подмассив\n            right = pivot - 1                 // Оставшийся неотсортированный диапазон: [left, pivot - 1]\n        }\n    }\n}\n
        quick_sort.swift
        /* Быстрая сортировка (оптимизация глубины рекурсии) */\nfunc quickSortTailCall(nums: inout [Int], left: Int, right: Int) {\n    var left = left\n    var right = right\n    // Завершить, когда длина подмассива равна 1\n    while left < right {\n        // Операция разбиения с опорными указателями\n        let pivot = partition(nums: &nums, left: left, right: right)\n        // Выполнить быструю сортировку для более короткого из двух подмассивов\n        if (pivot - left) < (right - pivot) {\n            quickSortTailCall(nums: &nums, left: left, right: pivot - 1) // Рекурсивно отсортировать левый подмассив\n            left = pivot + 1 // Оставшийся неотсортированный диапазон: [pivot + 1, right]\n        } else {\n            quickSortTailCall(nums: &nums, left: pivot + 1, right: right) // Рекурсивно отсортировать правый подмассив\n            right = pivot - 1 // Оставшийся неотсортированный диапазон: [left, pivot - 1]\n        }\n    }\n}\n
        quick_sort.js
        /* Быстрая сортировка (оптимизация глубины рекурсии) */\nquickSort(nums, left, right) {\n    // Завершить, когда длина подмассива равна 1\n    while (left < right) {\n        // Операция разбиения с опорными указателями\n        let pivot = this.partition(nums, left, right);\n        // Выполнить быструю сортировку для более короткого из двух подмассивов\n        if (pivot - left < right - pivot) {\n            this.quickSort(nums, left, pivot - 1); // Рекурсивно отсортировать левый подмассив\n            left = pivot + 1; // Оставшийся неотсортированный диапазон: [pivot + 1, right]\n        } else {\n            this.quickSort(nums, pivot + 1, right); // Рекурсивно отсортировать правый подмассив\n            right = pivot - 1; // Оставшийся неотсортированный диапазон: [left, pivot - 1]\n        }\n    }\n}\n
        quick_sort.ts
        /* Быстрая сортировка (оптимизация глубины рекурсии) */\nquickSort(nums: number[], left: number, right: number): void {\n    // Завершить, когда длина подмассива равна 1\n    while (left < right) {\n        // Операция разбиения с опорными указателями\n        let pivot = this.partition(nums, left, right);\n        // Выполнить быструю сортировку для более короткого из двух подмассивов\n        if (pivot - left < right - pivot) {\n            this.quickSort(nums, left, pivot - 1); // Рекурсивно отсортировать левый подмассив\n            left = pivot + 1; // Оставшийся неотсортированный диапазон: [pivot + 1, right]\n        } else {\n            this.quickSort(nums, pivot + 1, right); // Рекурсивно отсортировать правый подмассив\n            right = pivot - 1; // Оставшийся неотсортированный диапазон: [left, pivot - 1]\n        }\n    }\n}\n
        quick_sort.dart
        /* Быстрая сортировка (оптимизация глубины рекурсии) */\nvoid quickSort(List<int> nums, int left, int right) {\n  // Завершить, когда длина подмассива равна 1\n  while (left < right) {\n    // Операция разбиения с опорными указателями\n    int pivot = _partition(nums, left, right);\n    // Выполнить быструю сортировку для более короткого из двух подмассивов\n    if (pivot - left < right - pivot) {\n      quickSort(nums, left, pivot - 1); // Рекурсивно отсортировать левый подмассив\n      left = pivot + 1; // Оставшийся неотсортированный диапазон: [pivot + 1, right]\n    } else {\n      quickSort(nums, pivot + 1, right); // Рекурсивно отсортировать правый подмассив\n      right = pivot - 1; // Оставшийся неотсортированный диапазон: [left, pivot - 1]\n    }\n  }\n}\n
        quick_sort.rs
        /* Быстрая сортировка (оптимизация глубины рекурсии) */\npub fn quick_sort(mut left: i32, mut right: i32, nums: &mut [i32]) {\n    // Завершить, когда длина подмассива равна 1\n    while left < right {\n        // Операция разбиения с опорными указателями\n        let pivot = Self::partition(nums, left as usize, right as usize) as i32;\n        // Выполнить быструю сортировку для более короткого из двух подмассивов\n        if pivot - left < right - pivot {\n            Self::quick_sort(left, pivot - 1, nums); // Рекурсивно отсортировать левый подмассив\n            left = pivot + 1; // Оставшийся неотсортированный диапазон: [pivot + 1, right]\n        } else {\n            Self::quick_sort(pivot + 1, right, nums); // Рекурсивно отсортировать правый подмассив\n            right = pivot - 1; // Оставшийся неотсортированный диапазон: [left, pivot - 1]\n        }\n    }\n}\n
        quick_sort.c
        /* Быстрая сортировка (оптимизация глубины рекурсии) */\nvoid quickSortTailCall(int nums[], int left, int right) {\n    // Завершить, когда длина подмассива равна 1\n    while (left < right) {\n        // Операция разбиения с опорными указателями\n        int pivot = partition(nums, left, right);\n        // Выполнить быструю сортировку для более короткого из двух подмассивов\n        if (pivot - left < right - pivot) {\n            // Рекурсивно отсортировать левый подмассив\n            quickSortTailCall(nums, left, pivot - 1);\n            // Оставшийся неотсортированный диапазон: [pivot + 1, right]\n            left = pivot + 1;\n        } else {\n            // Рекурсивно отсортировать правый подмассив\n            quickSortTailCall(nums, pivot + 1, right);\n            // Оставшийся неотсортированный диапазон: [left, pivot - 1]\n            right = pivot - 1;\n        }\n    }\n}\n
        quick_sort.kt
        /* Быстрая сортировка (оптимизация глубины рекурсии) */\nfun quickSortTailCall(nums: IntArray, left: Int, right: Int) {\n    // Завершить, когда длина подмассива равна 1\n    var l = left\n    var r = right\n    while (l < r) {\n        // Операция разбиения с опорными указателями\n        val pivot = partition(nums, l, r)\n        // Выполнить быструю сортировку для более короткого из двух подмассивов\n        if (pivot - l < r - pivot) {\n            quickSort(nums, l, pivot - 1) // Рекурсивно отсортировать левый подмассив\n            l = pivot + 1 // Оставшийся неотсортированный диапазон: [pivot + 1, right]\n        } else {\n            quickSort(nums, pivot + 1, r) // Рекурсивно отсортировать правый подмассив\n            r = pivot - 1 // Оставшийся неотсортированный диапазон: [left, pivot - 1]\n        }\n    }\n}\n
        quick_sort.rb
        ### Разбиение с опорными указателями ###\ndef partition(nums, left, right)\n  # Использовать nums[left] как опорный элемент\n  i = left\n  j = right\n  while i < j\n    while i < j && nums[j] >= nums[left]\n      j -= 1 # Идти справа налево в поисках первого элемента меньше опорного\n    end\n    while i < j && nums[i] <= nums[left]\n      i += 1 # Идти слева направо в поисках первого элемента больше опорного\n    end\n    # Обмен элементов\n    nums[i], nums[j] = nums[j], nums[i]\n  end\n  # Переместить опорный элемент на границу двух подмассивов\n  nums[i], nums[left] = nums[left], nums[i]\n  i # Вернуть индекс опорного элемента\nend\n\n# ## Быстрая сортировка (оптимизация глубины рекурсии) ###\ndef quick_sort(nums, left, right)\n  # Рекурсивно обрабатывать, пока длина подмассива не станет равной 1\n  while left < right\n    # Разбиение с опорными указателями\n    pivot = partition(nums, left, right)\n    # Выполнить быструю сортировку для более короткого из двух подмассивов\n    if pivot - left < right - pivot\n      quick_sort(nums, left, pivot - 1)\n      left = pivot + 1 # Оставшийся неотсортированный диапазон: [pivot + 1, right]\n    else\n      quick_sort(nums, pivot + 1, right)\n      right = pivot - 1 # Оставшийся неотсортированный диапазон: [left, pivot - 1]\n    end\n  end\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 11. Сортировка","11.5   Быстрая сортировка"],"tags":[]},{"location":"chapter_sorting/radix_sort/","level":1,"title":"11.10   Поразрядная сортировка","text":"

        В предыдущем разделе была рассмотрена сортировка подсчетом: она хорошо подходит для случаев, когда объем данных \\(n\\) велик, а диапазон значений \\(m\\) сравнительно мал. Предположим теперь, что нужно отсортировать \\(n = 10^6\\) номеров студентов, причем каждый номер представляет собой \\(8\\)-значное число. Тогда диапазон данных \\(m = 10^8\\) оказывается очень большим. Сортировка подсчетом потребует огромного объема памяти, а поразрядная сортировка позволяет этого избежать.

        Поразрядная сортировка (radix sort) по своей основной идее совпадает с сортировкой подсчетом и тоже реализует сортировку через подсчет количества. При этом поразрядная сортировка использует соотношение между разрядами числа и последовательно сортирует данные по каждому разряду, получая итоговый упорядоченный результат.

        ","path":["Глава 11. Сортировка","11.10   Поразрядная сортировка"],"tags":[]},{"location":"chapter_sorting/radix_sort/#11101","level":2,"title":"11.10.1   Алгоритм","text":"

        Рассмотрим пример со студенческими номерами: будем считать, что младший разряд имеет номер \\(1\\) , а старший - номер \\(8\\) . Тогда процесс поразрядной сортировки показан на рисунке 11-18.

        1. Инициализировать номер разряда \\(k = 1\\) .
        2. Выполнить «сортировку подсчетом» по \\(k\\)-му разряду студенческого номера. После этого данные будут упорядочены по \\(k\\)-му разряду по возрастанию.
        3. Увеличить \\(k\\) на \\(1\\) и вернуться к шагу 2. , продолжая процесс, пока сортировка не будет выполнена для всех разрядов.

        Рисунок 11-18   Процесс поразрядной сортировки

        Ниже разберем реализацию кода. Для числа \\(x\\) в системе счисления с основанием \\(d\\) получить его \\(k\\)-й разряд \\(x_k\\) можно по формуле:

        \\[ x_k = \\lfloor\\frac{x}{d^{k-1}}\\rfloor \\bmod d \\]

        где \\(\\lfloor a \\rfloor\\) обозначает округление числа \\(a\\) вниз, а \\(\\bmod \\: d\\) означает взятие остатка по модулю \\(d\\) . Для студенческих номеров выполняется \\(d = 10\\) и \\(k \\in [1, 8]\\) .

        Кроме того, нам нужно слегка изменить код сортировки подсчетом, чтобы он мог сортировать числа по их \\(k\\)-му разряду:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby radix_sort.py
        def digit(num: int, exp: int) -> int:\n    \"\"\"Получить k-й разряд элемента num, где exp = 10^(k-1)\"\"\"\n    # Передача exp вместо k позволяет избежать повторного дорогостоящего вычисления степени\n    return (num // exp) % 10\n\ndef counting_sort_digit(nums: list[int], exp: int):\n    \"\"\"Сортировка подсчетом (сортировка по k-му разряду nums)\"\"\"\n    # Разряды десятичной системы лежат в диапазоне 0~9, поэтому нужен массив корзин длины 10\n    counter = [0] * 10\n    n = len(nums)\n    # Подсчитать число появлений каждой цифры от 0 до 9\n    for i in range(n):\n        d = digit(nums[i], exp)  # Получить k-й разряд nums[i], обозначив его как d\n        counter[d] += 1  # Подсчитать число появлений цифры d\n    # Вычислить префиксные суммы и преобразовать «число появлений» в «индекс массива»\n    for i in range(1, 10):\n        counter[i] += counter[i - 1]\n    # Выполняя обратный проход, заполнить res элементами по статистике в корзинах\n    res = [0] * n\n    for i in range(n - 1, -1, -1):\n        d = digit(nums[i], exp)\n        j = counter[d] - 1  # Получить индекс j цифры d в массиве\n        res[j] = nums[i]  # Поместить текущий элемент по индексу j\n        counter[d] -= 1  # Уменьшить количество d на 1\n    # Перезаписать исходный массив nums результатом\n    for i in range(n):\n        nums[i] = res[i]\n\ndef radix_sort(nums: list[int]):\n    \"\"\"Поразрядная сортировка\"\"\"\n    # Получить максимальный элемент массива, чтобы определить максимальное число разрядов\n    m = max(nums)\n    # Проходить разряды от младшего к старшему\n    exp = 1\n    while exp <= m:\n        # Выполнить сортировку подсчетом по k-му разряду элементов массива\n        # k = 1 -> exp = 1\n        # k = 2 -> exp = 10\n        # то есть exp = 10^(k-1)\n        counting_sort_digit(nums, exp)\n        exp *= 10\n
        radix_sort.cpp
        /* Получить k-й разряд элемента num, где exp = 10^(k-1) */\nint digit(int num, int exp) {\n    // Передача exp вместо k позволяет избежать повторного дорогостоящего вычисления степени\n    return (num / exp) % 10;\n}\n\n/* Сортировка подсчетом (сортировка по k-му разряду nums) */\nvoid countingSortDigit(vector<int> &nums, int exp) {\n    // Разряды десятичной системы лежат в диапазоне 0~9, поэтому нужен массив корзин длины 10\n    vector<int> counter(10, 0);\n    int n = nums.size();\n    // Подсчитать число появлений каждой цифры от 0 до 9\n    for (int i = 0; i < n; i++) {\n        int d = digit(nums[i], exp); // Получить k-й разряд nums[i], обозначив его как d\n        counter[d]++;                // Подсчитать число появлений цифры d\n    }\n    // Вычислить префиксные суммы и преобразовать «число появлений» в «индекс массива»\n    for (int i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // Выполняя обратный проход, заполнить res элементами по статистике в корзинах\n    vector<int> res(n, 0);\n    for (int i = n - 1; i >= 0; i--) {\n        int d = digit(nums[i], exp);\n        int j = counter[d] - 1; // Получить индекс j цифры d в массиве\n        res[j] = nums[i];       // Поместить текущий элемент по индексу j\n        counter[d]--;           // Уменьшить количество d на 1\n    }\n    // Перезаписать исходный массив nums результатом\n    for (int i = 0; i < n; i++)\n        nums[i] = res[i];\n}\n\n/* Поразрядная сортировка */\nvoid radixSort(vector<int> &nums) {\n    // Получить максимальный элемент массива, чтобы определить максимальное число разрядов\n    int m = *max_element(nums.begin(), nums.end());\n    // Проходить разряды от младшего к старшему\n    for (int exp = 1; exp <= m; exp *= 10)\n        // Выполнить сортировку подсчетом по k-му разряду элементов массива\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // то есть exp = 10^(k-1)\n        countingSortDigit(nums, exp);\n}\n
        radix_sort.java
        /* Получить k-й разряд элемента num, где exp = 10^(k-1) */\nint digit(int num, int exp) {\n    // Передача exp вместо k позволяет избежать повторного дорогостоящего вычисления степени\n    return (num / exp) % 10;\n}\n\n/* Сортировка подсчетом (сортировка по k-му разряду nums) */\nvoid countingSortDigit(int[] nums, int exp) {\n    // Разряды десятичной системы лежат в диапазоне 0~9, поэтому нужен массив корзин длины 10\n    int[] counter = new int[10];\n    int n = nums.length;\n    // Подсчитать число появлений каждой цифры от 0 до 9\n    for (int i = 0; i < n; i++) {\n        int d = digit(nums[i], exp); // Получить k-й разряд nums[i], обозначив его как d\n        counter[d]++;                // Подсчитать число появлений цифры d\n    }\n    // Вычислить префиксные суммы и преобразовать «число появлений» в «индекс массива»\n    for (int i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // Выполняя обратный проход, заполнить res элементами по статистике в корзинах\n    int[] res = new int[n];\n    for (int i = n - 1; i >= 0; i--) {\n        int d = digit(nums[i], exp);\n        int j = counter[d] - 1; // Получить индекс j цифры d в массиве\n        res[j] = nums[i];       // Поместить текущий элемент по индексу j\n        counter[d]--;           // Уменьшить количество d на 1\n    }\n    // Перезаписать исходный массив nums результатом\n    for (int i = 0; i < n; i++)\n        nums[i] = res[i];\n}\n\n/* Поразрядная сортировка */\nvoid radixSort(int[] nums) {\n    // Получить максимальный элемент массива, чтобы определить максимальное число разрядов\n    int m = Integer.MIN_VALUE;\n    for (int num : nums)\n        if (num > m)\n            m = num;\n    // Проходить разряды от младшего к старшему\n    for (int exp = 1; exp <= m; exp *= 10) {\n        // Выполнить сортировку подсчетом по k-му разряду элементов массива\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // то есть exp = 10^(k-1)\n        countingSortDigit(nums, exp);\n    }\n}\n
        radix_sort.cs
        /* Получить k-й разряд элемента num, где exp = 10^(k-1) */\nint Digit(int num, int exp) {\n    // Передача exp вместо k позволяет избежать повторного дорогостоящего вычисления степени\n    return (num / exp) % 10;\n}\n\n/* Сортировка подсчетом (сортировка по k-му разряду nums) */\nvoid CountingSortDigit(int[] nums, int exp) {\n    // Разряды десятичной системы лежат в диапазоне 0~9, поэтому нужен массив корзин длины 10\n    int[] counter = new int[10];\n    int n = nums.Length;\n    // Подсчитать число появлений каждой цифры от 0 до 9\n    for (int i = 0; i < n; i++) {\n        int d = Digit(nums[i], exp); // Получить k-й разряд nums[i], обозначив его как d\n        counter[d]++;                // Подсчитать число появлений цифры d\n    }\n    // Вычислить префиксные суммы и преобразовать «число появлений» в «индекс массива»\n    for (int i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // Выполняя обратный проход, заполнить res элементами по статистике в корзинах\n    int[] res = new int[n];\n    for (int i = n - 1; i >= 0; i--) {\n        int d = Digit(nums[i], exp);\n        int j = counter[d] - 1; // Получить индекс j цифры d в массиве\n        res[j] = nums[i];       // Поместить текущий элемент по индексу j\n        counter[d]--;           // Уменьшить количество d на 1\n    }\n    // Перезаписать исходный массив nums результатом\n    for (int i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n\n/* Поразрядная сортировка */\nvoid RadixSort(int[] nums) {\n    // Получить максимальный элемент массива, чтобы определить максимальное число разрядов\n    int m = int.MinValue;\n    foreach (int num in nums) {\n        if (num > m) m = num;\n    }\n    // Проходить разряды от младшего к старшему\n    for (int exp = 1; exp <= m; exp *= 10) {\n        // Выполнить сортировку подсчетом по k-му разряду элементов массива\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // то есть exp = 10^(k-1)\n        CountingSortDigit(nums, exp);\n    }\n}\n
        radix_sort.go
        /* Получить k-й разряд элемента num, где exp = 10^(k-1) */\nfunc digit(num, exp int) int {\n    // Передача exp вместо k позволяет избежать повторного дорогостоящего вычисления степени\n    return (num / exp) % 10\n}\n\n/* Сортировка подсчетом (сортировка по k-му разряду nums) */\nfunc countingSortDigit(nums []int, exp int) {\n    // Разряды десятичной системы лежат в диапазоне 0~9, поэтому нужен массив корзин длины 10\n    counter := make([]int, 10)\n    n := len(nums)\n    // Подсчитать число появлений каждой цифры от 0 до 9\n    for i := 0; i < n; i++ {\n        d := digit(nums[i], exp) // Получить k-й разряд nums[i], обозначив его как d\n        counter[d]++             // Подсчитать число появлений цифры d\n    }\n    // Вычислить префиксные суммы и преобразовать «число появлений» в «индекс массива»\n    for i := 1; i < 10; i++ {\n        counter[i] += counter[i-1]\n    }\n    // Выполняя обратный проход, заполнить res элементами по статистике в корзинах\n    res := make([]int, n)\n    for i := n - 1; i >= 0; i-- {\n        d := digit(nums[i], exp)\n        j := counter[d] - 1 // Получить индекс j цифры d в массиве\n        res[j] = nums[i]    // Поместить текущий элемент по индексу j\n        counter[d]--        // Уменьшить количество d на 1\n    }\n    // Перезаписать исходный массив nums результатом\n    for i := 0; i < n; i++ {\n        nums[i] = res[i]\n    }\n}\n\n/* Поразрядная сортировка */\nfunc radixSort(nums []int) {\n    // Получить максимальный элемент массива, чтобы определить максимальное число разрядов\n    max := math.MinInt\n    for _, num := range nums {\n        if num > max {\n            max = num\n        }\n    }\n    // Проходить разряды от младшего к старшему\n    for exp := 1; max >= exp; exp *= 10 {\n        // Выполнить сортировку подсчетом по k-му разряду элементов массива\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // то есть exp = 10^(k-1)\n        countingSortDigit(nums, exp)\n    }\n}\n
        radix_sort.swift
        /* Получить k-й разряд элемента num, где exp = 10^(k-1) */\nfunc digit(num: Int, exp: Int) -> Int {\n    // Передача exp вместо k позволяет избежать повторного дорогостоящего вычисления степени\n    (num / exp) % 10\n}\n\n/* Сортировка подсчетом (сортировка по k-му разряду nums) */\nfunc countingSortDigit(nums: inout [Int], exp: Int) {\n    // Разряды десятичной системы лежат в диапазоне 0~9, поэтому нужен массив корзин длины 10\n    var counter = Array(repeating: 0, count: 10)\n    // Подсчитать число появлений каждой цифры от 0 до 9\n    for i in nums.indices {\n        let d = digit(num: nums[i], exp: exp) // Получить k-й разряд nums[i], обозначив его как d\n        counter[d] += 1 // Подсчитать число появлений цифры d\n    }\n    // Вычислить префиксные суммы и преобразовать «число появлений» в «индекс массива»\n    for i in 1 ..< 10 {\n        counter[i] += counter[i - 1]\n    }\n    // Выполняя обратный проход, заполнить res элементами по статистике в корзинах\n    var res = Array(repeating: 0, count: nums.count)\n    for i in nums.indices.reversed() {\n        let d = digit(num: nums[i], exp: exp)\n        let j = counter[d] - 1 // Получить индекс j цифры d в массиве\n        res[j] = nums[i] // Поместить текущий элемент по индексу j\n        counter[d] -= 1 // Уменьшить количество d на 1\n    }\n    // Перезаписать исходный массив nums результатом\n    for i in nums.indices {\n        nums[i] = res[i]\n    }\n}\n\n/* Поразрядная сортировка */\nfunc radixSort(nums: inout [Int]) {\n    // Получить максимальный элемент массива, чтобы определить максимальное число разрядов\n    var m = Int.min\n    for num in nums {\n        if num > m {\n            m = num\n        }\n    }\n    // Проходить разряды от младшего к старшему\n    for exp in sequence(first: 1, next: { m >= ($0 * 10) ? $0 * 10 : nil }) {\n        // Выполнить сортировку подсчетом по k-му разряду элементов массива\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // то есть exp = 10^(k-1)\n        countingSortDigit(nums: &nums, exp: exp)\n    }\n}\n
        radix_sort.js
        /* Получить k-й разряд элемента num, где exp = 10^(k-1) */\nfunction digit(num, exp) {\n    // Передача exp вместо k позволяет избежать повторного дорогостоящего вычисления степени\n    return Math.floor(num / exp) % 10;\n}\n\n/* Сортировка подсчетом (сортировка по k-му разряду nums) */\nfunction countingSortDigit(nums, exp) {\n    // Разряды десятичной системы лежат в диапазоне 0~9, поэтому нужен массив корзин длины 10\n    const counter = new Array(10).fill(0);\n    const n = nums.length;\n    // Подсчитать число появлений каждой цифры от 0 до 9\n    for (let i = 0; i < n; i++) {\n        const d = digit(nums[i], exp); // Получить k-й разряд nums[i], обозначив его как d\n        counter[d]++; // Подсчитать число появлений цифры d\n    }\n    // Вычислить префиксные суммы и преобразовать «число появлений» в «индекс массива»\n    for (let i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // Выполняя обратный проход, заполнить res элементами по статистике в корзинах\n    const res = new Array(n).fill(0);\n    for (let i = n - 1; i >= 0; i--) {\n        const d = digit(nums[i], exp);\n        const j = counter[d] - 1; // Получить индекс j цифры d в массиве\n        res[j] = nums[i]; // Поместить текущий элемент по индексу j\n        counter[d]--; // Уменьшить количество d на 1\n    }\n    // Перезаписать исходный массив nums результатом\n    for (let i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n\n/* Поразрядная сортировка */\nfunction radixSort(nums) {\n    // Получить максимальный элемент массива, чтобы определить максимальное число разрядов\n    let m = Math.max(... nums);\n    // Проходить разряды от младшего к старшему\n    for (let exp = 1; exp <= m; exp *= 10) {\n        // Выполнить сортировку подсчетом по k-му разряду элементов массива\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // то есть exp = 10^(k-1)\n        countingSortDigit(nums, exp);\n    }\n}\n
        radix_sort.ts
        /* Получить k-й разряд элемента num, где exp = 10^(k-1) */\nfunction digit(num: number, exp: number): number {\n    // Передача exp вместо k позволяет избежать повторного дорогостоящего вычисления степени\n    return Math.floor(num / exp) % 10;\n}\n\n/* Сортировка подсчетом (сортировка по k-му разряду nums) */\nfunction countingSortDigit(nums: number[], exp: number): void {\n    // Разряды десятичной системы лежат в диапазоне 0~9, поэтому нужен массив корзин длины 10\n    const counter = new Array(10).fill(0);\n    const n = nums.length;\n    // Подсчитать число появлений каждой цифры от 0 до 9\n    for (let i = 0; i < n; i++) {\n        const d = digit(nums[i], exp); // Получить k-й разряд nums[i], обозначив его как d\n        counter[d]++; // Подсчитать число появлений цифры d\n    }\n    // Вычислить префиксные суммы и преобразовать «число появлений» в «индекс массива»\n    for (let i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // Выполняя обратный проход, заполнить res элементами по статистике в корзинах\n    const res = new Array(n).fill(0);\n    for (let i = n - 1; i >= 0; i--) {\n        const d = digit(nums[i], exp);\n        const j = counter[d] - 1; // Получить индекс j цифры d в массиве\n        res[j] = nums[i]; // Поместить текущий элемент по индексу j\n        counter[d]--; // Уменьшить количество d на 1\n    }\n    // Перезаписать исходный массив nums результатом\n    for (let i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n\n/* Поразрядная сортировка */\nfunction radixSort(nums: number[]): void {\n    // Получить максимальный элемент массива, чтобы определить максимальное число разрядов\n    let m: number = Math.max(... nums);\n    // Проходить разряды от младшего к старшему\n    for (let exp = 1; exp <= m; exp *= 10) {\n        // Выполнить сортировку подсчетом по k-му разряду элементов массива\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // то есть exp = 10^(k-1)\n        countingSortDigit(nums, exp);\n    }\n}\n
        radix_sort.dart
        /* Получить k-й разряд элемента _num, где exp = 10^(k-1) */\nint digit(int _num, int exp) {\n  // Передача exp вместо k позволяет избежать повторного дорогостоящего вычисления степени\n  return (_num ~/ exp) % 10;\n}\n\n/* Сортировка подсчетом (сортировка по k-му разряду nums) */\nvoid countingSortDigit(List<int> nums, int exp) {\n  // Разряды десятичной системы лежат в диапазоне 0~9, поэтому нужен массив корзин длины 10\n  List<int> counter = List<int>.filled(10, 0);\n  int n = nums.length;\n  // Подсчитать число появлений каждой цифры от 0 до 9\n  for (int i = 0; i < n; i++) {\n    int d = digit(nums[i], exp); // Получить k-й разряд nums[i], обозначив его как d\n    counter[d]++; // Подсчитать число появлений цифры d\n  }\n  // Вычислить префиксные суммы и преобразовать «число появлений» в «индекс массива»\n  for (int i = 1; i < 10; i++) {\n    counter[i] += counter[i - 1];\n  }\n  // Выполняя обратный проход, заполнить res элементами по статистике в корзинах\n  List<int> res = List<int>.filled(n, 0);\n  for (int i = n - 1; i >= 0; i--) {\n    int d = digit(nums[i], exp);\n    int j = counter[d] - 1; // Получить индекс j цифры d в массиве\n    res[j] = nums[i]; // Поместить текущий элемент по индексу j\n    counter[d]--; // Уменьшить количество d на 1\n  }\n  // Перезаписать исходный массив nums результатом\n  for (int i = 0; i < n; i++) nums[i] = res[i];\n}\n\n/* Поразрядная сортировка */\nvoid radixSort(List<int> nums) {\n  // Получить максимальный элемент массива, чтобы определить максимальное число разрядов\n  // В dart длина int составляет 64 бита\n  int m = -1 << 63;\n  for (int _num in nums) if (_num > m) m = _num;\n  // Проходить разряды от младшего к старшему\n  for (int exp = 1; exp <= m; exp *= 10)\n    // Выполнить сортировку подсчетом по k-му разряду элементов массива\n    // k = 1 -> exp = 1\n    // k = 2 -> exp = 10\n    // то есть exp = 10^(k-1)\n    countingSortDigit(nums, exp);\n}\n
        radix_sort.rs
        /* Получить k-й разряд элемента num, где exp = 10^(k-1) */\nfn digit(num: i32, exp: i32) -> usize {\n    // Передача exp вместо k позволяет избежать повторного дорогостоящего вычисления степени\n    return ((num / exp) % 10) as usize;\n}\n\n/* Сортировка подсчетом (сортировка по k-му разряду nums) */\nfn counting_sort_digit(nums: &mut [i32], exp: i32) {\n    // Разряды десятичной системы лежат в диапазоне 0~9, поэтому нужен массив корзин длины 10\n    let mut counter = [0; 10];\n    let n = nums.len();\n    // Подсчитать число появлений каждой цифры от 0 до 9\n    for i in 0..n {\n        let d = digit(nums[i], exp); // Получить k-й разряд nums[i], обозначив его как d\n        counter[d] += 1; // Подсчитать число появлений цифры d\n    }\n    // Вычислить префиксные суммы и преобразовать «число появлений» в «индекс массива»\n    for i in 1..10 {\n        counter[i] += counter[i - 1];\n    }\n    // Выполняя обратный проход, заполнить res элементами по статистике в корзинах\n    let mut res = vec![0; n];\n    for i in (0..n).rev() {\n        let d = digit(nums[i], exp);\n        let j = counter[d] - 1; // Получить индекс j цифры d в массиве\n        res[j] = nums[i]; // Поместить текущий элемент по индексу j\n        counter[d] -= 1; // Уменьшить количество d на 1\n    }\n    // Перезаписать исходный массив nums результатом\n    nums.copy_from_slice(&res);\n}\n\n/* Поразрядная сортировка */\nfn radix_sort(nums: &mut [i32]) {\n    // Получить максимальный элемент массива, чтобы определить максимальное число разрядов\n    let m = *nums.into_iter().max().unwrap();\n    // Проходить разряды от младшего к старшему\n    let mut exp = 1;\n    while exp <= m {\n        counting_sort_digit(nums, exp);\n        exp *= 10;\n    }\n}\n
        radix_sort.c
        /* Получить k-й разряд элемента num, где exp = 10^(k-1) */\nint digit(int num, int exp) {\n    // Передача exp вместо k позволяет избежать повторного дорогостоящего вычисления степени\n    return (num / exp) % 10;\n}\n\n/* Сортировка подсчетом (сортировка по k-му разряду nums) */\nvoid countingSortDigit(int nums[], int size, int exp) {\n    // Разряды десятичной системы лежат в диапазоне 0~9, поэтому нужен массив корзин длины 10\n    int *counter = (int *)malloc((sizeof(int) * 10));\n    memset(counter, 0, sizeof(int) * 10); // Инициализировать нулем для последующего освобождения памяти\n    // Подсчитать число появлений каждой цифры от 0 до 9\n    for (int i = 0; i < size; i++) {\n        // Получить k-й разряд nums[i], обозначив его как d\n        int d = digit(nums[i], exp);\n        // Подсчитать число появлений цифры d\n        counter[d]++;\n    }\n    // Вычислить префиксные суммы и преобразовать «число появлений» в «индекс массива»\n    for (int i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // Выполняя обратный проход, заполнить res элементами по статистике в корзинах\n    int *res = (int *)malloc(sizeof(int) * size);\n    for (int i = size - 1; i >= 0; i--) {\n        int d = digit(nums[i], exp);\n        int j = counter[d] - 1; // Получить индекс j цифры d в массиве\n        res[j] = nums[i];       // Поместить текущий элемент по индексу j\n        counter[d]--;           // Уменьшить количество d на 1\n    }\n    // Перезаписать исходный массив nums результатом\n    for (int i = 0; i < size; i++) {\n        nums[i] = res[i];\n    }\n    // Освободить память\n    free(res);\n    free(counter);\n}\n\n/* Поразрядная сортировка */\nvoid radixSort(int nums[], int size) {\n    // Получить максимальный элемент массива, чтобы определить максимальное число разрядов\n    int max = INT32_MIN;\n    for (int i = 0; i < size; i++) {\n        if (nums[i] > max) {\n            max = nums[i];\n        }\n    }\n    // Проходить разряды от младшего к старшему\n    for (int exp = 1; max >= exp; exp *= 10)\n        // Выполнить сортировку подсчетом по k-му разряду элементов массива\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // то есть exp = 10^(k-1)\n        countingSortDigit(nums, size, exp);\n}\n
        radix_sort.kt
        /* Получить k-й разряд элемента num, где exp = 10^(k-1) */\nfun digit(num: Int, exp: Int): Int {\n    // Передача exp вместо k позволяет избежать повторного дорогостоящего вычисления степени\n    return (num / exp) % 10\n}\n\n/* Сортировка подсчетом (сортировка по k-му разряду nums) */\nfun countingSortDigit(nums: IntArray, exp: Int) {\n    // Разряды десятичной системы лежат в диапазоне 0~9, поэтому нужен массив корзин длины 10\n    val counter = IntArray(10)\n    val n = nums.size\n    // Подсчитать число появлений каждой цифры от 0 до 9\n    for (i in 0..<n) {\n        val d = digit(nums[i], exp) // Получить k-й разряд nums[i], обозначив его как d\n        counter[d]++                // Подсчитать число появлений цифры d\n    }\n    // Вычислить префиксные суммы и преобразовать «число появлений» в «индекс массива»\n    for (i in 1..9) {\n        counter[i] += counter[i - 1]\n    }\n    // Выполняя обратный проход, заполнить res элементами по статистике в корзинах\n    val res = IntArray(n)\n    for (i in n - 1 downTo 0) {\n        val d = digit(nums[i], exp)\n        val j = counter[d] - 1 // Получить индекс j цифры d в массиве\n        res[j] = nums[i]       // Поместить текущий элемент по индексу j\n        counter[d]--           // Уменьшить количество d на 1\n    }\n    // Перезаписать исходный массив nums результатом\n    for (i in 0..<n)\n        nums[i] = res[i]\n}\n\n/* Поразрядная сортировка */\nfun radixSort(nums: IntArray) {\n    // Получить максимальный элемент массива, чтобы определить максимальное число разрядов\n    var m = Int.MIN_VALUE\n    for (num in nums) if (num > m) m = num\n    var exp = 1\n    // Проходить разряды от младшего к старшему\n    while (exp <= m) {\n        // Выполнить сортировку подсчетом по k-му разряду элементов массива\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // то есть exp = 10^(k-1)\n        countingSortDigit(nums, exp)\n        exp *= 10\n    }\n}\n
        radix_sort.rb
        ### Получить k-й разряд элемента num, где exp = 10^(k-1) ###\ndef digit(num, exp)\n  # Передача exp вместо k позволяет избежать повторного выполнения дорогостоящих вычислений степени\n  (num / exp) % 10\nend\n\n### Получить k-й разряд элемента num, где exp = 10^(k-1) ###\ndef digit(num, exp)\n  # Передача exp вместо k позволяет избежать повторного выполнения дорогостоящих вычислений степени\n  (num / exp) % 10\nend\n\n# ## Сортировка подсчетом (сортировка по k-му разряду nums) ###\ndef counting_sort_digit(nums, exp)\n  # Разряды десятичной системы лежат в диапазоне 0~9, поэтому нужен массив корзин длины 10\n  counter = Array.new(10, 0)\n  n = nums.length\n  # Подсчитать число появлений каждой цифры от 0 до 9\n  for i in 0...n\n    d = digit(nums[i], exp) # Получить k-й разряд nums[i], обозначив его как d\n    counter[d] += 1 # Подсчитать число появлений цифры d\n  end\n  # Вычислить префиксные суммы и преобразовать «число появлений» в «индекс массива»\n  (1...10).each { |i| counter[i] += counter[i - 1] }\n  # Выполняя обратный проход, заполнить res элементами по статистике в корзинах\n  res = Array.new(n, 0)\n  for i in (n - 1).downto(0)\n    d = digit(nums[i], exp)\n    j = counter[d] - 1 # Получить индекс j цифры d в массиве\n    res[j] = nums[i] # Поместить текущий элемент по индексу j\n    counter[d] -= 1 # Уменьшить количество d на 1\n  end\n  # Перезаписать исходный массив nums результатом\n  (0...n).each { |i| nums[i] = res[i] }\nend\n\n### Поразрядная сортировка ###\ndef radix_sort(nums)\n  # Получить максимальный элемент массива, чтобы определить максимальное число разрядов\n  m = nums.max\n  # Проходить разряды от младшего к старшему\n  exp = 1\n  while exp <= m\n    # Выполнить сортировку подсчетом по k-му разряду элементов массива\n    # k = 1 -> exp = 1\n    # k = 2 -> exp = 10\n    # то есть exp = 10^(k-1)\n    counting_sort_digit(nums, exp)\n    exp *= 10\n  end\nend\n
        Визуализация кода

        Во весь экран >

        Почему сортировка выполняется от младшего разряда к старшему?

        В последовательных раундах сортировки результаты более позднего раунда перекрывают результаты предыдущего. Например, если после первого раунда получилось \\(a < b\\) , а после второго - \\(a > b\\) , то именно результат второго раунда станет окончательным. Поскольку старшие разряды имеют более высокий приоритет, сначала нужно сортировать по младшим разрядам, а затем по старшим.

        ","path":["Глава 11. Сортировка","11.10   Поразрядная сортировка"],"tags":[]},{"location":"chapter_sorting/radix_sort/#11102","level":2,"title":"11.10.2   Характеристики алгоритма","text":"

        По сравнению с сортировкой подсчетом поразрядная сортировка подходит для случаев с большим диапазоном чисел, но только при условии, что данные можно представить в виде чисел фиксированной длины и число разрядов не слишком велико. Например, числа с плавающей запятой плохо подходят для поразрядной сортировки, потому что число разрядов \\(k\\) слишком велико и может привести к ситуации \\(O(nk) \\gg O(n^2)\\) .

        • Временная сложность равна \\(O(nk)\\), алгоритм не является адаптивным: пусть объем данных равен \\(n\\) , числа записаны в системе счисления с основанием \\(d\\) , а максимальное число разрядов равно \\(k\\) . Тогда выполнение сортировки подсчетом для одного разряда требует \\(O(n + d)\\) времени, а сортировка по всем \\(k\\) разрядам требует \\(O((n + d)k)\\) времени. Обычно \\(d\\) и \\(k\\) сравнительно малы, поэтому временная сложность стремится к \\(O(n)\\) .
        • Пространственная сложность равна \\(O(n + d)\\), сортировка не выполняется на месте: как и в сортировке подсчетом, здесь требуются массивы res и counter длины \\(n\\) и \\(d\\) .
        • Стабильная сортировка: если сортировка подсчетом стабильна, то и поразрядная сортировка стабильна. Если же сортировка подсчетом нестабильна, поразрядная сортировка не может гарантировать корректный результат.
        ","path":["Глава 11. Сортировка","11.10   Поразрядная сортировка"],"tags":[]},{"location":"chapter_sorting/selection_sort/","level":1,"title":"11.2   Сортировка выбором","text":"

        Сортировка выбором (selection sort) работает очень просто: запускается цикл, и на каждом шаге из неотсортированного диапазона выбирается минимальный элемент, после чего он переносится в конец уже отсортированного диапазона.

        Пусть длина массива равна \\(n\\). Тогда процесс сортировки выбором выглядит так, как показано на рисунке 11-2.

        1. В начальном состоянии все элементы не отсортированы, то есть неотсортированный диапазон индексов равен \\([0, n-1]\\) .
        2. Выбрать минимальный элемент из диапазона \\([0, n-1]\\) и поменять его местами с элементом в позиции \\(0\\) . После этого первый элемент массива отсортирован.
        3. Выбрать минимальный элемент из диапазона \\([1, n-1]\\) и поменять его местами с элементом в позиции \\(1\\) . После этого первые два элемента массива отсортированы.
        4. Продолжать по аналогии. После \\(n - 1\\) раундов выбора и обмена первые \\(n - 1\\) элементов массива будут отсортированы.
        5. Оставшийся элемент обязательно является максимальным, сортировать его не нужно, поэтому массив считается отсортированным.
        <1><2><3><4><5><6><7><8><9><10><11>

        Рисунок 11-2   Шаги сортировки выбором

        В коде мы используем \\(k\\) для записи минимального элемента в пределах неотсортированного диапазона:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby selection_sort.py
        def selection_sort(nums: list[int]):\n    \"\"\"Сортировка выбором\"\"\"\n    n = len(nums)\n    # Внешний цикл: неотсортированный диапазон [i, n-1]\n    for i in range(n - 1):\n        # Внутренний цикл: найти минимальный элемент в неотсортированном диапазоне\n        k = i\n        for j in range(i + 1, n):\n            if nums[j] < nums[k]:\n                k = j  # Записать индекс минимального элемента\n        # Поменять этот минимальный элемент местами с первым элементом неотсортированного диапазона\n        nums[i], nums[k] = nums[k], nums[i]\n
        selection_sort.cpp
        /* Сортировка выбором */\nvoid selectionSort(vector<int> &nums) {\n    int n = nums.size();\n    // Внешний цикл: неотсортированный диапазон [i, n-1]\n    for (int i = 0; i < n - 1; i++) {\n        // Внутренний цикл: найти минимальный элемент в неотсортированном диапазоне\n        int k = i;\n        for (int j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k])\n                k = j; // Записать индекс минимального элемента\n        }\n        // Поменять этот минимальный элемент местами с первым элементом неотсортированного диапазона\n        swap(nums[i], nums[k]);\n    }\n}\n
        selection_sort.java
        /* Сортировка выбором */\nvoid selectionSort(int[] nums) {\n    int n = nums.length;\n    // Внешний цикл: неотсортированный диапазон [i, n-1]\n    for (int i = 0; i < n - 1; i++) {\n        // Внутренний цикл: найти минимальный элемент в неотсортированном диапазоне\n        int k = i;\n        for (int j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k])\n                k = j; // Записать индекс минимального элемента\n        }\n        // Поменять этот минимальный элемент местами с первым элементом неотсортированного диапазона\n        int temp = nums[i];\n        nums[i] = nums[k];\n        nums[k] = temp;\n    }\n}\n
        selection_sort.cs
        /* Сортировка выбором */\nvoid SelectionSort(int[] nums) {\n    int n = nums.Length;\n    // Внешний цикл: неотсортированный диапазон [i, n-1]\n    for (int i = 0; i < n - 1; i++) {\n        // Внутренний цикл: найти минимальный элемент в неотсортированном диапазоне\n        int k = i;\n        for (int j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k])\n                k = j; // Записать индекс минимального элемента\n        }\n        // Поменять этот минимальный элемент местами с первым элементом неотсортированного диапазона\n        (nums[k], nums[i]) = (nums[i], nums[k]);\n    }\n}\n
        selection_sort.go
        /* Сортировка выбором */\nfunc selectionSort(nums []int) {\n    n := len(nums)\n    // Внешний цикл: неотсортированный диапазон [i, n-1]\n    for i := 0; i < n-1; i++ {\n        // Внутренний цикл: найти минимальный элемент в неотсортированном диапазоне\n        k := i\n        for j := i + 1; j < n; j++ {\n            if nums[j] < nums[k] {\n                // Записать индекс минимального элемента\n                k = j\n            }\n        }\n        // Поменять этот минимальный элемент местами с первым элементом неотсортированного диапазона\n        nums[i], nums[k] = nums[k], nums[i]\n\n    }\n}\n
        selection_sort.swift
        /* Сортировка выбором */\nfunc selectionSort(nums: inout [Int]) {\n    // Внешний цикл: неотсортированный диапазон [i, n-1]\n    for i in nums.indices.dropLast() {\n        // Внутренний цикл: найти минимальный элемент в неотсортированном диапазоне\n        var k = i\n        for j in nums.indices.dropFirst(i + 1) {\n            if nums[j] < nums[k] {\n                k = j // Записать индекс минимального элемента\n            }\n        }\n        // Поменять этот минимальный элемент местами с первым элементом неотсортированного диапазона\n        nums.swapAt(i, k)\n    }\n}\n
        selection_sort.js
        /* Сортировка выбором */\nfunction selectionSort(nums) {\n    let n = nums.length;\n    // Внешний цикл: неотсортированный диапазон [i, n-1]\n    for (let i = 0; i < n - 1; i++) {\n        // Внутренний цикл: найти минимальный элемент в неотсортированном диапазоне\n        let k = i;\n        for (let j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k]) {\n                k = j; // Записать индекс минимального элемента\n            }\n        }\n        // Поменять этот минимальный элемент местами с первым элементом неотсортированного диапазона\n        [nums[i], nums[k]] = [nums[k], nums[i]];\n    }\n}\n
        selection_sort.ts
        /* Сортировка выбором */\nfunction selectionSort(nums: number[]): void {\n    let n = nums.length;\n    // Внешний цикл: неотсортированный диапазон [i, n-1]\n    for (let i = 0; i < n - 1; i++) {\n        // Внутренний цикл: найти минимальный элемент в неотсортированном диапазоне\n        let k = i;\n        for (let j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k]) {\n                k = j; // Записать индекс минимального элемента\n            }\n        }\n        // Поменять этот минимальный элемент местами с первым элементом неотсортированного диапазона\n        [nums[i], nums[k]] = [nums[k], nums[i]];\n    }\n}\n
        selection_sort.dart
        /* Сортировка выбором */\nvoid selectionSort(List<int> nums) {\n  int n = nums.length;\n  // Внешний цикл: неотсортированный диапазон [i, n-1]\n  for (int i = 0; i < n - 1; i++) {\n    // Внутренний цикл: найти минимальный элемент в неотсортированном диапазоне\n    int k = i;\n    for (int j = i + 1; j < n; j++) {\n      if (nums[j] < nums[k]) k = j; // Записать индекс минимального элемента\n    }\n    // Поменять этот минимальный элемент местами с первым элементом неотсортированного диапазона\n    int temp = nums[i];\n    nums[i] = nums[k];\n    nums[k] = temp;\n  }\n}\n
        selection_sort.rs
        /* Сортировка выбором */\nfn selection_sort(nums: &mut [i32]) {\n    if nums.is_empty() {\n        return;\n    }\n    let n = nums.len();\n    // Внешний цикл: неотсортированный диапазон [i, n-1]\n    for i in 0..n - 1 {\n        // Внутренний цикл: найти минимальный элемент в неотсортированном диапазоне\n        let mut k = i;\n        for j in i + 1..n {\n            if nums[j] < nums[k] {\n                k = j; // Записать индекс минимального элемента\n            }\n        }\n        // Поменять этот минимальный элемент местами с первым элементом неотсортированного диапазона\n        nums.swap(i, k);\n    }\n}\n
        selection_sort.c
        /* Сортировка выбором */\nvoid selectionSort(int nums[], int n) {\n    // Внешний цикл: неотсортированный диапазон [i, n-1]\n    for (int i = 0; i < n - 1; i++) {\n        // Внутренний цикл: найти минимальный элемент в неотсортированном диапазоне\n        int k = i;\n        for (int j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k])\n                k = j; // Записать индекс минимального элемента\n        }\n        // Поменять этот минимальный элемент местами с первым элементом неотсортированного диапазона\n        int temp = nums[i];\n        nums[i] = nums[k];\n        nums[k] = temp;\n    }\n}\n
        selection_sort.kt
        /* Сортировка выбором */\nfun selectionSort(nums: IntArray) {\n    val n = nums.size\n    // Внешний цикл: неотсортированный диапазон [i, n-1]\n    for (i in 0..<n - 1) {\n        var k = i\n        // Внутренний цикл: найти минимальный элемент в неотсортированном диапазоне\n        for (j in i + 1..<n) {\n            if (nums[j] < nums[k])\n                k = j // Записать индекс минимального элемента\n        }\n        // Поменять этот минимальный элемент местами с первым элементом неотсортированного диапазона\n        val temp = nums[i]\n        nums[i] = nums[k]\n        nums[k] = temp\n    }\n}\n
        selection_sort.rb
        ### Сортировка выбором ###\ndef selection_sort(nums)\n  n = nums.length\n  # Внешний цикл: неотсортированный диапазон [i, n-1]\n  for i in 0...(n - 1)\n    # Внутренний цикл: найти минимальный элемент в неотсортированном диапазоне\n    k = i\n    for j in (i + 1)...n\n      if nums[j] < nums[k]\n        k = j # Записать индекс минимального элемента\n      end\n    end\n    # Поменять этот минимальный элемент местами с первым элементом неотсортированного диапазона\n    nums[i], nums[k] = nums[k], nums[i]\n  end\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 11. Сортировка","11.2   Сортировка выбором"],"tags":[]},{"location":"chapter_sorting/selection_sort/#1121","level":2,"title":"11.2.1   Характеристики алгоритма","text":"
        • Временная сложность равна \\(O(n^2)\\), сортировка не является адаптивной: внешний цикл выполняется \\(n - 1\\) раз. В первом раунде длина неотсортированного диапазона равна \\(n\\) , а в последнем - \\(2\\) , то есть отдельные раунды содержат \\(n\\), \\(n - 1\\), \\(\\dots\\), \\(3\\), \\(2\\) проходов внутреннего цикла, их сумма равна \\(\\frac{(n - 1)(n + 2)}{2}\\) .
        • Пространственная сложность равна \\(O(1)\\), сортировка выполняется на месте: указатели \\(i\\) и \\(j\\) используют константный объем дополнительной памяти.
        • Нестабильная сортировка: как показано на рисунке 11-3, элемент nums[i] может быть переставлен вправо от другого равного ему элемента, из-за чего их относительный порядок изменится.

        Рисунок 11-3   Пример нестабильности сортировки выбором

        ","path":["Глава 11. Сортировка","11.2   Сортировка выбором"],"tags":[]},{"location":"chapter_sorting/sorting_algorithm/","level":1,"title":"11.1   Алгоритмы сортировки","text":"

        Алгоритмы сортировки (sorting algorithm) используются для упорядочивания набора данных по определенному правилу. Они применяются очень широко, потому что упорядоченные данные обычно проще анализировать, обрабатывать и искать в них нужные элементы.

        Как показано на рисунке 11-1, данными в алгоритмах сортировки могут быть целые числа, числа с плавающей запятой, символы, строки и другие типы. Критерий сравнения тоже можно задать по-разному, например по величине чисел, по порядку ASCII-кодов символов или по пользовательскому правилу.

        Рисунок 11-1   Примеры типов данных и правил сравнения

        ","path":["Глава 11. Сортировка","11.1   Алгоритмы сортировки"],"tags":[]},{"location":"chapter_sorting/sorting_algorithm/#1111","level":2,"title":"11.1.1   Критерии оценки","text":"

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

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

        Стабильность: стабильная сортировка после завершения не меняет относительный порядок одинаковых элементов в массиве.

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

        # Входные данные уже отсортированы по имени\n# (name, age)\n  ('A', 19)\n  ('B', 18)\n  ('C', 21)\n  ('D', 19)\n  ('E', 23)\n\n# Если затем нестабильным алгоритмом отсортировать список по возрасту,\n# относительный порядок ('D', 19) и ('A', 19) изменится,\n# и свойство упорядоченности по имени будет потеряно\n  ('B', 18)\n  ('D', 19)\n  ('A', 19)\n  ('C', 21)\n  ('E', 23)\n

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

        Основанность на сравнении: сортировка на основе сравнений использует операторы сравнения (\\(<\\), \\(=\\), \\(>\\)), чтобы определить относительный порядок элементов и отсортировать массив. Ее теоретически лучшая временная сложность равна \\(O(n \\log n)\\) . А вот сортировка без сравнений не опирается на операторы сравнения, поэтому может достигать \\(O(n)\\) , но универсальность у нее ниже.

        ","path":["Глава 11. Сортировка","11.1   Алгоритмы сортировки"],"tags":[]},{"location":"chapter_sorting/sorting_algorithm/#1112","level":2,"title":"11.1.2   Идеальный алгоритм сортировки","text":"

        Быстрый, выполняющийся на месте, стабильный, адаптивный и универсальный. Очевидно, что на сегодняшний день не существует алгоритма сортировки, который одновременно обладал бы всеми этими свойствами. Поэтому при выборе алгоритма сортировки нужно исходить из конкретных особенностей данных и требований задачи.

        Далее мы последовательно изучим разные алгоритмы сортировки и на основании приведенных выше критериев разберем их преимущества и недостатки.

        ","path":["Глава 11. Сортировка","11.1   Алгоритмы сортировки"],"tags":[]},{"location":"chapter_sorting/summary/","level":1,"title":"11.11   Резюме","text":"","path":["Глава 11. Сортировка","11.11   Резюме"],"tags":[]},{"location":"chapter_sorting/summary/#1","level":3,"title":"1.   Ключевые выводы","text":"
        • Сортировка пузырьком выполняет сортировку за счет обмена соседних элементов. Если добавить флаг для досрочного выхода, лучшую временную сложность пузырьковой сортировки можно оптимизировать до \\(O(n)\\) .
        • Сортировка вставками на каждом раунде вставляет элемент из неотсортированного диапазона в правильную позицию внутри отсортированного диапазона. Хотя ее временная сложность равна \\(O(n^2)\\) , она очень популярна для задач сортировки небольших массивов, поскольку число элементарных операций у нее сравнительно невелико.
        • Быстрая сортировка основана на операции разделения с опорным элементом. При неудачном выборе опорного элемента на каждом раунде ее временная сложность может деградировать до \\(O(n^2)\\) . Использование медианы трех элементов или случайного опорного элемента уменьшает вероятность этой деградации. Если всегда рекурсивно обрабатывать более короткий поддиапазон первым, можно эффективно уменьшить глубину рекурсии и оптимизировать пространственную сложность до \\(O(\\log n)\\) .
        • Сортировка слиянием включает этапы разделения и слияния и служит типичным проявлением стратегии «разделяй и властвуй». Для сортировки массива ей требуется вспомогательный массив, поэтому пространственная сложность равна \\(O(n)\\). Однако при сортировке связного списка пространственную сложность можно оптимизировать до \\(O(1)\\) .
        • Блочная сортировка включает три этапа: распределение данных по блокам, сортировку внутри блоков и объединение результатов. Она тоже отражает стратегию «разделяй и властвуй» и подходит для очень больших объемов данных. Ключ к эффективности блочной сортировки - равномерное распределение данных.
        • Сортировка подсчетом является частным случаем блочной сортировки. Она реализует сортировку через подсчет числа вхождений данных. Сортировка подсчетом подходит для случаев, когда объем данных велик, но диапазон значений ограничен, и при этом данные можно преобразовать в положительные целые числа.
        • Поразрядная сортировка выполняет сортировку данных путем последовательной сортировки по каждому разряду и требует, чтобы данные можно было представить в виде чисел фиксированной разрядности.
        • В общем случае нам хотелось бы найти алгоритм сортировки, который одновременно обладал бы высокой эффективностью, стабильностью, выполнением на месте и адаптивностью. Но, как и в других разделах алгоритмов и структур данных, не существует одного алгоритма сортировки, способного удовлетворить всем этим требованиям одновременно. На практике приходится выбирать подходящий алгоритм в зависимости от свойств данных.
        • На рисунке 11-19 сравниваются эффективность, стабильность, выполнение на месте и адаптивность основных алгоритмов сортировки.

        Рисунок 11-19   Сравнение алгоритмов сортировки

        ","path":["Глава 11. Сортировка","11.11   Резюме"],"tags":[]},{"location":"chapter_sorting/summary/#2","level":3,"title":"2.   Вопросы и ответы","text":"

        В: В каких случаях стабильность алгоритма сортировки является обязательной?

        В реальных задачах нам может понадобиться сортировать объекты по некоторому атрибуту. Например, у студентов есть два атрибута: имя и рост. Мы хотим выполнить многоуровневую сортировку: сначала отсортировать по имени и получить (A, 180) (B, 185) (C, 170) (D, 170) , а затем отсортировать по росту. Если используемый алгоритм сортировки нестабилен, то мы можем получить (D, 170) (C, 170) (A, 180) (B, 185) .

        Нетрудно увидеть, что в этом случае студенты D и C поменялись местами, порядок по имени разрушился, а именно этого мы и не хотим.

        В: Можно ли поменять местами порядок «поиска справа налево» и «поиска слева направо» в разделении с опорным элементом?

        Нет. Если в качестве опорного элемента выбирается самый левый элемент, необходимо сначала выполнять «поиск справа налево», а уже затем - «поиск слева направо». Этот вывод кажется немного неочевидным, поэтому разберем его подробнее.

        Последний шаг partition() - это обмен nums[left] и nums[i] . После обмена все элементы слева от опорного должны быть <= опорного, а значит, перед этим обменом должно выполняться условие nums[left] >= nums[i]. Если сначала выполнять «поиск слева направо», то в случае, когда не удается найти элемент больше опорного, цикл завершится в состоянии i == j , и при этом может оказаться, что nums[j] == nums[i] > nums[left]. Иными словами, на последнем шаге обмена элемент, больший опорного, будет помещен в начало массива, из-за чего разделение завершится неверно.

        Например, для массива [0, 0, 0, 0, 1] , если сначала выполнять «поиск слева направо», после разделения получится [1, 0, 0, 0, 0] , а это неправильный результат.

        Если же выбрать nums[right] в качестве опорного элемента, то ситуация станет противоположной, и тогда сначала нужно выполнять «поиск слева направо».

        В: Почему при оптимизации глубины рекурсии в быстрой сортировке выбор короткого массива гарантирует, что глубина рекурсии не превысит \\(\\log n\\) ?

        Глубина рекурсии - это число текущих рекурсивных вызовов, которые еще не завершились. На каждом раунде разделения исходный массив разбивается на два подмассива. После оптимизации глубины рекурсии длина подмассива, в который мы продолжаем рекурсивный спуск, не превышает половины длины исходного массива. Если рассматривать худший случай, когда длина каждый раз становится ровно вдвое меньше, итоговая глубина рекурсии и будет равна \\(\\log n\\) .

        В исходной версии быстрой сортировки может происходить последовательный рекурсивный вызов для более длинных массивов. В худшем случае это будут длины \\(n\\) , \\(n - 1\\) , \\(\\dots\\) , \\(2\\) , \\(1\\) , а глубина рекурсии окажется равной \\(n\\) . Оптимизация глубины рекурсии как раз и позволяет избежать такого сценария.

        В: Если все элементы массива равны, будет ли временная сложность быстрой сортировки равна \\(O(n^2)\\) ? Как справиться с таким вырождением?

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

        В: Почему худшая временная сложность блочной сортировки равна \\(O(n^2)\\) ?

        В худшем случае все элементы попадут в один и тот же блок. Если затем сортировать этот блок алгоритмом со сложностью \\(O(n^2)\\) , то общая временная сложность тоже станет \\(O(n^2)\\) .

        ","path":["Глава 11. Сортировка","11.11   Резюме"],"tags":[]},{"location":"chapter_stack_and_queue/","level":1,"title":"Глава 5.   Стек и очередь","text":"

        Abstract

        Стек и очередь - две базовые линейные структуры данных.

        Они соответственно воплощают принципы «последним пришел - первым вышел» и «первым пришел - первым вышел».

        ","path":["Глава 5. Стек и очередь","Глава 5.   Стек и очередь"],"tags":[]},{"location":"chapter_stack_and_queue/#_1","level":2,"title":"Содержание главы","text":"
        • 5.1   Стек
        • 5.2   Очередь
        • 5.3   Двусторонняя очередь
        • 5.4   Резюме
        ","path":["Глава 5. Стек и очередь","Глава 5.   Стек и очередь"],"tags":[]},{"location":"chapter_stack_and_queue/deque/","level":1,"title":"5.3   Двусторонняя очередь","text":"

        В обычной очереди мы можем удалять элементы только из головы и добавлять их только в хвост. Как показано на рисунке 5-7, двусторонняя очередь (double-ended queue) обеспечивает большую гибкость и позволяет выполнять добавление и удаление элементов как с головы, так и с хвоста.

        Рисунок 5-7   Операции двусторонней очереди

        ","path":["Глава 5. Стек и очередь","5.3   Двусторонняя очередь"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#531","level":2,"title":"5.3.1   Основные операции с двусторонней очередью","text":"

        Распространенные операции двусторонней очереди приведены в таблице 5-3. Конкретные названия методов зависят от используемого языка программирования.

        Таблица 5-3   Эффективность операций двусторонней очереди

        Имя метода Описание Временная сложность push_first() Добавить элемент в голову очереди \\(O(1)\\) push_last() Добавить элемент в хвост очереди \\(O(1)\\) pop_first() Удалить элемент из головы очереди \\(O(1)\\) pop_last() Удалить элемент из хвоста очереди \\(O(1)\\) peek_first() Просмотреть элемент в голове очереди \\(O(1)\\) peek_last() Просмотреть элемент в хвосте очереди \\(O(1)\\)

        Точно так же мы можем напрямую использовать уже реализованные в языках программирования классы двусторонней очереди:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby deque.py
        from collections import deque\n\n# Инициализация двусторонней очереди\ndeq: deque[int] = deque()\n\n# Поместить элементы в очередь\ndeq.append(2)      # Добавить в хвост\ndeq.append(5)\ndeq.append(4)\ndeq.appendleft(3)  # Добавить в голову\ndeq.appendleft(1)\n\n# Просмотреть элементы\nfront: int = deq[0]  # Элемент в голове\nrear: int = deq[-1]  # Элемент в хвосте\n\n# Извлечь элементы из очереди\npop_front: int = deq.popleft()  # Извлечь элемент из головы\npop_rear: int = deq.pop()       # Извлечь элемент из хвоста\n\n# Получить длину двусторонней очереди\nsize: int = len(deq)\n\n# Проверить, пуста ли двусторонняя очередь\nis_empty: bool = len(deq) == 0\n
        deque.cpp
        /* Инициализация двусторонней очереди */\ndeque<int> deque;\n\n/* Поместить элементы в очередь */\ndeque.push_back(2);   // Добавить в хвост\ndeque.push_back(5);\ndeque.push_back(4);\ndeque.push_front(3);  // Добавить в голову\ndeque.push_front(1);\n\n/* Просмотреть элементы */\nint front = deque.front(); // Элемент в голове\nint back = deque.back();   // Элемент в хвосте\n\n/* Извлечь элементы из очереди */\ndeque.pop_front();  // Извлечь элемент из головы\ndeque.pop_back();   // Извлечь элемент из хвоста\n\n/* Получить длину двусторонней очереди */\nint size = deque.size();\n\n/* Проверить, пуста ли двусторонняя очередь */\nbool empty = deque.empty();\n
        deque.java
        /* Инициализация двусторонней очереди */\nDeque<Integer> deque = new LinkedList<>();\n\n/* Поместить элементы в очередь */\ndeque.offerLast(2);   // Добавить в хвост\ndeque.offerLast(5);\ndeque.offerLast(4);\ndeque.offerFirst(3);  // Добавить в голову\ndeque.offerFirst(1);\n\n/* Просмотреть элементы */\nint peekFirst = deque.peekFirst();  // Элемент в голове\nint peekLast = deque.peekLast();    // Элемент в хвосте\n\n/* Извлечь элементы из очереди */\nint popFirst = deque.pollFirst();  // Извлечь элемент из головы\nint popLast = deque.pollLast();    // Извлечь элемент из хвоста\n\n/* Получить длину двусторонней очереди */\nint size = deque.size();\n\n/* Проверить, пуста ли двусторонняя очередь */\nboolean isEmpty = deque.isEmpty();\n
        deque.cs
        /* Инициализация двусторонней очереди */\n// В C# двустороннюю очередь обычно моделируют через связный список LinkedList\nLinkedList<int> deque = new();\n\n/* Поместить элементы в очередь */\ndeque.AddLast(2);   // Добавить в хвост\ndeque.AddLast(5);\ndeque.AddLast(4);\ndeque.AddFirst(3);  // Добавить в голову\ndeque.AddFirst(1);\n\n/* Просмотреть элементы */\nint peekFirst = deque.First.Value;  // Элемент в голове\nint peekLast = deque.Last.Value;    // Элемент в хвосте\n\n/* Извлечь элементы из очереди */\ndeque.RemoveFirst();  // Извлечь элемент из головы\ndeque.RemoveLast();   // Извлечь элемент из хвоста\n\n/* Получить длину двусторонней очереди */\nint size = deque.Count;\n\n/* Проверить, пуста ли двусторонняя очередь */\nbool isEmpty = deque.Count == 0;\n
        deque_test.go
        /* Инициализация двусторонней очереди */\n// В Go list обычно используется как двусторонняя очередь\ndeque := list.New()\n\n/* Поместить элементы в очередь */\ndeque.PushBack(2)      // Добавить в хвост\ndeque.PushBack(5)\ndeque.PushBack(4)\ndeque.PushFront(3)     // Добавить в голову\ndeque.PushFront(1)\n\n/* Просмотреть элементы */\nfront := deque.Front() // Элемент в голове\nrear := deque.Back()   // Элемент в хвосте\n\n/* Извлечь элементы из очереди */\ndeque.Remove(front)    // Извлечь элемент из головы\ndeque.Remove(rear)     // Извлечь элемент из хвоста\n\n/* Получить длину двусторонней очереди */\nsize := deque.Len()\n\n/* Проверить, пуста ли двусторонняя очередь */\nisEmpty := deque.Len() == 0\n
        deque.swift
        /* Инициализация двусторонней очереди */\n// В Swift нет встроенного класса двусторонней очереди, поэтому можно использовать Array как двустороннюю очередь\nvar deque: [Int] = []\n\n/* Поместить элементы в очередь */\ndeque.append(2) // Добавить в хвост\ndeque.append(5)\ndeque.append(4)\ndeque.insert(3, at: 0) // Добавить в голову\ndeque.insert(1, at: 0)\n\n/* Просмотреть элементы */\nlet peekFirst = deque.first! // Элемент в голове\nlet peekLast = deque.last! // Элемент в хвосте\n\n/* Извлечь элементы из очереди */\n// При моделировании через Array сложность popFirst равна O(n)\nlet popFirst = deque.removeFirst() // Извлечь элемент из головы\nlet popLast = deque.removeLast() // Извлечь элемент из хвоста\n\n/* Получить длину двусторонней очереди */\nlet size = deque.count\n\n/* Проверить, пуста ли двусторонняя очередь */\nlet isEmpty = deque.isEmpty\n
        deque.js
        /* Инициализация двусторонней очереди */\n// В JavaScript нет встроенной двусторонней очереди, поэтому можно использовать Array как двустороннюю очередь\nconst deque = [];\n\n/* Поместить элементы в очередь */\ndeque.push(2);\ndeque.push(5);\ndeque.push(4);\n// Обрати внимание: поскольку это массив, метод unshift() имеет сложность O(n)\ndeque.unshift(3);\ndeque.unshift(1);\n\n/* Просмотреть элементы */\nconst peekFirst = deque[0];\nconst peekLast = deque[deque.length - 1];\n\n/* Извлечь элементы из очереди */\n// Обрати внимание: поскольку это массив, метод shift() имеет сложность O(n)\nconst popFront = deque.shift();\nconst popBack = deque.pop();\n\n/* Получить длину двусторонней очереди */\nconst size = deque.length;\n\n/* Проверить, пуста ли двусторонняя очередь */\nconst isEmpty = size === 0;\n
        deque.ts
        /* Инициализация двусторонней очереди */\n// В TypeScript нет встроенной двусторонней очереди, поэтому можно использовать Array как двустороннюю очередь\nconst deque: number[] = [];\n\n/* Поместить элементы в очередь */\ndeque.push(2);\ndeque.push(5);\ndeque.push(4);\n// Обрати внимание: поскольку это массив, метод unshift() имеет сложность O(n)\ndeque.unshift(3);\ndeque.unshift(1);\n\n/* Просмотреть элементы */\nconst peekFirst: number = deque[0];\nconst peekLast: number = deque[deque.length - 1];\n\n/* Извлечь элементы из очереди */\n// Обрати внимание: поскольку это массив, метод shift() имеет сложность O(n)\nconst popFront: number = deque.shift() as number;\nconst popBack: number = deque.pop() as number;\n\n/* Получить длину двусторонней очереди */\nconst size: number = deque.length;\n\n/* Проверить, пуста ли двусторонняя очередь */\nconst isEmpty: boolean = size === 0;\n
        deque.dart
        /* Инициализация двусторонней очереди */\n// В Dart Queue определена как двусторонняя очередь\nQueue<int> deque = Queue<int>();\n\n/* Поместить элементы в очередь */\ndeque.addLast(2);  // Добавить в хвост\ndeque.addLast(5);\ndeque.addLast(4);\ndeque.addFirst(3); // Добавить в голову\ndeque.addFirst(1);\n\n/* Просмотреть элементы */\nint peekFirst = deque.first; // Элемент в голове\nint peekLast = deque.last;   // Элемент в хвосте\n\n/* Извлечь элементы из очереди */\nint popFirst = deque.removeFirst(); // Извлечь элемент из головы\nint popLast = deque.removeLast();   // Извлечь элемент из хвоста\n\n/* Получить длину двусторонней очереди */\nint size = deque.length;\n\n/* Проверить, пуста ли двусторонняя очередь */\nbool isEmpty = deque.isEmpty;\n
        deque.rs
        /* Инициализация двусторонней очереди */\nlet mut deque: VecDeque<u32> = VecDeque::new();\n\n/* Поместить элементы в очередь */\ndeque.push_back(2);  // Добавить в хвост\ndeque.push_back(5);\ndeque.push_back(4);\ndeque.push_front(3); // Добавить в голову\ndeque.push_front(1);\n\n/* Просмотреть элементы */\nif let Some(front) = deque.front() { // Элемент в голове\n}\nif let Some(rear) = deque.back() {   // Элемент в хвосте\n}\n\n/* Извлечь элементы из очереди */\nif let Some(pop_front) = deque.pop_front() { // Извлечь элемент из головы\n}\nif let Some(pop_rear) = deque.pop_back() {   // Извлечь элемент из хвоста\n}\n\n/* Получить длину двусторонней очереди */\nlet size = deque.len();\n\n/* Проверить, пуста ли двусторонняя очередь */\nlet is_empty = deque.is_empty();\n
        deque.c
        // В C нет встроенной двусторонней очереди\n
        deque.kt
        /* Инициализация двусторонней очереди */\nval deque = LinkedList<Int>()\n\n/* Поместить элементы в очередь */\ndeque.offerLast(2)  // Добавить в хвост\ndeque.offerLast(5)\ndeque.offerLast(4)\ndeque.offerFirst(3) // Добавить в голову\ndeque.offerFirst(1)\n\n/* Просмотреть элементы */\nval peekFirst = deque.peekFirst() // Элемент в голове\nval peekLast = deque.peekLast()   // Элемент в хвосте\n\n/* Извлечь элементы из очереди */\nval popFirst = deque.pollFirst() // Извлечь элемент из головы\nval popLast = deque.pollLast()   // Извлечь элемент из хвоста\n\n/* Получить длину двусторонней очереди */\nval size = deque.size\n\n/* Проверить, пуста ли двусторонняя очередь */\nval isEmpty = deque.isEmpty()\n
        deque.rb
        # Инициализация двусторонней очереди\n# В Ruby нет встроенной двусторонней очереди, поэтому можно использовать Array как двустороннюю очередь\ndeque = []\n\n# Поместить элементы в очередь\ndeque << 2\ndeque << 5\ndeque << 4\n# Обрати внимание: поскольку это массив, метод Array#unshift имеет сложность O(n)\ndeque.unshift(3)\ndeque.unshift(1)\n\n# Просмотреть элементы\npeek_first = deque.first\npeek_last = deque.last\n\n# Извлечь элементы из очереди\n# Обрати внимание: поскольку это массив, метод Array#shift имеет сложность O(n)\npop_front = deque.shift\npop_back = deque.pop\n\n# Получить длину двусторонней очереди\nsize = deque.length\n\n# Проверить, пуста ли двусторонняя очередь\nis_empty = size.zero?\n
        Визуализация выполнения

        https://pythontutor.com/render.html#code=from%20collections%20import%20deque%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D0%B4%D0%B2%D1%83%D1%81%D1%82%D0%BE%D1%80%D0%BE%D0%BD%D0%BD%D1%8E%D1%8E%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C%0A%20%20%20%20deq%20%3D%20deque%28%29%0A%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BC%D0%B5%D1%81%D1%82%D0%B8%D1%82%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B2%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C%0A%20%20%20%20deq.append%282%29%20%20%23%20%D0%94%D0%BE%D0%B1%D0%B0%D0%B2%D0%B8%D1%82%D1%8C%20%D0%B2%20%D1%85%D0%B2%D0%BE%D1%81%D1%82%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%0A%20%20%20%20deq.append%285%29%0A%20%20%20%20deq.append%284%29%0A%20%20%20%20deq.appendleft%283%29%20%20%23%20%D0%94%D0%BE%D0%B1%D0%B0%D0%B2%D0%B8%D1%82%D1%8C%20%D0%B2%20%D0%B3%D0%BE%D0%BB%D0%BE%D0%B2%D1%83%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%0A%20%20%20%20deq.appendleft%281%29%0A%20%20%20%20print%28%22%D0%B4%D0%B2%D1%83%D1%81%D1%82%D0%BE%D1%80%D0%BE%D0%BD%D0%BD%D1%8F%D1%8F%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C%20deque%20%3D%22%2C%20deq%29%0A%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B8%D1%82%D1%8C%20%D0%B4%D0%BE%D1%81%D1%82%D1%83%D0%BF%20%D0%BA%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D1%83%0A%20%20%20%20front%20%3D%20deq%5B0%5D%20%20%23%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B2%20%D0%B3%D0%BE%D0%BB%D0%BE%D0%B2%D0%B5%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%0A%20%20%20%20print%28%22%D0%AD%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B2%20%D0%BD%D0%B0%D1%87%D0%B0%D0%BB%D0%B5%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%20front%20%3D%22%2C%20front%29%0A%20%20%20%20rear%20%3D%20deq%5B-1%5D%20%20%23%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B2%20%D1%85%D0%B2%D0%BE%D1%81%D1%82%D0%B5%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%0A%20%20%20%20print%28%22%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B2%20%D1%85%D0%B2%D0%BE%D1%81%D1%82%D0%B5%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%20rear%20%3D%22%2C%20rear%29%0A%0A%20%20%20%20%23%20%D0%98%D0%B7%D0%B2%D0%BB%D0%B5%D1%87%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B8%D0%B7%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%0A%20%20%20%20pop_front%20%3D%20deq.popleft%28%29%20%20%23%20%D0%B3%D0%BE%D0%BB%D0%BE%D0%B2%D0%B0%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%D0%98%D0%B7%D0%B2%D0%BB%D0%B5%D1%87%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B8%D0%B7%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%0A%20%20%20%20print%28%22%D0%AD%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%2C%20%D0%B8%D0%B7%D0%B2%D0%BB%D0%B5%D1%87%D0%B5%D0%BD%D0%BD%D1%8B%D0%B9%20%D0%B8%D0%B7%20%D0%B3%D0%BE%D0%BB%D0%BE%D0%B2%D1%8B%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%2C%20pop_front%20%3D%22%2C%20pop_front%29%0A%20%20%20%20print%28%22deque%20%D0%BF%D0%BE%D1%81%D0%BB%D0%B5%20%D0%B8%D0%B7%D0%B2%D0%BB%D0%B5%D1%87%D0%B5%D0%BD%D0%B8%D1%8F%20%D0%B8%D0%B7%20%D0%B3%D0%BE%D0%BB%D0%BE%D0%B2%D1%8B%20%3D%22%2C%20deq%29%0A%20%20%20%20pop_rear%20%3D%20deq.pop%28%29%20%20%23%20%D1%85%D0%B2%D0%BE%D1%81%D1%82%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%D0%98%D0%B7%D0%B2%D0%BB%D0%B5%D1%87%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B8%D0%B7%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%0A%20%20%20%20print%28%22%D0%AD%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%2C%20%D0%B8%D0%B7%D0%B2%D0%BB%D0%B5%D1%87%D0%B5%D0%BD%D0%BD%D1%8B%D0%B9%20%D0%B8%D0%B7%20%D1%85%D0%B2%D0%BE%D1%81%D1%82%D0%B0%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%2C%20pop_rear%20%3D%22%2C%20pop_rear%29%0A%20%20%20%20print%28%22deque%20%D0%BF%D0%BE%D1%81%D0%BB%D0%B5%20%D0%B8%D0%B7%D0%B2%D0%BB%D0%B5%D1%87%D0%B5%D0%BD%D0%B8%D1%8F%20%D0%B8%D0%B7%20%D1%85%D0%B2%D0%BE%D1%81%D1%82%D0%B0%20%3D%22%2C%20deq%29%0A%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B8%D1%82%D1%8C%20%D0%B4%D0%BB%D0%B8%D0%BD%D1%83%20%D0%B4%D0%B2%D1%83%D1%81%D1%82%D0%BE%D1%80%D0%BE%D0%BD%D0%BD%D0%B5%D0%B9%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%0A%20%20%20%20size%20%3D%20len%28deq%29%0A%20%20%20%20print%28%22%D0%94%D0%BB%D0%B8%D0%BD%D0%B0%20%D0%B4%D0%B2%D1%83%D1%81%D1%82%D0%BE%D1%80%D0%BE%D0%BD%D0%BD%D0%B5%D0%B9%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%20size%20%3D%22%2C%20size%29%0A%0A%20%20%20%20%23%20%D0%9F%D1%80%D0%BE%D0%B2%D0%B5%D1%80%D0%B8%D1%82%D1%8C%2C%20%D0%BF%D1%83%D1%81%D1%82%D0%B0%20%D0%BB%D0%B8%20%D0%B4%D0%B2%D1%83%D1%81%D1%82%D0%BE%D1%80%D0%BE%D0%BD%D0%BD%D1%8F%D1%8F%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C%0A%20%20%20%20is_empty%20%3D%20len%28deq%29%20%3D%3D%200%0A%20%20%20%20print%28%22%D0%9F%D1%83%D1%81%D1%82%D0%B0%20%D0%BB%D0%B8%20%D0%B4%D0%B2%D1%83%D1%81%D1%82%D0%BE%D1%80%D0%BE%D0%BD%D0%BD%D1%8F%D1%8F%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C%20%3D%22%2C%20is_empty%29&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

        ","path":["Глава 5. Стек и очередь","5.3   Двусторонняя очередь"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#532","level":2,"title":"5.3.2   Реализация двусторонней очереди *","text":"

        Реализация двусторонней очереди похожа на реализацию обычной очереди: в качестве базовой структуры данных можно выбрать связный список или массив.

        ","path":["Глава 5. Стек и очередь","5.3   Двусторонняя очередь"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#1","level":3,"title":"1.   Реализация на основе двусвязного списка","text":"

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

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

        Как показано на рисунке 5-8, мы рассматриваем головной и хвостовой узлы двусвязного списка как голову и хвост двусторонней очереди и одновременно реализуем функции добавления и удаления узлов с обеих сторон.

        <1><2><3><4><5>

        Рисунок 5-8   Операции enqueue и dequeue для двусторонней очереди на связном списке

        Код реализации приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linkedlist_deque.py
        class ListNode:\n    \"\"\"Узел двусвязного списка\"\"\"\n\n    def __init__(self, val: int):\n        \"\"\"Конструктор\"\"\"\n        self.val: int = val\n        self.next: ListNode | None = None  # Ссылка на узел-преемник\n        self.prev: ListNode | None = None  # Ссылка на узел-предшественник\n\nclass LinkedListDeque:\n    \"\"\"Двусторонняя очередь на основе двусвязного списка\"\"\"\n\n    def __init__(self):\n        \"\"\"Конструктор\"\"\"\n        self._front: ListNode | None = None  # Головной узел front\n        self._rear: ListNode | None = None  # Хвостовой узел rear\n        self._size: int = 0  # Длина двусторонней очереди\n\n    def size(self) -> int:\n        \"\"\"Получение длины двусторонней очереди\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"Проверка, пуста ли двусторонняя очередь\"\"\"\n        return self._size == 0\n\n    def push(self, num: int, is_front: bool):\n        \"\"\"Операция добавления в очередь\"\"\"\n        node = ListNode(num)\n        # Если связный список пуст, сделать так, чтобы и front, и rear указывали на node\n        if self.is_empty():\n            self._front = self._rear = node\n        # Операция добавления в голову очереди\n        elif is_front:\n            # Добавить node в голову списка\n            self._front.prev = node\n            node.next = self._front\n            self._front = node  # Обновить головной узел\n        # Операция добавления в хвост очереди\n        else:\n            # Добавить node в хвост списка\n            self._rear.next = node\n            node.prev = self._rear\n            self._rear = node  # Обновить хвостовой узел\n        self._size += 1  # Обновить длину очереди\n\n    def push_first(self, num: int):\n        \"\"\"Добавление в голову очереди\"\"\"\n        self.push(num, True)\n\n    def push_last(self, num: int):\n        \"\"\"Добавление в хвост очереди\"\"\"\n        self.push(num, False)\n\n    def pop(self, is_front: bool) -> int:\n        \"\"\"Операция извлечения из очереди\"\"\"\n        if self.is_empty():\n            raise IndexError(\"двусторонняя очередь пуста\")\n        # Операция извлечения из головы очереди\n        if is_front:\n            val: int = self._front.val  # Временно сохранить значение головного узла\n            # Удалить головной узел\n            fnext: ListNode | None = self._front.next\n            if fnext is not None:\n                fnext.prev = None\n                self._front.next = None\n            self._front = fnext  # Обновить головной узел\n        # Операция извлечения из хвоста очереди\n        else:\n            val: int = self._rear.val  # Временно сохранить значение хвостового узла\n            # Удалить хвостовой узел\n            rprev: ListNode | None = self._rear.prev\n            if rprev is not None:\n                rprev.next = None\n                self._rear.prev = None\n            self._rear = rprev  # Обновить хвостовой узел\n        self._size -= 1  # Обновить длину очереди\n        return val\n\n    def pop_first(self) -> int:\n        \"\"\"Извлечение из головы очереди\"\"\"\n        return self.pop(True)\n\n    def pop_last(self) -> int:\n        \"\"\"Извлечение из хвоста очереди\"\"\"\n        return self.pop(False)\n\n    def peek_first(self) -> int:\n        \"\"\"Доступ к элементу в начале очереди\"\"\"\n        if self.is_empty():\n            raise IndexError(\"двусторонняя очередь пуста\")\n        return self._front.val\n\n    def peek_last(self) -> int:\n        \"\"\"Доступ к элементу в конце очереди\"\"\"\n        if self.is_empty():\n            raise IndexError(\"двусторонняя очередь пуста\")\n        return self._rear.val\n\n    def to_array(self) -> list[int]:\n        \"\"\"Вернуть массив для вывода\"\"\"\n        node = self._front\n        res = [0] * self.size()\n        for i in range(self.size()):\n            res[i] = node.val\n            node = node.next\n        return res\n
        linkedlist_deque.cpp
        /* Узел двусвязного списка */\nstruct DoublyListNode {\n    int val;              // Значение узла\n    DoublyListNode *next; // Указатель на узел-преемник\n    DoublyListNode *prev; // Указатель на узел-предшественник\n    DoublyListNode(int val) : val(val), prev(nullptr), next(nullptr) {\n    }\n};\n\n/* Двусторонняя очередь на основе двусвязного списка */\nclass LinkedListDeque {\n  private:\n    DoublyListNode *front, *rear; // Головной узел front, хвостовой узел rear\n    int queSize = 0;              // Длина двусторонней очереди\n\n  public:\n    /* Конструктор */\n    LinkedListDeque() : front(nullptr), rear(nullptr) {\n    }\n\n    /* Метод-деструктор */\n    ~LinkedListDeque() {\n        // Обходить связный список, удалять узлы и освобождать память\n        DoublyListNode *pre, *cur = front;\n        while (cur != nullptr) {\n            pre = cur;\n            cur = cur->next;\n            delete pre;\n        }\n    }\n\n    /* Получение длины двусторонней очереди */\n    int size() {\n        return queSize;\n    }\n\n    /* Проверка, пуста ли двусторонняя очередь */\n    bool isEmpty() {\n        return size() == 0;\n    }\n\n    /* Операция добавления в очередь */\n    void push(int num, bool isFront) {\n        DoublyListNode *node = new DoublyListNode(num);\n        // Если связный список пуст, сделать так, чтобы и front, и rear указывали на node\n        if (isEmpty())\n            front = rear = node;\n        // Операция добавления в голову очереди\n        else if (isFront) {\n            // Добавить node в голову списка\n            front->prev = node;\n            node->next = front;\n            front = node; // Обновить головной узел\n        // Операция добавления в хвост очереди\n        } else {\n            // Добавить node в хвост списка\n            rear->next = node;\n            node->prev = rear;\n            rear = node; // Обновить хвостовой узел\n        }\n        queSize++; // Обновить длину очереди\n    }\n\n    /* Добавление в голову очереди */\n    void pushFirst(int num) {\n        push(num, true);\n    }\n\n    /* Добавление в хвост очереди */\n    void pushLast(int num) {\n        push(num, false);\n    }\n\n    /* Операция извлечения из очереди */\n    int pop(bool isFront) {\n        if (isEmpty())\n            throw out_of_range(\"очередь пуста\");\n        int val;\n        // Операция извлечения из головы очереди\n        if (isFront) {\n            val = front->val; // Временно сохранить значение головного узла\n            // Удалить головной узел\n            DoublyListNode *fNext = front->next;\n            if (fNext != nullptr) {\n                fNext->prev = nullptr;\n                front->next = nullptr;\n            }\n            delete front;\n            front = fNext; // Обновить головной узел\n        // Операция извлечения из хвоста очереди\n        } else {\n            val = rear->val; // Временно сохранить значение хвостового узла\n            // Удалить хвостовой узел\n            DoublyListNode *rPrev = rear->prev;\n            if (rPrev != nullptr) {\n                rPrev->next = nullptr;\n                rear->prev = nullptr;\n            }\n            delete rear;\n            rear = rPrev; // Обновить хвостовой узел\n        }\n        queSize--; // Обновить длину очереди\n        return val;\n    }\n\n    /* Извлечение из головы очереди */\n    int popFirst() {\n        return pop(true);\n    }\n\n    /* Извлечение из хвоста очереди */\n    int popLast() {\n        return pop(false);\n    }\n\n    /* Доступ к элементу в начале очереди */\n    int peekFirst() {\n        if (isEmpty())\n            throw out_of_range(\"двусторонняя очередь пуста\");\n        return front->val;\n    }\n\n    /* Доступ к элементу в конце очереди */\n    int peekLast() {\n        if (isEmpty())\n            throw out_of_range(\"двусторонняя очередь пуста\");\n        return rear->val;\n    }\n\n    /* Вернуть массив для вывода */\n    vector<int> toVector() {\n        DoublyListNode *node = front;\n        vector<int> res(size());\n        for (int i = 0; i < res.size(); i++) {\n            res[i] = node->val;\n            node = node->next;\n        }\n        return res;\n    }\n};\n
        linkedlist_deque.java
        /* Узел двусвязного списка */\nclass ListNode {\n    int val; // Значение узла\n    ListNode next; // Ссылка на узел-преемник\n    ListNode prev; // Ссылка на узел-предшественник\n\n    ListNode(int val) {\n        this.val = val;\n        prev = next = null;\n    }\n}\n\n/* Двусторонняя очередь на основе двусвязного списка */\nclass LinkedListDeque {\n    private ListNode front, rear; // Головной узел front, хвостовой узел rear\n    private int queSize = 0; // Длина двусторонней очереди\n\n    public LinkedListDeque() {\n        front = rear = null;\n    }\n\n    /* Получение длины двусторонней очереди */\n    public int size() {\n        return queSize;\n    }\n\n    /* Проверка, пуста ли двусторонняя очередь */\n    public boolean isEmpty() {\n        return size() == 0;\n    }\n\n    /* Операция добавления в очередь */\n    private void push(int num, boolean isFront) {\n        ListNode node = new ListNode(num);\n        // Если связный список пуст, сделать так, чтобы и front, и rear указывали на node\n        if (isEmpty())\n            front = rear = node;\n        // Операция добавления в голову очереди\n        else if (isFront) {\n            // Добавить node в голову списка\n            front.prev = node;\n            node.next = front;\n            front = node; // Обновить головной узел\n        // Операция добавления в хвост очереди\n        } else {\n            // Добавить node в хвост списка\n            rear.next = node;\n            node.prev = rear;\n            rear = node; // Обновить хвостовой узел\n        }\n        queSize++; // Обновить длину очереди\n    }\n\n    /* Добавление в голову очереди */\n    public void pushFirst(int num) {\n        push(num, true);\n    }\n\n    /* Добавление в хвост очереди */\n    public void pushLast(int num) {\n        push(num, false);\n    }\n\n    /* Операция извлечения из очереди */\n    private int pop(boolean isFront) {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        int val;\n        // Операция извлечения из головы очереди\n        if (isFront) {\n            val = front.val; // Временно сохранить значение головного узла\n            // Удалить головной узел\n            ListNode fNext = front.next;\n            if (fNext != null) {\n                fNext.prev = null;\n                front.next = null;\n            }\n            front = fNext; // Обновить головной узел\n        // Операция извлечения из хвоста очереди\n        } else {\n            val = rear.val; // Временно сохранить значение хвостового узла\n            // Удалить хвостовой узел\n            ListNode rPrev = rear.prev;\n            if (rPrev != null) {\n                rPrev.next = null;\n                rear.prev = null;\n            }\n            rear = rPrev; // Обновить хвостовой узел\n        }\n        queSize--; // Обновить длину очереди\n        return val;\n    }\n\n    /* Извлечение из головы очереди */\n    public int popFirst() {\n        return pop(true);\n    }\n\n    /* Извлечение из хвоста очереди */\n    public int popLast() {\n        return pop(false);\n    }\n\n    /* Доступ к элементу в начале очереди */\n    public int peekFirst() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return front.val;\n    }\n\n    /* Доступ к элементу в конце очереди */\n    public int peekLast() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return rear.val;\n    }\n\n    /* Вернуть массив для вывода */\n    public int[] toArray() {\n        ListNode node = front;\n        int[] res = new int[size()];\n        for (int i = 0; i < res.length; i++) {\n            res[i] = node.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
        linkedlist_deque.cs
        /* Узел двусвязного списка */\nclass ListNode(int val) {\n    public int val = val;       // Значение узла\n    public ListNode? next = null; // Ссылка на узел-преемник\n    public ListNode? prev = null; // Ссылка на узел-предшественник\n}\n\n/* Двусторонняя очередь на основе двусвязного списка */\nclass LinkedListDeque {\n    ListNode? front, rear; // Головной узел front, хвостовой узел rear\n    int queSize = 0;      // Длина двусторонней очереди\n\n    public LinkedListDeque() {\n        front = null;\n        rear = null;\n    }\n\n    /* Получение длины двусторонней очереди */\n    public int Size() {\n        return queSize;\n    }\n\n    /* Проверка, пуста ли двусторонняя очередь */\n    public bool IsEmpty() {\n        return Size() == 0;\n    }\n\n    /* Операция добавления в очередь */\n    void Push(int num, bool isFront) {\n        ListNode node = new(num);\n        // Если связный список пуст, сделать так, чтобы и front, и rear указывали на node\n        if (IsEmpty()) {\n            front = node;\n            rear = node;\n        }\n        // Операция добавления в голову очереди\n        else if (isFront) {\n            // Добавить node в голову списка\n            front!.prev = node;\n            node.next = front;\n            front = node; // Обновить головной узел\n        }\n        // Операция добавления в хвост очереди\n        else {\n            // Добавить node в хвост списка\n            rear!.next = node;\n            node.prev = rear;\n            rear = node;  // Обновить хвостовой узел\n        }\n\n        queSize++; // Обновить длину очереди\n    }\n\n    /* Добавление в голову очереди */\n    public void PushFirst(int num) {\n        Push(num, true);\n    }\n\n    /* Добавление в хвост очереди */\n    public void PushLast(int num) {\n        Push(num, false);\n    }\n\n    /* Операция извлечения из очереди */\n    int? Pop(bool isFront) {\n        if (IsEmpty())\n            throw new Exception();\n        int? val;\n        // Операция извлечения из головы очереди\n        if (isFront) {\n            val = front?.val; // Временно сохранить значение головного узла\n            // Удалить головной узел\n            ListNode? fNext = front?.next;\n            if (fNext != null) {\n                fNext.prev = null;\n                front!.next = null;\n            }\n            front = fNext;   // Обновить головной узел\n        }\n        // Операция извлечения из хвоста очереди\n        else {\n            val = rear?.val;  // Временно сохранить значение хвостового узла\n            // Удалить хвостовой узел\n            ListNode? rPrev = rear?.prev;\n            if (rPrev != null) {\n                rPrev.next = null;\n                rear!.prev = null;\n            }\n            rear = rPrev;    // Обновить хвостовой узел\n        }\n\n        queSize--; // Обновить длину очереди\n        return val;\n    }\n\n    /* Извлечение из головы очереди */\n    public int? PopFirst() {\n        return Pop(true);\n    }\n\n    /* Извлечение из хвоста очереди */\n    public int? PopLast() {\n        return Pop(false);\n    }\n\n    /* Доступ к элементу в начале очереди */\n    public int? PeekFirst() {\n        if (IsEmpty())\n            throw new Exception();\n        return front?.val;\n    }\n\n    /* Доступ к элементу в конце очереди */\n    public int? PeekLast() {\n        if (IsEmpty())\n            throw new Exception();\n        return rear?.val;\n    }\n\n    /* Вернуть массив для вывода */\n    public int?[] ToArray() {\n        ListNode? node = front;\n        int?[] res = new int?[Size()];\n        for (int i = 0; i < res.Length; i++) {\n            res[i] = node?.val;\n            node = node?.next;\n        }\n\n        return res;\n    }\n}\n
        linkedlist_deque.go
        /* Двусторонняя очередь на основе двусвязного списка */\ntype linkedListDeque struct {\n    // Использовать встроенный пакет list\n    data *list.List\n}\n\n/* Инициализировать двустороннюю очередь */\nfunc newLinkedListDeque() *linkedListDeque {\n    return &linkedListDeque{\n        data: list.New(),\n    }\n}\n\n/* Поместить элемент в голову очереди */\nfunc (s *linkedListDeque) pushFirst(value any) {\n    s.data.PushFront(value)\n}\n\n/* Поместить элемент в хвост очереди */\nfunc (s *linkedListDeque) pushLast(value any) {\n    s.data.PushBack(value)\n}\n\n/* Извлечь элемент из головы очереди */\nfunc (s *linkedListDeque) popFirst() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Front()\n    s.data.Remove(e)\n    return e.Value\n}\n\n/* Извлечь элемент из хвоста очереди */\nfunc (s *linkedListDeque) popLast() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Back()\n    s.data.Remove(e)\n    return e.Value\n}\n\n/* Доступ к элементу в начале очереди */\nfunc (s *linkedListDeque) peekFirst() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Front()\n    return e.Value\n}\n\n/* Доступ к элементу в конце очереди */\nfunc (s *linkedListDeque) peekLast() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Back()\n    return e.Value\n}\n\n/* Получение длины очереди */\nfunc (s *linkedListDeque) size() int {\n    return s.data.Len()\n}\n\n/* Проверка, пуста ли очередь */\nfunc (s *linkedListDeque) isEmpty() bool {\n    return s.data.Len() == 0\n}\n\n/* Получить List для вывода */\nfunc (s *linkedListDeque) toList() *list.List {\n    return s.data\n}\n
        linkedlist_deque.swift
        /* Узел двусвязного списка */\nclass ListNode {\n    var val: Int // Значение узла\n    var next: ListNode? // Ссылка на узел-преемник\n    weak var prev: ListNode? // Ссылка на узел-предшественник\n\n    init(val: Int) {\n        self.val = val\n    }\n}\n\n/* Двусторонняя очередь на основе двусвязного списка */\nclass LinkedListDeque {\n    private var front: ListNode? // Головной узел front\n    private var rear: ListNode? // Хвостовой узел rear\n    private var _size: Int // Длина двусторонней очереди\n\n    init() {\n        _size = 0\n    }\n\n    /* Получение длины двусторонней очереди */\n    func size() -> Int {\n        _size\n    }\n\n    /* Проверка, пуста ли двусторонняя очередь */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* Операция добавления в очередь */\n    private func push(num: Int, isFront: Bool) {\n        let node = ListNode(val: num)\n        // Если связный список пуст, сделать так, чтобы и front, и rear указывали на node\n        if isEmpty() {\n            front = node\n            rear = node\n        }\n        // Операция добавления в голову очереди\n        else if isFront {\n            // Добавить node в голову списка\n            front?.prev = node\n            node.next = front\n            front = node // Обновить головной узел\n        }\n        // Операция добавления в хвост очереди\n        else {\n            // Добавить node в хвост списка\n            rear?.next = node\n            node.prev = rear\n            rear = node // Обновить хвостовой узел\n        }\n        _size += 1 // Обновить длину очереди\n    }\n\n    /* Добавление в голову очереди */\n    func pushFirst(num: Int) {\n        push(num: num, isFront: true)\n    }\n\n    /* Добавление в хвост очереди */\n    func pushLast(num: Int) {\n        push(num: num, isFront: false)\n    }\n\n    /* Операция извлечения из очереди */\n    private func pop(isFront: Bool) -> Int {\n        if isEmpty() {\n            fatalError(\"двусторонняя очередь пуста\")\n        }\n        let val: Int\n        // Операция извлечения из головы очереди\n        if isFront {\n            val = front!.val // Временно сохранить значение головного узла\n            // Удалить головной узел\n            let fNext = front?.next\n            if fNext != nil {\n                fNext?.prev = nil\n                front?.next = nil\n            }\n            front = fNext // Обновить головной узел\n        }\n        // Операция извлечения из хвоста очереди\n        else {\n            val = rear!.val // Временно сохранить значение хвостового узла\n            // Удалить хвостовой узел\n            let rPrev = rear?.prev\n            if rPrev != nil {\n                rPrev?.next = nil\n                rear?.prev = nil\n            }\n            rear = rPrev // Обновить хвостовой узел\n        }\n        _size -= 1 // Обновить длину очереди\n        return val\n    }\n\n    /* Извлечение из головы очереди */\n    func popFirst() -> Int {\n        pop(isFront: true)\n    }\n\n    /* Извлечение из хвоста очереди */\n    func popLast() -> Int {\n        pop(isFront: false)\n    }\n\n    /* Доступ к элементу в начале очереди */\n    func peekFirst() -> Int {\n        if isEmpty() {\n            fatalError(\"двусторонняя очередь пуста\")\n        }\n        return front!.val\n    }\n\n    /* Доступ к элементу в конце очереди */\n    func peekLast() -> Int {\n        if isEmpty() {\n            fatalError(\"двусторонняя очередь пуста\")\n        }\n        return rear!.val\n    }\n\n    /* Вернуть массив для вывода */\n    func toArray() -> [Int] {\n        var node = front\n        var res = Array(repeating: 0, count: size())\n        for i in res.indices {\n            res[i] = node!.val\n            node = node?.next\n        }\n        return res\n    }\n}\n
        linkedlist_deque.js
        /* Узел двусвязного списка */\nclass ListNode {\n    prev; // Ссылка на узел-предшественник (указатель)\n    next; // Ссылка на узел-преемник (указатель)\n    val; // Значение узла\n\n    constructor(val) {\n        this.val = val;\n        this.next = null;\n        this.prev = null;\n    }\n}\n\n/* Двусторонняя очередь на основе двусвязного списка */\nclass LinkedListDeque {\n    #front; // Головной узел front\n    #rear; // Хвостовой узел rear\n    #queSize; // Длина двусторонней очереди\n\n    constructor() {\n        this.#front = null;\n        this.#rear = null;\n        this.#queSize = 0;\n    }\n\n    /* Операция добавления в хвост очереди */\n    pushLast(val) {\n        const node = new ListNode(val);\n        // Если связный список пуст, сделать так, чтобы и front, и rear указывали на node\n        if (this.#queSize === 0) {\n            this.#front = node;\n            this.#rear = node;\n        } else {\n            // Добавить node в хвост списка\n            this.#rear.next = node;\n            node.prev = this.#rear;\n            this.#rear = node; // Обновить хвостовой узел\n        }\n        this.#queSize++;\n    }\n\n    /* Операция добавления в голову очереди */\n    pushFirst(val) {\n        const node = new ListNode(val);\n        // Если связный список пуст, сделать так, чтобы и front, и rear указывали на node\n        if (this.#queSize === 0) {\n            this.#front = node;\n            this.#rear = node;\n        } else {\n            // Добавить node в голову списка\n            this.#front.prev = node;\n            node.next = this.#front;\n            this.#front = node; // Обновить головной узел\n        }\n        this.#queSize++;\n    }\n\n    /* Операция извлечения из хвоста очереди */\n    popLast() {\n        if (this.#queSize === 0) {\n            return null;\n        }\n        const value = this.#rear.val; // Сохранить значение хвостового узла\n        // Удалить хвостовой узел\n        let temp = this.#rear.prev;\n        if (temp !== null) {\n            temp.next = null;\n            this.#rear.prev = null;\n        }\n        this.#rear = temp; // Обновить хвостовой узел\n        this.#queSize--;\n        return value;\n    }\n\n    /* Операция извлечения из головы очереди */\n    popFirst() {\n        if (this.#queSize === 0) {\n            return null;\n        }\n        const value = this.#front.val; // Сохранить значение хвостового узла\n        // Удалить головной узел\n        let temp = this.#front.next;\n        if (temp !== null) {\n            temp.prev = null;\n            this.#front.next = null;\n        }\n        this.#front = temp; // Обновить головной узел\n        this.#queSize--;\n        return value;\n    }\n\n    /* Доступ к элементу в конце очереди */\n    peekLast() {\n        return this.#queSize === 0 ? null : this.#rear.val;\n    }\n\n    /* Доступ к элементу в начале очереди */\n    peekFirst() {\n        return this.#queSize === 0 ? null : this.#front.val;\n    }\n\n    /* Получение длины двусторонней очереди */\n    size() {\n        return this.#queSize;\n    }\n\n    /* Проверка, пуста ли двусторонняя очередь */\n    isEmpty() {\n        return this.#queSize === 0;\n    }\n\n    /* Вывести двустороннюю очередь */\n    print() {\n        const arr = [];\n        let temp = this.#front;\n        while (temp !== null) {\n            arr.push(temp.val);\n            temp = temp.next;\n        }\n        console.log('[' + arr.join(', ') + ']');\n    }\n}\n
        linkedlist_deque.ts
        /* Узел двусвязного списка */\nclass ListNode {\n    prev: ListNode; // Ссылка на узел-предшественник (указатель)\n    next: ListNode; // Ссылка на узел-преемник (указатель)\n    val: number; // Значение узла\n\n    constructor(val: number) {\n        this.val = val;\n        this.next = null;\n        this.prev = null;\n    }\n}\n\n/* Двусторонняя очередь на основе двусвязного списка */\nclass LinkedListDeque {\n    private front: ListNode; // Головной узел front\n    private rear: ListNode; // Хвостовой узел rear\n    private queSize: number; // Длина двусторонней очереди\n\n    constructor() {\n        this.front = null;\n        this.rear = null;\n        this.queSize = 0;\n    }\n\n    /* Операция добавления в хвост очереди */\n    pushLast(val: number): void {\n        const node: ListNode = new ListNode(val);\n        // Если связный список пуст, сделать так, чтобы и front, и rear указывали на node\n        if (this.queSize === 0) {\n            this.front = node;\n            this.rear = node;\n        } else {\n            // Добавить node в хвост списка\n            this.rear.next = node;\n            node.prev = this.rear;\n            this.rear = node; // Обновить хвостовой узел\n        }\n        this.queSize++;\n    }\n\n    /* Операция добавления в голову очереди */\n    pushFirst(val: number): void {\n        const node: ListNode = new ListNode(val);\n        // Если связный список пуст, сделать так, чтобы и front, и rear указывали на node\n        if (this.queSize === 0) {\n            this.front = node;\n            this.rear = node;\n        } else {\n            // Добавить node в голову списка\n            this.front.prev = node;\n            node.next = this.front;\n            this.front = node; // Обновить головной узел\n        }\n        this.queSize++;\n    }\n\n    /* Операция извлечения из хвоста очереди */\n    popLast(): number {\n        if (this.queSize === 0) {\n            return null;\n        }\n        const value: number = this.rear.val; // Сохранить значение хвостового узла\n        // Удалить хвостовой узел\n        let temp: ListNode = this.rear.prev;\n        if (temp !== null) {\n            temp.next = null;\n            this.rear.prev = null;\n        }\n        this.rear = temp; // Обновить хвостовой узел\n        this.queSize--;\n        return value;\n    }\n\n    /* Операция извлечения из головы очереди */\n    popFirst(): number {\n        if (this.queSize === 0) {\n            return null;\n        }\n        const value: number = this.front.val; // Сохранить значение хвостового узла\n        // Удалить головной узел\n        let temp: ListNode = this.front.next;\n        if (temp !== null) {\n            temp.prev = null;\n            this.front.next = null;\n        }\n        this.front = temp; // Обновить головной узел\n        this.queSize--;\n        return value;\n    }\n\n    /* Доступ к элементу в конце очереди */\n    peekLast(): number {\n        return this.queSize === 0 ? null : this.rear.val;\n    }\n\n    /* Доступ к элементу в начале очереди */\n    peekFirst(): number {\n        return this.queSize === 0 ? null : this.front.val;\n    }\n\n    /* Получение длины двусторонней очереди */\n    size(): number {\n        return this.queSize;\n    }\n\n    /* Проверка, пуста ли двусторонняя очередь */\n    isEmpty(): boolean {\n        return this.queSize === 0;\n    }\n\n    /* Вывести двустороннюю очередь */\n    print(): void {\n        const arr: number[] = [];\n        let temp: ListNode = this.front;\n        while (temp !== null) {\n            arr.push(temp.val);\n            temp = temp.next;\n        }\n        console.log('[' + arr.join(', ') + ']');\n    }\n}\n
        linkedlist_deque.dart
        /* Узел двусвязного списка */\nclass ListNode {\n  int val; // Значение узла\n  ListNode? next; // Ссылка на узел-преемник\n  ListNode? prev; // Ссылка на узел-предшественник\n\n  ListNode(this.val, {this.next, this.prev});\n}\n\n/* Двусторонняя очередь на основе двусвязного списка */\nclass LinkedListDeque {\n  late ListNode? _front; // Головной узел _front\n  late ListNode? _rear; // Хвостовой узел _rear\n  int _queSize = 0; // Длина двусторонней очереди\n\n  LinkedListDeque() {\n    this._front = null;\n    this._rear = null;\n  }\n\n  /* Получить длину двусторонней очереди */\n  int size() {\n    return this._queSize;\n  }\n\n  /* Проверка, пуста ли двусторонняя очередь */\n  bool isEmpty() {\n    return size() == 0;\n  }\n\n  /* Операция добавления в очередь */\n  void push(int _num, bool isFront) {\n    final ListNode node = ListNode(_num);\n    if (isEmpty()) {\n      // Если связный список пуст, пусть _front и _rear оба указывают на node\n      _front = _rear = node;\n    } else if (isFront) {\n      // Операция добавления в голову очереди\n      // Добавить node в начало связного списка\n      _front!.prev = node;\n      node.next = _front;\n      _front = node; // Обновить головной узел\n    } else {\n      // Операция добавления в хвост очереди\n      // Добавить node в конец связного списка\n      _rear!.next = node;\n      node.prev = _rear;\n      _rear = node; // Обновить хвостовой узел\n    }\n    _queSize++; // Обновить длину очереди\n  }\n\n  /* Добавление в голову очереди */\n  void pushFirst(int _num) {\n    push(_num, true);\n  }\n\n  /* Добавление в хвост очереди */\n  void pushLast(int _num) {\n    push(_num, false);\n  }\n\n  /* Операция извлечения из очереди */\n  int? pop(bool isFront) {\n    // Если очередь пуста, сразу вернуть null\n    if (isEmpty()) {\n      return null;\n    }\n    final int val;\n    if (isFront) {\n      // Операция извлечения из головы очереди\n      val = _front!.val; // Временно сохранить значение головного узла\n      // Удалить головной узел\n      ListNode? fNext = _front!.next;\n      if (fNext != null) {\n        fNext.prev = null;\n        _front!.next = null;\n      }\n      _front = fNext; // Обновить головной узел\n    } else {\n      // Операция извлечения из хвоста очереди\n      val = _rear!.val; // Временно сохранить значение хвостового узла\n      // Удалить хвостовой узел\n      ListNode? rPrev = _rear!.prev;\n      if (rPrev != null) {\n        rPrev.next = null;\n        _rear!.prev = null;\n      }\n      _rear = rPrev; // Обновить хвостовой узел\n    }\n    _queSize--; // Обновить длину очереди\n    return val;\n  }\n\n  /* Извлечение из головы очереди */\n  int? popFirst() {\n    return pop(true);\n  }\n\n  /* Извлечение из хвоста очереди */\n  int? popLast() {\n    return pop(false);\n  }\n\n  /* Доступ к элементу в начале очереди */\n  int? peekFirst() {\n    return _front?.val;\n  }\n\n  /* Доступ к элементу в конце очереди */\n  int? peekLast() {\n    return _rear?.val;\n  }\n\n  /* Вернуть массив для вывода */\n  List<int> toArray() {\n    ListNode? node = _front;\n    final List<int> res = [];\n    for (int i = 0; i < _queSize; i++) {\n      res.add(node!.val);\n      node = node.next;\n    }\n    return res;\n  }\n}\n
        linkedlist_deque.rs
        /* Узел двусвязного списка */\npub struct ListNode<T> {\n    pub val: T,                                 // Значение узла\n    pub next: Option<Rc<RefCell<ListNode<T>>>>, // Указатель на узел-преемник\n    pub prev: Option<Rc<RefCell<ListNode<T>>>>, // Указатель на узел-предшественник\n}\n\nimpl<T> ListNode<T> {\n    pub fn new(val: T) -> Rc<RefCell<ListNode<T>>> {\n        Rc::new(RefCell::new(ListNode {\n            val,\n            next: None,\n            prev: None,\n        }))\n    }\n}\n\n/* Двусторонняя очередь на основе двусвязного списка */\n#[allow(dead_code)]\npub struct LinkedListDeque<T> {\n    front: Option<Rc<RefCell<ListNode<T>>>>, // Головной узел front\n    rear: Option<Rc<RefCell<ListNode<T>>>>,  // Хвостовой узел rear\n    que_size: usize,                         // Длина двусторонней очереди\n}\n\nimpl<T: Copy> LinkedListDeque<T> {\n    pub fn new() -> Self {\n        Self {\n            front: None,\n            rear: None,\n            que_size: 0,\n        }\n    }\n\n    /* Получение длины двусторонней очереди */\n    pub fn size(&self) -> usize {\n        return self.que_size;\n    }\n\n    /* Проверка, пуста ли двусторонняя очередь */\n    pub fn is_empty(&self) -> bool {\n        return self.que_size == 0;\n    }\n\n    /* Операция добавления в очередь */\n    fn push(&mut self, num: T, is_front: bool) {\n        let node = ListNode::new(num);\n        // Операция добавления в голову очереди\n        if is_front {\n            match self.front.take() {\n                // Если связный список пуст, сделать так, чтобы и front, и rear указывали на node\n                None => {\n                    self.rear = Some(node.clone());\n                    self.front = Some(node);\n                }\n                // Добавить node в голову списка\n                Some(old_front) => {\n                    old_front.borrow_mut().prev = Some(node.clone());\n                    node.borrow_mut().next = Some(old_front);\n                    self.front = Some(node); // Обновить головной узел\n                }\n            }\n        }\n        // Операция добавления в хвост очереди\n        else {\n            match self.rear.take() {\n                // Если связный список пуст, сделать так, чтобы и front, и rear указывали на node\n                None => {\n                    self.front = Some(node.clone());\n                    self.rear = Some(node);\n                }\n                // Добавить node в хвост списка\n                Some(old_rear) => {\n                    old_rear.borrow_mut().next = Some(node.clone());\n                    node.borrow_mut().prev = Some(old_rear);\n                    self.rear = Some(node); // Обновить хвостовой узел\n                }\n            }\n        }\n        self.que_size += 1; // Обновить длину очереди\n    }\n\n    /* Добавление в голову очереди */\n    pub fn push_first(&mut self, num: T) {\n        self.push(num, true);\n    }\n\n    /* Добавление в хвост очереди */\n    pub fn push_last(&mut self, num: T) {\n        self.push(num, false);\n    }\n\n    /* Операция извлечения из очереди */\n    fn pop(&mut self, is_front: bool) -> Option<T> {\n        // Если очередь пуста, сразу вернуть None\n        if self.is_empty() {\n            return None;\n        };\n        // Операция извлечения из головы очереди\n        if is_front {\n            self.front.take().map(|old_front| {\n                match old_front.borrow_mut().next.take() {\n                    Some(new_front) => {\n                        new_front.borrow_mut().prev.take();\n                        self.front = Some(new_front); // Обновить головной узел\n                    }\n                    None => {\n                        self.rear.take();\n                    }\n                }\n                self.que_size -= 1; // Обновить длину очереди\n                old_front.borrow().val\n            })\n        }\n        // Операция извлечения из хвоста очереди\n        else {\n            self.rear.take().map(|old_rear| {\n                match old_rear.borrow_mut().prev.take() {\n                    Some(new_rear) => {\n                        new_rear.borrow_mut().next.take();\n                        self.rear = Some(new_rear); // Обновить хвостовой узел\n                    }\n                    None => {\n                        self.front.take();\n                    }\n                }\n                self.que_size -= 1; // Обновить длину очереди\n                old_rear.borrow().val\n            })\n        }\n    }\n\n    /* Извлечение из головы очереди */\n    pub fn pop_first(&mut self) -> Option<T> {\n        return self.pop(true);\n    }\n\n    /* Извлечение из хвоста очереди */\n    pub fn pop_last(&mut self) -> Option<T> {\n        return self.pop(false);\n    }\n\n    /* Доступ к элементу в начале очереди */\n    pub fn peek_first(&self) -> Option<&Rc<RefCell<ListNode<T>>>> {\n        self.front.as_ref()\n    }\n\n    /* Доступ к элементу в конце очереди */\n    pub fn peek_last(&self) -> Option<&Rc<RefCell<ListNode<T>>>> {\n        self.rear.as_ref()\n    }\n\n    /* Вернуть массив для вывода */\n    pub fn to_array(&self, head: Option<&Rc<RefCell<ListNode<T>>>>) -> Vec<T> {\n        let mut res: Vec<T> = Vec::new();\n        fn recur<T: Copy>(cur: Option<&Rc<RefCell<ListNode<T>>>>, res: &mut Vec<T>) {\n            if let Some(cur) = cur {\n                res.push(cur.borrow().val);\n                recur(cur.borrow().next.as_ref(), res);\n            }\n        }\n\n        recur(head, &mut res);\n        res\n    }\n}\n
        linkedlist_deque.c
        /* Узел двусвязного списка */\ntypedef struct DoublyListNode {\n    int val;                     // Значение узла\n    struct DoublyListNode *next; // Узел-преемник\n    struct DoublyListNode *prev; // Узел-предшественник\n} DoublyListNode;\n\n/* Конструктор */\nDoublyListNode *newDoublyListNode(int num) {\n    DoublyListNode *new = (DoublyListNode *)malloc(sizeof(DoublyListNode));\n    new->val = num;\n    new->next = NULL;\n    new->prev = NULL;\n    return new;\n}\n\n/* Деструктор */\nvoid delDoublyListNode(DoublyListNode *node) {\n    free(node);\n}\n\n/* Двусторонняя очередь на основе двусвязного списка */\ntypedef struct {\n    DoublyListNode *front, *rear; // Головной узел front, хвостовой узел rear\n    int queSize;                  // Длина двусторонней очереди\n} LinkedListDeque;\n\n/* Конструктор */\nLinkedListDeque *newLinkedListDeque() {\n    LinkedListDeque *deque = (LinkedListDeque *)malloc(sizeof(LinkedListDeque));\n    deque->front = NULL;\n    deque->rear = NULL;\n    deque->queSize = 0;\n    return deque;\n}\n\n/* Деструктор */\nvoid delLinkedListdeque(LinkedListDeque *deque) {\n    // Освободить все узлы\n    for (int i = 0; i < deque->queSize && deque->front != NULL; i++) {\n        DoublyListNode *tmp = deque->front;\n        deque->front = deque->front->next;\n        free(tmp);\n    }\n    // Освободить структуру deque\n    free(deque);\n}\n\n/* Получение длины очереди */\nint size(LinkedListDeque *deque) {\n    return deque->queSize;\n}\n\n/* Проверка, пуста ли очередь */\nbool empty(LinkedListDeque *deque) {\n    return (size(deque) == 0);\n}\n\n/* Поместить в очередь */\nvoid push(LinkedListDeque *deque, int num, bool isFront) {\n    DoublyListNode *node = newDoublyListNode(num);\n    // Если связный список пуст, пусть front и rear оба указывают на node\n    if (empty(deque)) {\n        deque->front = deque->rear = node;\n    }\n    // Операция добавления в голову очереди\n    else if (isFront) {\n        // Добавить node в голову списка\n        deque->front->prev = node;\n        node->next = deque->front;\n        deque->front = node; // Обновить головной узел\n    }\n    // Операция добавления в хвост очереди\n    else {\n        // Добавить node в хвост списка\n        deque->rear->next = node;\n        node->prev = deque->rear;\n        deque->rear = node;\n    }\n    deque->queSize++; // Обновить длину очереди\n}\n\n/* Добавление в голову очереди */\nvoid pushFirst(LinkedListDeque *deque, int num) {\n    push(deque, num, true);\n}\n\n/* Добавление в хвост очереди */\nvoid pushLast(LinkedListDeque *deque, int num) {\n    push(deque, num, false);\n}\n\n/* Доступ к элементу в начале очереди */\nint peekFirst(LinkedListDeque *deque) {\n    assert(size(deque) && deque->front);\n    return deque->front->val;\n}\n\n/* Доступ к элементу в конце очереди */\nint peekLast(LinkedListDeque *deque) {\n    assert(size(deque) && deque->rear);\n    return deque->rear->val;\n}\n\n/* Извлечь из очереди */\nint pop(LinkedListDeque *deque, bool isFront) {\n    if (empty(deque))\n        return -1;\n    int val;\n    // Операция извлечения из головы очереди\n    if (isFront) {\n        val = peekFirst(deque); // Временно сохранить значение головного узла\n        DoublyListNode *fNext = deque->front->next;\n        if (fNext) {\n            fNext->prev = NULL;\n            deque->front->next = NULL;\n        }\n        delDoublyListNode(deque->front);\n        deque->front = fNext; // Обновить головной узел\n    }\n    // Операция извлечения из хвоста очереди\n    else {\n        val = peekLast(deque); // Временно сохранить значение хвостового узла\n        DoublyListNode *rPrev = deque->rear->prev;\n        if (rPrev) {\n            rPrev->next = NULL;\n            deque->rear->prev = NULL;\n        }\n        delDoublyListNode(deque->rear);\n        deque->rear = rPrev; // Обновить хвостовой узел\n    }\n    deque->queSize--; // Обновить длину очереди\n    return val;\n}\n\n/* Извлечение из головы очереди */\nint popFirst(LinkedListDeque *deque) {\n    return pop(deque, true);\n}\n\n/* Извлечение из хвоста очереди */\nint popLast(LinkedListDeque *deque) {\n    return pop(deque, false);\n}\n\n/* Вывести очередь */\nvoid printLinkedListDeque(LinkedListDeque *deque) {\n    int *arr = malloc(sizeof(int) * deque->queSize);\n    // Скопировать данные связного списка в массив\n    int i;\n    DoublyListNode *node;\n    for (i = 0, node = deque->front; i < deque->queSize; i++) {\n        arr[i] = node->val;\n        node = node->next;\n    }\n    printArray(arr, deque->queSize);\n    free(arr);\n}\n
        linkedlist_deque.kt
        /* Узел двусвязного списка */\nclass ListNode(var _val: Int) {\n    // Значение узла\n    var next: ListNode? = null // Ссылка на узел-преемник\n    var prev: ListNode? = null // Ссылка на узел-предшественник\n}\n\n/* Двусторонняя очередь на основе двусвязного списка */\nclass LinkedListDeque {\n    private var front: ListNode? = null // Головной узел front\n    private var rear: ListNode? = null // Хвостовой узел rear\n    private var queSize: Int = 0 // Длина двусторонней очереди\n\n    /* Получение длины двусторонней очереди */\n    fun size(): Int {\n        return queSize\n    }\n\n    /* Проверка, пуста ли двусторонняя очередь */\n    fun isEmpty(): Boolean {\n        return size() == 0\n    }\n\n    /* Операция добавления в очередь */\n    fun push(num: Int, isFront: Boolean) {\n        val node = ListNode(num)\n        // Если связный список пуст, сделать так, чтобы и front, и rear указывали на node\n        if (isEmpty()) {\n            rear = node\n            front = rear\n            // Операция добавления в голову очереди\n        } else if (isFront) {\n            // Добавить node в голову списка\n            front?.prev = node\n            node.next = front\n            front = node // Обновить головной узел\n            // Операция добавления в хвост очереди\n        } else {\n            // Добавить node в хвост списка\n            rear?.next = node\n            node.prev = rear\n            rear = node // Обновить хвостовой узел\n        }\n        queSize++ // Обновить длину очереди\n    }\n\n    /* Добавление в голову очереди */\n    fun pushFirst(num: Int) {\n        push(num, true)\n    }\n\n    /* Добавление в хвост очереди */\n    fun pushLast(num: Int) {\n        push(num, false)\n    }\n\n    /* Операция извлечения из очереди */\n    fun pop(isFront: Boolean): Int {\n        if (isEmpty()) \n            throw IndexOutOfBoundsException()\n        val _val: Int\n        // Операция извлечения из головы очереди\n        if (isFront) {\n            _val = front!!._val // Временно сохранить значение головного узла\n            // Удалить головной узел\n            val fNext = front!!.next\n            if (fNext != null) {\n                fNext.prev = null\n                front!!.next = null\n            }\n            front = fNext // Обновить головной узел\n            // Операция извлечения из хвоста очереди\n        } else {\n            _val = rear!!._val // Временно сохранить значение хвостового узла\n            // Удалить хвостовой узел\n            val rPrev = rear!!.prev\n            if (rPrev != null) {\n                rPrev.next = null\n                rear!!.prev = null\n            }\n            rear = rPrev // Обновить хвостовой узел\n        }\n        queSize-- // Обновить длину очереди\n        return _val\n    }\n\n    /* Извлечение из головы очереди */\n    fun popFirst(): Int {\n        return pop(true)\n    }\n\n    /* Извлечение из хвоста очереди */\n    fun popLast(): Int {\n        return pop(false)\n    }\n\n    /* Доступ к элементу в начале очереди */\n    fun peekFirst(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return front!!._val\n    }\n\n    /* Доступ к элементу в конце очереди */\n    fun peekLast(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return rear!!._val\n    }\n\n    /* Вернуть массив для вывода */\n    fun toArray(): IntArray {\n        var node = front\n        val res = IntArray(size())\n        for (i in res.indices) {\n            res[i] = node!!._val\n            node = node.next\n        }\n        return res\n    }\n}\n
        linkedlist_deque.rb
        =begin\nFile: linkedlist_deque.rb\nCreated Time: 2024-04-06\nAuthor: Xuan Khoa Tu Nguyen (ngxktuzkai2000@gmail.com)\n=end\n\n# ## Узел двусвязного списка\nclass ListNode\n  attr_accessor :val\n  attr_accessor :next # Ссылка на узел-преемник\n  attr_accessor :prev # Ссылка на узел-предшественник\n\n  ### Конструктор ###\n  def initialize(val)\n    @val = val\n  end\nend\n\n### Двусторонняя очередь на основе двусвязного списка ###\nclass LinkedListDeque\n  ### Получение длины двусторонней очереди ###\n  attr_reader :size\n\n  ### Конструктор ###\n  def initialize\n    @front = nil  # Головной узел front\n    @rear = nil   # Хвостовой узел rear\n    @size = 0     # Длина двусторонней очереди\n  end\n\n  ### Проверка, пуста ли двусторонняя очередь ###\n  def is_empty?\n    size.zero?\n  end\n\n  ### Операция добавления в очередь ###\n  def push(num, is_front)\n    node = ListNode.new(num)\n    # Если связный список пуст, пусть front и rear оба указывают на node\n    if is_empty?\n      @front = @rear = node\n    # Операция добавления в голову очереди\n    elsif is_front\n      # Добавить node в голову списка\n      @front.prev = node\n      node.next = @front\n      @front = node # Обновить головной узел\n    # Операция добавления в хвост очереди\n    else\n      # Добавить node в хвост списка\n      @rear.next = node\n      node.prev = @rear\n      @rear = node # Обновить хвостовой узел\n    end\n    @size += 1 # Обновить длину очереди\n  end\n\n  ### Добавление в голову очереди ###\n  def push_first(num)\n    push(num, true)\n  end\n\n  ### Добавление в хвост очереди ###\n  def push_last(num)\n    push(num, false)\n  end\n\n  ### Операция извлечения из очереди ###\n  def pop(is_front)\n    raise IndexError, 'двусторонняя очередь пуста' if is_empty?\n\n    # Операция извлечения из головы очереди\n    if is_front\n      val = @front.val # Временно сохранить значение головного узла\n      # Удалить головной узел\n      fnext = @front.next\n      unless fnext.nil?\n        fnext.prev = nil\n        @front.next = nil\n      end\n      @front = fnext # Обновить головной узел\n    # Операция извлечения из хвоста очереди\n    else\n      val = @rear.val # Временно сохранить значение хвостового узла\n      # Удалить хвостовой узел\n      rprev = @rear.prev\n      unless rprev.nil?\n        rprev.next = nil\n        @rear.prev = nil\n      end\n      @rear = rprev # Обновить хвостовой узел\n    end\n    @size -= 1 # Обновить длину очереди\n\n    val\n  end\n\n  ### Извлечение из головы очереди ###\n  def pop_first\n    pop(true)\n  end\n\n  ### Извлечение из головы очереди ###\n  def pop_last\n    pop(false)\n  end\n\n  ### Доступ к элементу в начале очереди ###\n  def peek_first\n    raise IndexError, 'двусторонняя очередь пуста' if is_empty?\n\n    @front.val\n  end\n\n  ### Доступ к элементу в хвосте очереди ###\n  def peek_last\n    raise IndexError, 'двусторонняя очередь пуста' if is_empty?\n\n    @rear.val\n  end\n\n  ### Вернуть массив для вывода ###\n  def to_array\n    node = @front\n    res = Array.new(size, 0)\n    for i in 0...size\n      res[i] = node.val\n      node = node.next\n    end\n    res\n  end\nend\n
        ","path":["Глава 5. Стек и очередь","5.3   Двусторонняя очередь"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#2","level":3,"title":"2.   Реализация на основе массива","text":"

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

        <1><2><3><4><5>

        Рисунок 5-9   Операции enqueue и dequeue для двусторонней очереди на массиве

        На основе реализации обычной очереди нужно лишь добавить методы добавления в голову очереди и удаления из хвоста:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_deque.py
        class ArrayDeque:\n    \"\"\"Двусторонняя очередь на основе кольцевого массива\"\"\"\n\n    def __init__(self, capacity: int):\n        \"\"\"Конструктор\"\"\"\n        self._nums: list[int] = [0] * capacity\n        self._front: int = 0\n        self._size: int = 0\n\n    def capacity(self) -> int:\n        \"\"\"Получить вместимость двусторонней очереди\"\"\"\n        return len(self._nums)\n\n    def size(self) -> int:\n        \"\"\"Получение длины двусторонней очереди\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"Проверка, пуста ли двусторонняя очередь\"\"\"\n        return self._size == 0\n\n    def index(self, i: int) -> int:\n        \"\"\"Вычислить индекс в кольцевом массиве\"\"\"\n        # С помощью операции взятия по модулю соединить начало и конец массива\n        # Когда i выходит за конец массива, он возвращается в начало\n        # Когда i выходит за начало массива, он возвращается в конец\n        return (i + self.capacity()) % self.capacity()\n\n    def push_first(self, num: int):\n        \"\"\"Добавление в голову очереди\"\"\"\n        if self._size == self.capacity():\n            print(\"Двусторонняя очередь заполнена\")\n            return\n        # Указатель головы сдвигается на одну позицию влево\n        # С помощью операции взятия по модулю front после выхода за начало массива возвращается в хвост\n        self._front = self.index(self._front - 1)\n        # Добавить num в голову очереди\n        self._nums[self._front] = num\n        self._size += 1\n\n    def push_last(self, num: int):\n        \"\"\"Добавление в хвост очереди\"\"\"\n        if self._size == self.capacity():\n            print(\"Двусторонняя очередь заполнена\")\n            return\n        # Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n        rear = self.index(self._front + self._size)\n        # Добавить num в хвост очереди\n        self._nums[rear] = num\n        self._size += 1\n\n    def pop_first(self) -> int:\n        \"\"\"Извлечение из головы очереди\"\"\"\n        num = self.peek_first()\n        # Указатель головы сдвигается на одну позицию назад\n        self._front = self.index(self._front + 1)\n        self._size -= 1\n        return num\n\n    def pop_last(self) -> int:\n        \"\"\"Извлечение из хвоста очереди\"\"\"\n        num = self.peek_last()\n        self._size -= 1\n        return num\n\n    def peek_first(self) -> int:\n        \"\"\"Доступ к элементу в начале очереди\"\"\"\n        if self.is_empty():\n            raise IndexError(\"двусторонняя очередь пуста\")\n        return self._nums[self._front]\n\n    def peek_last(self) -> int:\n        \"\"\"Доступ к элементу в конце очереди\"\"\"\n        if self.is_empty():\n            raise IndexError(\"двусторонняя очередь пуста\")\n        # Вычислить индекс хвостового элемента\n        last = self.index(self._front + self._size - 1)\n        return self._nums[last]\n\n    def to_array(self) -> list[int]:\n        \"\"\"Вернуть массив для вывода\"\"\"\n        # Преобразовывать только элементы списка в пределах фактической длины\n        res = []\n        for i in range(self._size):\n            res.append(self._nums[self.index(self._front + i)])\n        return res\n
        array_deque.cpp
        /* Двусторонняя очередь на основе кольцевого массива */\nclass ArrayDeque {\n  private:\n    vector<int> nums; // Массив для хранения элементов двусторонней очереди\n    int front;        // Указатель head, указывающий на первый элемент очереди\n    int queSize;      // Длина двусторонней очереди\n\n  public:\n    /* Конструктор */\n    ArrayDeque(int capacity) {\n        nums.resize(capacity);\n        front = queSize = 0;\n    }\n\n    /* Получить вместимость двусторонней очереди */\n    int capacity() {\n        return nums.size();\n    }\n\n    /* Получение длины двусторонней очереди */\n    int size() {\n        return queSize;\n    }\n\n    /* Проверка, пуста ли двусторонняя очередь */\n    bool isEmpty() {\n        return queSize == 0;\n    }\n\n    /* Вычислить индекс в кольцевом массиве */\n    int index(int i) {\n        // С помощью операции взятия по модулю соединить начало и конец массива\n        // Когда i выходит за конец массива, он возвращается в начало\n        // Когда i выходит за начало массива, он возвращается в конец\n        return (i + capacity()) % capacity();\n    }\n\n    /* Добавление в голову очереди */\n    void pushFirst(int num) {\n        if (queSize == capacity()) {\n            cout << \"Двусторонняя очередь заполнена\" << endl;\n            return;\n        }\n        // Указатель головы сдвигается на одну позицию влево\n        // С помощью операции взятия по модулю front после выхода за начало массива возвращается в хвост\n        front = index(front - 1);\n        // Добавить num в голову очереди\n        nums[front] = num;\n        queSize++;\n    }\n\n    /* Добавление в хвост очереди */\n    void pushLast(int num) {\n        if (queSize == capacity()) {\n            cout << \"Двусторонняя очередь заполнена\" << endl;\n            return;\n        }\n        // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n        int rear = index(front + queSize);\n        // Добавить num в хвост очереди\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* Извлечение из головы очереди */\n    int popFirst() {\n        int num = peekFirst();\n        // Указатель головы сдвигается на одну позицию назад\n        front = index(front + 1);\n        queSize--;\n        return num;\n    }\n\n    /* Извлечение из хвоста очереди */\n    int popLast() {\n        int num = peekLast();\n        queSize--;\n        return num;\n    }\n\n    /* Доступ к элементу в начале очереди */\n    int peekFirst() {\n        if (isEmpty())\n            throw out_of_range(\"двусторонняя очередь пуста\");\n        return nums[front];\n    }\n\n    /* Доступ к элементу в конце очереди */\n    int peekLast() {\n        if (isEmpty())\n            throw out_of_range(\"двусторонняя очередь пуста\");\n        // Вычислить индекс хвостового элемента\n        int last = index(front + queSize - 1);\n        return nums[last];\n    }\n\n    /* Вернуть массив для вывода */\n    vector<int> toVector() {\n        // Преобразовывать только элементы списка в пределах фактической длины\n        vector<int> res(queSize);\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            res[i] = nums[index(j)];\n        }\n        return res;\n    }\n};\n
        array_deque.java
        /* Двусторонняя очередь на основе кольцевого массива */\nclass ArrayDeque {\n    private int[] nums; // Массив для хранения элементов двусторонней очереди\n    private int front; // Указатель head, указывающий на первый элемент очереди\n    private int queSize; // Длина двусторонней очереди\n\n    /* Конструктор */\n    public ArrayDeque(int capacity) {\n        this.nums = new int[capacity];\n        front = queSize = 0;\n    }\n\n    /* Получить вместимость двусторонней очереди */\n    public int capacity() {\n        return nums.length;\n    }\n\n    /* Получение длины двусторонней очереди */\n    public int size() {\n        return queSize;\n    }\n\n    /* Проверка, пуста ли двусторонняя очередь */\n    public boolean isEmpty() {\n        return queSize == 0;\n    }\n\n    /* Вычислить индекс в кольцевом массиве */\n    private int index(int i) {\n        // С помощью операции взятия по модулю соединить начало и конец массива\n        // Когда i выходит за конец массива, он возвращается в начало\n        // Когда i выходит за начало массива, он возвращается в конец\n        return (i + capacity()) % capacity();\n    }\n\n    /* Добавление в голову очереди */\n    public void pushFirst(int num) {\n        if (queSize == capacity()) {\n            System.out.println(\"Двусторонняя очередь заполнена\");\n            return;\n        }\n        // Указатель головы сдвигается на одну позицию влево\n        // С помощью операции взятия по модулю front после выхода за начало массива возвращается в хвост\n        front = index(front - 1);\n        // Добавить num в голову очереди\n        nums[front] = num;\n        queSize++;\n    }\n\n    /* Добавление в хвост очереди */\n    public void pushLast(int num) {\n        if (queSize == capacity()) {\n            System.out.println(\"Двусторонняя очередь заполнена\");\n            return;\n        }\n        // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n        int rear = index(front + queSize);\n        // Добавить num в хвост очереди\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* Извлечение из головы очереди */\n    public int popFirst() {\n        int num = peekFirst();\n        // Указатель головы сдвигается на одну позицию назад\n        front = index(front + 1);\n        queSize--;\n        return num;\n    }\n\n    /* Извлечение из хвоста очереди */\n    public int popLast() {\n        int num = peekLast();\n        queSize--;\n        return num;\n    }\n\n    /* Доступ к элементу в начале очереди */\n    public int peekFirst() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return nums[front];\n    }\n\n    /* Доступ к элементу в конце очереди */\n    public int peekLast() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        // Вычислить индекс хвостового элемента\n        int last = index(front + queSize - 1);\n        return nums[last];\n    }\n\n    /* Вернуть массив для вывода */\n    public int[] toArray() {\n        // Преобразовывать только элементы списка в пределах фактической длины\n        int[] res = new int[queSize];\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            res[i] = nums[index(j)];\n        }\n        return res;\n    }\n}\n
        array_deque.cs
        /* Двусторонняя очередь на основе кольцевого массива */\nclass ArrayDeque {\n    int[] nums;  // Массив для хранения элементов двусторонней очереди\n    int front;   // Указатель head, указывающий на первый элемент очереди\n    int queSize; // Длина двусторонней очереди\n\n    /* Конструктор */\n    public ArrayDeque(int capacity) {\n        nums = new int[capacity];\n        front = queSize = 0;\n    }\n\n    /* Получить вместимость двусторонней очереди */\n    int Capacity() {\n        return nums.Length;\n    }\n\n    /* Получение длины двусторонней очереди */\n    public int Size() {\n        return queSize;\n    }\n\n    /* Проверка, пуста ли двусторонняя очередь */\n    public bool IsEmpty() {\n        return queSize == 0;\n    }\n\n    /* Вычислить индекс в кольцевом массиве */\n    int Index(int i) {\n        // С помощью операции взятия по модулю соединить начало и конец массива\n        // Когда i выходит за конец массива, он возвращается в начало\n        // Когда i выходит за начало массива, он возвращается в конец\n        return (i + Capacity()) % Capacity();\n    }\n\n    /* Добавление в голову очереди */\n    public void PushFirst(int num) {\n        if (queSize == Capacity()) {\n            Console.WriteLine(\"Двусторонняя очередь заполнена\");\n            return;\n        }\n        // Указатель головы сдвигается на одну позицию влево\n        // С помощью операции взятия по модулю front после выхода за начало массива возвращается в хвост\n        front = Index(front - 1);\n        // Добавить num в голову очереди\n        nums[front] = num;\n        queSize++;\n    }\n\n    /* Добавление в хвост очереди */\n    public void PushLast(int num) {\n        if (queSize == Capacity()) {\n            Console.WriteLine(\"Двусторонняя очередь заполнена\");\n            return;\n        }\n        // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n        int rear = Index(front + queSize);\n        // Добавить num в хвост очереди\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* Извлечение из головы очереди */\n    public int PopFirst() {\n        int num = PeekFirst();\n        // Указатель головы сдвигается на одну позицию назад\n        front = Index(front + 1);\n        queSize--;\n        return num;\n    }\n\n    /* Извлечение из хвоста очереди */\n    public int PopLast() {\n        int num = PeekLast();\n        queSize--;\n        return num;\n    }\n\n    /* Доступ к элементу в начале очереди */\n    public int PeekFirst() {\n        if (IsEmpty()) {\n            throw new InvalidOperationException();\n        }\n        return nums[front];\n    }\n\n    /* Доступ к элементу в конце очереди */\n    public int PeekLast() {\n        if (IsEmpty()) {\n            throw new InvalidOperationException();\n        }\n        // Вычислить индекс хвостового элемента\n        int last = Index(front + queSize - 1);\n        return nums[last];\n    }\n\n    /* Вернуть массив для вывода */\n    public int[] ToArray() {\n        // Преобразовывать только элементы списка в пределах фактической длины\n        int[] res = new int[queSize];\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            res[i] = nums[Index(j)];\n        }\n        return res;\n    }\n}\n
        array_deque.go
        /* Двусторонняя очередь на основе кольцевого массива */\ntype arrayDeque struct {\n    nums        []int // Массив для хранения элементов двусторонней очереди\n    front       int   // Указатель head, указывающий на первый элемент очереди\n    queSize     int   // Длина двусторонней очереди\n    queCapacity int   // Вместимость очереди (то есть максимальное число элементов)\n}\n\n/* Инициализация очереди */\nfunc newArrayDeque(queCapacity int) *arrayDeque {\n    return &arrayDeque{\n        nums:        make([]int, queCapacity),\n        queCapacity: queCapacity,\n        front:       0,\n        queSize:     0,\n    }\n}\n\n/* Получение длины двусторонней очереди */\nfunc (q *arrayDeque) size() int {\n    return q.queSize\n}\n\n/* Проверка, пуста ли двусторонняя очередь */\nfunc (q *arrayDeque) isEmpty() bool {\n    return q.queSize == 0\n}\n\n/* Вычислить индекс в кольцевом массиве */\nfunc (q *arrayDeque) index(i int) int {\n    // С помощью операции взятия по модулю соединить начало и конец массива\n    // Когда i выходит за конец массива, он возвращается в начало\n    // Когда i выходит за начало массива, он возвращается в конец\n    return (i + q.queCapacity) % q.queCapacity\n}\n\n/* Добавление в голову очереди */\nfunc (q *arrayDeque) pushFirst(num int) {\n    if q.queSize == q.queCapacity {\n        fmt.Println(\"Двусторонняя очередь заполнена\")\n        return\n    }\n    // Указатель головы сдвигается на одну позицию влево\n    // С помощью операции взятия по модулю front после выхода за начало массива возвращается в хвост\n    q.front = q.index(q.front - 1)\n    // Добавить num в голову очереди\n    q.nums[q.front] = num\n    q.queSize++\n}\n\n/* Добавление в хвост очереди */\nfunc (q *arrayDeque) pushLast(num int) {\n    if q.queSize == q.queCapacity {\n        fmt.Println(\"Двусторонняя очередь заполнена\")\n        return\n    }\n    // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n    rear := q.index(q.front + q.queSize)\n    // Добавить num в хвост очереди\n    q.nums[rear] = num\n    q.queSize++\n}\n\n/* Извлечение из головы очереди */\nfunc (q *arrayDeque) popFirst() any {\n    num := q.peekFirst()\n    if num == nil {\n        return nil\n    }\n    // Указатель головы сдвигается на одну позицию назад\n    q.front = q.index(q.front + 1)\n    q.queSize--\n    return num\n}\n\n/* Извлечение из хвоста очереди */\nfunc (q *arrayDeque) popLast() any {\n    num := q.peekLast()\n    if num == nil {\n        return nil\n    }\n    q.queSize--\n    return num\n}\n\n/* Доступ к элементу в начале очереди */\nfunc (q *arrayDeque) peekFirst() any {\n    if q.isEmpty() {\n        return nil\n    }\n    return q.nums[q.front]\n}\n\n/* Доступ к элементу в конце очереди */\nfunc (q *arrayDeque) peekLast() any {\n    if q.isEmpty() {\n        return nil\n    }\n    // Вычислить индекс хвостового элемента\n    last := q.index(q.front + q.queSize - 1)\n    return q.nums[last]\n}\n\n/* Получить Slice для вывода */\nfunc (q *arrayDeque) toSlice() []int {\n    // Преобразовывать только элементы списка в пределах фактической длины\n    res := make([]int, q.queSize)\n    for i, j := 0, q.front; i < q.queSize; i++ {\n        res[i] = q.nums[q.index(j)]\n        j++\n    }\n    return res\n}\n
        array_deque.swift
        /* Двусторонняя очередь на основе кольцевого массива */\nclass ArrayDeque {\n    private var nums: [Int] // Массив для хранения элементов двусторонней очереди\n    private var front: Int // Указатель head, указывающий на первый элемент очереди\n    private var _size: Int // Длина двусторонней очереди\n\n    /* Конструктор */\n    init(capacity: Int) {\n        nums = Array(repeating: 0, count: capacity)\n        front = 0\n        _size = 0\n    }\n\n    /* Получить вместимость двусторонней очереди */\n    func capacity() -> Int {\n        nums.count\n    }\n\n    /* Получение длины двусторонней очереди */\n    func size() -> Int {\n        _size\n    }\n\n    /* Проверка, пуста ли двусторонняя очередь */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* Вычислить индекс в кольцевом массиве */\n    private func index(i: Int) -> Int {\n        // С помощью операции взятия по модулю соединить начало и конец массива\n        // Когда i выходит за конец массива, он возвращается в начало\n        // Когда i выходит за начало массива, он возвращается в конец\n        (i + capacity()) % capacity()\n    }\n\n    /* Добавление в голову очереди */\n    func pushFirst(num: Int) {\n        if size() == capacity() {\n            print(\"Двусторонняя очередь заполнена\")\n            return\n        }\n        // Указатель головы сдвигается на одну позицию влево\n        // С помощью операции взятия по модулю front после выхода за начало массива возвращается в хвост\n        front = index(i: front - 1)\n        // Добавить num в голову очереди\n        nums[front] = num\n        _size += 1\n    }\n\n    /* Добавление в хвост очереди */\n    func pushLast(num: Int) {\n        if size() == capacity() {\n            print(\"Двусторонняя очередь заполнена\")\n            return\n        }\n        // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n        let rear = index(i: front + size())\n        // Добавить num в хвост очереди\n        nums[rear] = num\n        _size += 1\n    }\n\n    /* Извлечение из головы очереди */\n    func popFirst() -> Int {\n        let num = peekFirst()\n        // Указатель головы сдвигается на одну позицию назад\n        front = index(i: front + 1)\n        _size -= 1\n        return num\n    }\n\n    /* Извлечение из хвоста очереди */\n    func popLast() -> Int {\n        let num = peekLast()\n        _size -= 1\n        return num\n    }\n\n    /* Доступ к элементу в начале очереди */\n    func peekFirst() -> Int {\n        if isEmpty() {\n            fatalError(\"двусторонняя очередь пуста\")\n        }\n        return nums[front]\n    }\n\n    /* Доступ к элементу в конце очереди */\n    func peekLast() -> Int {\n        if isEmpty() {\n            fatalError(\"двусторонняя очередь пуста\")\n        }\n        // Вычислить индекс хвостового элемента\n        let last = index(i: front + size() - 1)\n        return nums[last]\n    }\n\n    /* Вернуть массив для вывода */\n    func toArray() -> [Int] {\n        // Преобразовывать только элементы списка в пределах фактической длины\n        (front ..< front + size()).map { nums[index(i: $0)] }\n    }\n}\n
        array_deque.js
        /* Двусторонняя очередь на основе кольцевого массива */\nclass ArrayDeque {\n    #nums; // Массив для хранения элементов двусторонней очереди\n    #front; // Указатель head, указывающий на первый элемент очереди\n    #queSize; // Длина двусторонней очереди\n\n    /* Конструктор */\n    constructor(capacity) {\n        this.#nums = new Array(capacity);\n        this.#front = 0;\n        this.#queSize = 0;\n    }\n\n    /* Получить вместимость двусторонней очереди */\n    capacity() {\n        return this.#nums.length;\n    }\n\n    /* Получение длины двусторонней очереди */\n    size() {\n        return this.#queSize;\n    }\n\n    /* Проверка, пуста ли двусторонняя очередь */\n    isEmpty() {\n        return this.#queSize === 0;\n    }\n\n    /* Вычислить индекс в кольцевом массиве */\n    index(i) {\n        // С помощью операции взятия по модулю соединить начало и конец массива\n        // Когда i выходит за конец массива, он возвращается в начало\n        // Когда i выходит за начало массива, он возвращается в конец\n        return (i + this.capacity()) % this.capacity();\n    }\n\n    /* Добавление в голову очереди */\n    pushFirst(num) {\n        if (this.#queSize === this.capacity()) {\n            console.log('Двусторонняя очередь заполнена');\n            return;\n        }\n        // Указатель головы сдвигается на одну позицию влево\n        // С помощью операции взятия по модулю front после выхода за начало массива возвращается в хвост\n        this.#front = this.index(this.#front - 1);\n        // Добавить num в голову очереди\n        this.#nums[this.#front] = num;\n        this.#queSize++;\n    }\n\n    /* Добавление в хвост очереди */\n    pushLast(num) {\n        if (this.#queSize === this.capacity()) {\n            console.log('Двусторонняя очередь заполнена');\n            return;\n        }\n        // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n        const rear = this.index(this.#front + this.#queSize);\n        // Добавить num в хвост очереди\n        this.#nums[rear] = num;\n        this.#queSize++;\n    }\n\n    /* Извлечение из головы очереди */\n    popFirst() {\n        const num = this.peekFirst();\n        // Указатель головы сдвигается на одну позицию назад\n        this.#front = this.index(this.#front + 1);\n        this.#queSize--;\n        return num;\n    }\n\n    /* Извлечение из хвоста очереди */\n    popLast() {\n        const num = this.peekLast();\n        this.#queSize--;\n        return num;\n    }\n\n    /* Доступ к элементу в начале очереди */\n    peekFirst() {\n        if (this.isEmpty()) throw new Error('The Deque Is Empty.');\n        return this.#nums[this.#front];\n    }\n\n    /* Доступ к элементу в конце очереди */\n    peekLast() {\n        if (this.isEmpty()) throw new Error('The Deque Is Empty.');\n        // Вычислить индекс хвостового элемента\n        const last = this.index(this.#front + this.#queSize - 1);\n        return this.#nums[last];\n    }\n\n    /* Вернуть массив для вывода */\n    toArray() {\n        // Преобразовывать только элементы списка в пределах фактической длины\n        const res = [];\n        for (let i = 0, j = this.#front; i < this.#queSize; i++, j++) {\n            res[i] = this.#nums[this.index(j)];\n        }\n        return res;\n    }\n}\n
        array_deque.ts
        /* Двусторонняя очередь на основе кольцевого массива */\nclass ArrayDeque {\n    private nums: number[]; // Массив для хранения элементов двусторонней очереди\n    private front: number; // Указатель head, указывающий на первый элемент очереди\n    private queSize: number; // Длина двусторонней очереди\n\n    /* Конструктор */\n    constructor(capacity: number) {\n        this.nums = new Array(capacity);\n        this.front = 0;\n        this.queSize = 0;\n    }\n\n    /* Получить вместимость двусторонней очереди */\n    capacity(): number {\n        return this.nums.length;\n    }\n\n    /* Получение длины двусторонней очереди */\n    size(): number {\n        return this.queSize;\n    }\n\n    /* Проверка, пуста ли двусторонняя очередь */\n    isEmpty(): boolean {\n        return this.queSize === 0;\n    }\n\n    /* Вычислить индекс в кольцевом массиве */\n    index(i: number): number {\n        // С помощью операции взятия по модулю соединить начало и конец массива\n        // Когда i выходит за конец массива, он возвращается в начало\n        // Когда i выходит за начало массива, он возвращается в конец\n        return (i + this.capacity()) % this.capacity();\n    }\n\n    /* Добавление в голову очереди */\n    pushFirst(num: number): void {\n        if (this.queSize === this.capacity()) {\n            console.log('Двусторонняя очередь заполнена');\n            return;\n        }\n        // Указатель головы сдвигается на одну позицию влево\n        // С помощью операции взятия по модулю front после выхода за начало массива возвращается в хвост\n        this.front = this.index(this.front - 1);\n        // Добавить num в голову очереди\n        this.nums[this.front] = num;\n        this.queSize++;\n    }\n\n    /* Добавление в хвост очереди */\n    pushLast(num: number): void {\n        if (this.queSize === this.capacity()) {\n            console.log('Двусторонняя очередь заполнена');\n            return;\n        }\n        // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n        const rear: number = this.index(this.front + this.queSize);\n        // Добавить num в хвост очереди\n        this.nums[rear] = num;\n        this.queSize++;\n    }\n\n    /* Извлечение из головы очереди */\n    popFirst(): number {\n        const num: number = this.peekFirst();\n        // Указатель головы сдвигается на одну позицию назад\n        this.front = this.index(this.front + 1);\n        this.queSize--;\n        return num;\n    }\n\n    /* Извлечение из хвоста очереди */\n    popLast(): number {\n        const num: number = this.peekLast();\n        this.queSize--;\n        return num;\n    }\n\n    /* Доступ к элементу в начале очереди */\n    peekFirst(): number {\n        if (this.isEmpty()) throw new Error('The Deque Is Empty.');\n        return this.nums[this.front];\n    }\n\n    /* Доступ к элементу в конце очереди */\n    peekLast(): number {\n        if (this.isEmpty()) throw new Error('The Deque Is Empty.');\n        // Вычислить индекс хвостового элемента\n        const last = this.index(this.front + this.queSize - 1);\n        return this.nums[last];\n    }\n\n    /* Вернуть массив для вывода */\n    toArray(): number[] {\n        // Преобразовывать только элементы списка в пределах фактической длины\n        const res: number[] = [];\n        for (let i = 0, j = this.front; i < this.queSize; i++, j++) {\n            res[i] = this.nums[this.index(j)];\n        }\n        return res;\n    }\n}\n
        array_deque.dart
        /* Двусторонняя очередь на основе кольцевого массива */\nclass ArrayDeque {\n  late List<int> _nums; // Массив для хранения элементов двусторонней очереди\n  late int _front; // Указатель head, указывающий на первый элемент очереди\n  late int _queSize; // Длина двусторонней очереди\n\n  /* Конструктор */\n  ArrayDeque(int capacity) {\n    this._nums = List.filled(capacity, 0);\n    this._front = this._queSize = 0;\n  }\n\n  /* Получить вместимость двусторонней очереди */\n  int capacity() {\n    return _nums.length;\n  }\n\n  /* Получение длины двусторонней очереди */\n  int size() {\n    return _queSize;\n  }\n\n  /* Проверка, пуста ли двусторонняя очередь */\n  bool isEmpty() {\n    return _queSize == 0;\n  }\n\n  /* Вычислить индекс в кольцевом массиве */\n  int index(int i) {\n    // С помощью операции взятия по модулю соединить начало и конец массива\n    // Когда i выходит за конец массива, он возвращается в начало\n    // Когда i выходит за начало массива, он возвращается в конец\n    return (i + capacity()) % capacity();\n  }\n\n  /* Добавление в голову очереди */\n  void pushFirst(int _num) {\n    if (_queSize == capacity()) {\n      throw Exception(\"Двусторонняя очередь заполнена\");\n    }\n    // Указатель головы сместить влево на одну позицию\n    // С помощью операции взятия остатка реализовать возврат _front к хвосту после выхода за начало массива\n    _front = index(_front - 1);\n    // Добавить _num в голову очереди\n    _nums[_front] = _num;\n    _queSize++;\n  }\n\n  /* Добавление в хвост очереди */\n  void pushLast(int _num) {\n    if (_queSize == capacity()) {\n      throw Exception(\"Двусторонняя очередь заполнена\");\n    }\n    // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n    int rear = index(_front + _queSize);\n    // Добавить _num в хвост очереди\n    _nums[rear] = _num;\n    _queSize++;\n  }\n\n  /* Извлечение из головы очереди */\n  int popFirst() {\n    int _num = peekFirst();\n    // Указатель головы сместить вправо на одну позицию\n    _front = index(_front + 1);\n    _queSize--;\n    return _num;\n  }\n\n  /* Извлечение из хвоста очереди */\n  int popLast() {\n    int _num = peekLast();\n    _queSize--;\n    return _num;\n  }\n\n  /* Доступ к элементу в начале очереди */\n  int peekFirst() {\n    if (isEmpty()) {\n      throw Exception(\"двусторонняя очередь пуста\");\n    }\n    return _nums[_front];\n  }\n\n  /* Доступ к элементу в конце очереди */\n  int peekLast() {\n    if (isEmpty()) {\n      throw Exception(\"двусторонняя очередь пуста\");\n    }\n    // Вычислить индекс хвостового элемента\n    int last = index(_front + _queSize - 1);\n    return _nums[last];\n  }\n\n  /* Вернуть массив для вывода */\n  List<int> toArray() {\n    // Преобразовывать только элементы списка в пределах фактической длины\n    List<int> res = List.filled(_queSize, 0);\n    for (int i = 0, j = _front; i < _queSize; i++, j++) {\n      res[i] = _nums[index(j)];\n    }\n    return res;\n  }\n}\n
        array_deque.rs
        /* Двусторонняя очередь на основе кольцевого массива */\nstruct ArrayDeque<T> {\n    nums: Vec<T>,    // Массив для хранения элементов двусторонней очереди\n    front: usize,    // Указатель head, указывающий на первый элемент очереди\n    que_size: usize, // Длина двусторонней очереди\n}\n\nimpl<T: Copy + Default> ArrayDeque<T> {\n    /* Конструктор */\n    pub fn new(capacity: usize) -> Self {\n        Self {\n            nums: vec![T::default(); capacity],\n            front: 0,\n            que_size: 0,\n        }\n    }\n\n    /* Получить вместимость двусторонней очереди */\n    pub fn capacity(&self) -> usize {\n        self.nums.len()\n    }\n\n    /* Получение длины двусторонней очереди */\n    pub fn size(&self) -> usize {\n        self.que_size\n    }\n\n    /* Проверка, пуста ли двусторонняя очередь */\n    pub fn is_empty(&self) -> bool {\n        self.que_size == 0\n    }\n\n    /* Вычислить индекс в кольцевом массиве */\n    fn index(&self, i: i32) -> usize {\n        // С помощью операции взятия по модулю соединить начало и конец массива\n        // Когда i выходит за конец массива, он возвращается в начало\n        // Когда i выходит за начало массива, он возвращается в конец\n        ((i + self.capacity() as i32) % self.capacity() as i32) as usize\n    }\n\n    /* Добавление в голову очереди */\n    pub fn push_first(&mut self, num: T) {\n        if self.que_size == self.capacity() {\n            println!(\"Двусторонняя очередь заполнена\");\n            return;\n        }\n        // Указатель головы сдвигается на одну позицию влево\n        // С помощью операции взятия по модулю front после выхода за начало массива возвращается в хвост\n        self.front = self.index(self.front as i32 - 1);\n        // Добавить num в голову очереди\n        self.nums[self.front] = num;\n        self.que_size += 1;\n    }\n\n    /* Добавление в хвост очереди */\n    pub fn push_last(&mut self, num: T) {\n        if self.que_size == self.capacity() {\n            println!(\"Двусторонняя очередь заполнена\");\n            return;\n        }\n        // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n        let rear = self.index(self.front as i32 + self.que_size as i32);\n        // Добавить num в хвост очереди\n        self.nums[rear] = num;\n        self.que_size += 1;\n    }\n\n    /* Извлечение из головы очереди */\n    fn pop_first(&mut self) -> T {\n        let num = self.peek_first();\n        // Указатель головы сдвигается на одну позицию назад\n        self.front = self.index(self.front as i32 + 1);\n        self.que_size -= 1;\n        num\n    }\n\n    /* Извлечение из хвоста очереди */\n    fn pop_last(&mut self) -> T {\n        let num = self.peek_last();\n        self.que_size -= 1;\n        num\n    }\n\n    /* Доступ к элементу в начале очереди */\n    fn peek_first(&self) -> T {\n        if self.is_empty() {\n            panic!(\"двусторонняя очередь пуста\")\n        };\n        self.nums[self.front]\n    }\n\n    /* Доступ к элементу в конце очереди */\n    fn peek_last(&self) -> T {\n        if self.is_empty() {\n            panic!(\"двусторонняя очередь пуста\")\n        };\n        // Вычислить индекс хвостового элемента\n        let last = self.index(self.front as i32 + self.que_size as i32 - 1);\n        self.nums[last]\n    }\n\n    /* Вернуть массив для вывода */\n    fn to_array(&self) -> Vec<T> {\n        // Преобразовывать только элементы списка в пределах фактической длины\n        let mut res = vec![T::default(); self.que_size];\n        let mut j = self.front;\n        for i in 0..self.que_size {\n            res[i] = self.nums[self.index(j as i32)];\n            j += 1;\n        }\n        res\n    }\n}\n
        array_deque.c
        /* Двусторонняя очередь на основе кольцевого массива */\ntypedef struct {\n    int *nums;       // Массив для хранения элементов очереди\n    int front;       // Указатель head, указывающий на первый элемент очереди\n    int queSize;     // Указатель хвоста, указывающий на позицию после хвоста\n    int queCapacity; // Вместимость очереди\n} ArrayDeque;\n\n/* Конструктор */\nArrayDeque *newArrayDeque(int capacity) {\n    ArrayDeque *deque = (ArrayDeque *)malloc(sizeof(ArrayDeque));\n    // Инициализация массива\n    deque->queCapacity = capacity;\n    deque->nums = (int *)malloc(sizeof(int) * deque->queCapacity);\n    deque->front = deque->queSize = 0;\n    return deque;\n}\n\n/* Деструктор */\nvoid delArrayDeque(ArrayDeque *deque) {\n    free(deque->nums);\n    free(deque);\n}\n\n/* Получить вместимость двусторонней очереди */\nint capacity(ArrayDeque *deque) {\n    return deque->queCapacity;\n}\n\n/* Получение длины двусторонней очереди */\nint size(ArrayDeque *deque) {\n    return deque->queSize;\n}\n\n/* Проверка, пуста ли двусторонняя очередь */\nbool empty(ArrayDeque *deque) {\n    return deque->queSize == 0;\n}\n\n/* Вычислить индекс в кольцевом массиве */\nint dequeIndex(ArrayDeque *deque, int i) {\n    // С помощью операции взятия остатка соединить начало и конец массива\n    // Когда i выходит за хвост массива, вернуться к началу\n    // Когда i выходит за голову массива, вернуться к концу\n    return ((i + capacity(deque)) % capacity(deque));\n}\n\n/* Добавление в голову очереди */\nvoid pushFirst(ArrayDeque *deque, int num) {\n    if (deque->queSize == capacity(deque)) {\n        printf(\"Дек заполнен\\r\\n\");\n        return;\n    }\n    // Указатель головы сместить влево на одну позицию\n    // С помощью операции взятия остатка реализовать возврат front к хвосту после выхода за начало массива\n    deque->front = dequeIndex(deque, deque->front - 1);\n    // Добавить num в голову очереди\n    deque->nums[deque->front] = num;\n    deque->queSize++;\n}\n\n/* Добавление в хвост очереди */\nvoid pushLast(ArrayDeque *deque, int num) {\n    if (deque->queSize == capacity(deque)) {\n        printf(\"Дек заполнен\\r\\n\");\n        return;\n    }\n    // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n    int rear = dequeIndex(deque, deque->front + deque->queSize);\n    // Добавить num в хвост очереди\n    deque->nums[rear] = num;\n    deque->queSize++;\n}\n\n/* Доступ к элементу в начале очереди */\nint peekFirst(ArrayDeque *deque) {\n    // Ошибка доступа: двусторонняя очередь пуста\n    assert(empty(deque) == 0);\n    return deque->nums[deque->front];\n}\n\n/* Доступ к элементу в конце очереди */\nint peekLast(ArrayDeque *deque) {\n    // Ошибка доступа: двусторонняя очередь пуста\n    assert(empty(deque) == 0);\n    int last = dequeIndex(deque, deque->front + deque->queSize - 1);\n    return deque->nums[last];\n}\n\n/* Извлечение из головы очереди */\nint popFirst(ArrayDeque *deque) {\n    int num = peekFirst(deque);\n    // Указатель головы сдвигается на одну позицию назад\n    deque->front = dequeIndex(deque, deque->front + 1);\n    deque->queSize--;\n    return num;\n}\n\n/* Извлечение из хвоста очереди */\nint popLast(ArrayDeque *deque) {\n    int num = peekLast(deque);\n    deque->queSize--;\n    return num;\n}\n\n/* Вернуть массив для вывода */\nint *toArray(ArrayDeque *deque, int *queSize) {\n    *queSize = deque->queSize;\n    int *res = (int *)calloc(deque->queSize, sizeof(int));\n    int j = deque->front;\n    for (int i = 0; i < deque->queSize; i++) {\n        res[i] = deque->nums[j % deque->queCapacity];\n        j++;\n    }\n    return res;\n}\n
        array_deque.kt
        /* Конструктор */\nclass ArrayDeque(capacity: Int) {\n    private var nums: IntArray = IntArray(capacity) // Массив для хранения элементов двусторонней очереди\n    private var front: Int = 0 // Указатель head, указывающий на первый элемент очереди\n    private var queSize: Int = 0 // Длина двусторонней очереди\n\n    /* Получить вместимость двусторонней очереди */\n    fun capacity(): Int {\n        return nums.size\n    }\n\n    /* Получение длины двусторонней очереди */\n    fun size(): Int {\n        return queSize\n    }\n\n    /* Проверка, пуста ли двусторонняя очередь */\n    fun isEmpty(): Boolean {\n        return queSize == 0\n    }\n\n    /* Вычислить индекс в кольцевом массиве */\n    private fun index(i: Int): Int {\n        // С помощью операции взятия по модулю соединить начало и конец массива\n        // Когда i выходит за конец массива, он возвращается в начало\n        // Когда i выходит за начало массива, он возвращается в конец\n        return (i + capacity()) % capacity()\n    }\n\n    /* Добавление в голову очереди */\n    fun pushFirst(num: Int) {\n        if (queSize == capacity()) {\n            println(\"Двусторонняя очередь заполнена\")\n            return\n        }\n        // Указатель головы сдвигается на одну позицию влево\n        // С помощью операции взятия по модулю front после выхода за начало массива возвращается в хвост\n        front = index(front - 1)\n        // Добавить num в голову очереди\n        nums[front] = num\n        queSize++\n    }\n\n    /* Добавление в хвост очереди */\n    fun pushLast(num: Int) {\n        if (queSize == capacity()) {\n            println(\"Двусторонняя очередь заполнена\")\n            return\n        }\n        // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n        val rear = index(front + queSize)\n        // Добавить num в хвост очереди\n        nums[rear] = num\n        queSize++\n    }\n\n    /* Извлечение из головы очереди */\n    fun popFirst(): Int {\n        val num = peekFirst()\n        // Указатель головы сдвигается на одну позицию назад\n        front = index(front + 1)\n        queSize--\n        return num\n    }\n\n    /* Извлечение из хвоста очереди */\n    fun popLast(): Int {\n        val num = peekLast()\n        queSize--\n        return num\n    }\n\n    /* Доступ к элементу в начале очереди */\n    fun peekFirst(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return nums[front]\n    }\n\n    /* Доступ к элементу в конце очереди */\n    fun peekLast(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        // Вычислить индекс хвостового элемента\n        val last = index(front + queSize - 1)\n        return nums[last]\n    }\n\n    /* Вернуть массив для вывода */\n    fun toArray(): IntArray {\n        // Преобразовывать только элементы списка в пределах фактической длины\n        val res = IntArray(queSize)\n        var i = 0\n        var j = front\n        while (i < queSize) {\n            res[i] = nums[index(j)]\n            i++\n            j++\n        }\n        return res\n    }\n}\n
        array_deque.rb
        ### Двусторонняя очередь на основе кольцевого массива ###\nclass ArrayDeque\n  ### Получение длины двусторонней очереди ###\n  attr_reader :size\n\n  ### Конструктор ###\n  def initialize(capacity)\n    @nums = Array.new(capacity, 0)\n    @front = 0\n    @size = 0\n  end\n\n  ### Получить вместимость двусторонней очереди ###\n  def capacity\n    @nums.length\n  end\n\n  ### Проверка, пуста ли двусторонняя очередь ###\n  def is_empty?\n    size.zero?\n  end\n\n  ### Добавление в голову очереди ###\n  def push_first(num)\n    if size == capacity\n      puts 'Двусторонняя очередь заполнена'\n      return\n    end\n\n    # Указатель головы сдвигается на одну позицию влево\n    # С помощью операции взятия по модулю front после выхода за начало массива возвращается в хвост\n    @front = index(@front - 1)\n    # Добавить num в голову очереди\n    @nums[@front] = num\n    @size += 1\n  end\n\n  ### Добавление в хвост очереди ###\n  def push_last(num)\n    if size == capacity\n      puts 'Двусторонняя очередь заполнена'\n      return\n    end\n\n    # Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n    rear = index(@front + size)\n    # Добавить num в хвост очереди\n    @nums[rear] = num\n    @size += 1\n  end\n\n  ### Извлечение из головы очереди ###\n  def pop_first\n    num = peek_first\n    # Указатель головы сдвигается на одну позицию назад\n    @front = index(@front + 1)\n    @size -= 1\n    num\n  end\n\n  ### Извлечение из хвоста очереди ###\n  def pop_last\n    num = peek_last\n    @size -= 1\n    num\n  end\n\n  ### Доступ к элементу в начале очереди ###\n  def peek_first\n    raise IndexError, 'двусторонняя очередь пуста' if is_empty?\n\n    @nums[@front]\n  end\n\n  ### Доступ к элементу в хвосте очереди ###\n  def peek_last\n    raise IndexError, 'двусторонняя очередь пуста' if is_empty?\n\n    # Вычислить индекс хвостового элемента\n    last = index(@front + size - 1)\n    @nums[last]\n  end\n\n  ### Вернуть массив для вывода ###\n  def to_array\n    # Преобразовывать только элементы списка в пределах фактической длины\n    res = []\n    for i in 0...size\n      res << @nums[index(@front + i)]\n    end\n    res\n  end\n\n  private\n\n  ### Вычислить индекс в кольцевом массиве ###\n  def index(i)\n    # С помощью операции взятия по модулю соединить начало и конец массива\n    # Когда i выходит за конец массива, он возвращается в начало\n    # Когда i выходит за начало массива, он возвращается в конец\n    (i + capacity) % capacity\n  end\nend\n
        ","path":["Глава 5. Стек и очередь","5.3   Двусторонняя очередь"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#533","level":2,"title":"5.3.3   Применение двусторонней очереди","text":"

        Двусторонняя очередь сочетает в себе логику стека и очереди, поэтому она может покрыть все сценарии применения обеих структур и при этом предоставляет более высокую степень свободы.

        Мы знаем, что функция «undo» в программном обеспечении обычно реализуется с помощью стека: система помещает каждое изменение в стек с помощью push , а затем использует pop для отмены. Однако, учитывая ограниченность системных ресурсов, программы обычно ограничивают число шагов отмены, например разрешают хранить только \\(50\\) шагов. Когда длина стека превышает этот предел, программе нужно удалить элемент с дна стека, то есть с головы очереди. Но стек не может реализовать такую операцию, и в этом случае его приходится заменять двусторонней очередью. Обрати внимание: основная логика «undo» по-прежнему следует стековому правилу LIFO, просто двусторонняя очередь позволяет более гибко реализовать некоторые дополнительные механизмы.

        ","path":["Глава 5. Стек и очередь","5.3   Двусторонняя очередь"],"tags":[]},{"location":"chapter_stack_and_queue/queue/","level":1,"title":"5.2   Очередь","text":"

        Очередь (queue) - это линейная структура данных, подчиняющаяся правилу «первым пришел - первым вышел». Как видно из названия, очередь моделирует обычную ситуацию ожидания: новые люди непрерывно присоединяются к хвосту очереди, а стоящие в начале по одному уходят.

        Как показано на рисунке 5-4, начало очереди называется головой очереди, а конец - хвостом очереди. Операцию добавления элемента в хвост называют enqueue, а операцию удаления элемента из головы - dequeue.

        Рисунок 5-4   Правило FIFO для очереди

        ","path":["Глава 5. Стек и очередь","5.2   Очередь"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#521","level":2,"title":"5.2.1   Основные операции с очередью","text":"

        Распространенные операции с очередью показаны в таблице 5-2. Следует учитывать, что названия методов в разных языках могут различаться. Здесь мы используем те же названия, что и для стека.

        Таблица 5-2   Эффективность операций с очередью

        Имя метода Описание Временная сложность push() Поместить элемент в очередь, то есть добавить его в хвост \\(O(1)\\) pop() Извлечь элемент из головы очереди \\(O(1)\\) peek() Просмотреть элемент в голове очереди \\(O(1)\\)

        Обычно достаточно использовать готовые классы очереди, предоставляемые языками программирования:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby queue.py
        from collections import deque\n\n# Инициализация очереди\n# В Python обычно используют двустороннюю очередь deque как обычную очередь\n# Хотя queue.Queue() является \"чистой\" очередью, она не слишком удобна, поэтому ее не рекомендуют\nque: deque[int] = deque()\n\n# Поместить элементы в очередь\nque.append(1)\nque.append(3)\nque.append(2)\nque.append(5)\nque.append(4)\n\n# Просмотреть элемент в голове очереди\nfront: int = que[0]\n\n# Извлечь элемент из очереди\npop: int = que.popleft()\n\n# Получить длину очереди\nsize: int = len(que)\n\n# Проверить, пуста ли очередь\nis_empty: bool = len(que) == 0\n
        queue.cpp
        /* Инициализация очереди */\nqueue<int> queue;\n\n/* Поместить элементы в очередь */\nqueue.push(1);\nqueue.push(3);\nqueue.push(2);\nqueue.push(5);\nqueue.push(4);\n\n/* Просмотреть элемент в голове очереди */\nint front = queue.front();\n\n/* Извлечь элемент из очереди */\nqueue.pop();\n\n/* Получить длину очереди */\nint size = queue.size();\n\n/* Проверить, пуста ли очередь */\nbool empty = queue.empty();\n
        queue.java
        /* Инициализация очереди */\nQueue<Integer> queue = new LinkedList<>();\n\n/* Поместить элементы в очередь */\nqueue.offer(1);\nqueue.offer(3);\nqueue.offer(2);\nqueue.offer(5);\nqueue.offer(4);\n\n/* Просмотреть элемент в голове очереди */\nint peek = queue.peek();\n\n/* Извлечь элемент из очереди */\nint pop = queue.poll();\n\n/* Получить длину очереди */\nint size = queue.size();\n\n/* Проверить, пуста ли очередь */\nboolean isEmpty = queue.isEmpty();\n
        queue.cs
        /* Инициализация очереди */\nQueue<int> queue = new();\n\n/* Поместить элементы в очередь */\nqueue.Enqueue(1);\nqueue.Enqueue(3);\nqueue.Enqueue(2);\nqueue.Enqueue(5);\nqueue.Enqueue(4);\n\n/* Просмотреть элемент в голове очереди */\nint peek = queue.Peek();\n\n/* Извлечь элемент из очереди */\nint pop = queue.Dequeue();\n\n/* Получить длину очереди */\nint size = queue.Count;\n\n/* Проверить, пуста ли очередь */\nbool isEmpty = queue.Count == 0;\n
        queue_test.go
        /* Инициализация очереди */\n// В Go очередь обычно реализуют через list\nqueue := list.New()\n\n/* Поместить элементы в очередь */\nqueue.PushBack(1)\nqueue.PushBack(3)\nqueue.PushBack(2)\nqueue.PushBack(5)\nqueue.PushBack(4)\n\n/* Просмотреть элемент в голове очереди */\npeek := queue.Front()\n\n/* Извлечь элемент из очереди */\npop := queue.Front()\nqueue.Remove(pop)\n\n/* Получить длину очереди */\nsize := queue.Len()\n\n/* Проверить, пуста ли очередь */\nisEmpty := queue.Len() == 0\n
        queue.swift
        /* Инициализация очереди */\n// В Swift нет встроенного класса очереди, поэтому можно использовать Array как очередь\nvar queue: [Int] = []\n\n/* Поместить элементы в очередь */\nqueue.append(1)\nqueue.append(3)\nqueue.append(2)\nqueue.append(5)\nqueue.append(4)\n\n/* Просмотреть элемент в голове очереди */\nlet peek = queue.first!\n\n/* Извлечь элемент из очереди */\n// Поскольку в основе лежит массив, removeFirst имеет сложность O(n)\nlet pool = queue.removeFirst()\n\n/* Получить длину очереди */\nlet size = queue.count\n\n/* Проверить, пуста ли очередь */\nlet isEmpty = queue.isEmpty\n
        queue.js
        /* Инициализация очереди */\n// В JavaScript нет встроенной очереди, поэтому можно использовать Array как очередь\nconst queue = [];\n\n/* Поместить элементы в очередь */\nqueue.push(1);\nqueue.push(3);\nqueue.push(2);\nqueue.push(5);\nqueue.push(4);\n\n/* Просмотреть элемент в голове очереди */\nconst peek = queue[0];\n\n/* Извлечь элемент из очереди */\n// В основе лежит массив, поэтому shift() имеет сложность O(n)\nconst pop = queue.shift();\n\n/* Получить длину очереди */\nconst size = queue.length;\n\n/* Проверить, пуста ли очередь */\nconst empty = queue.length === 0;\n
        queue.ts
        /* Инициализация очереди */\n// В TypeScript нет встроенной очереди, поэтому можно использовать Array как очередь\nconst queue: number[] = [];\n\n/* Поместить элементы в очередь */\nqueue.push(1);\nqueue.push(3);\nqueue.push(2);\nqueue.push(5);\nqueue.push(4);\n\n/* Просмотреть элемент в голове очереди */\nconst peek = queue[0];\n\n/* Извлечь элемент из очереди */\n// В основе лежит массив, поэтому shift() имеет сложность O(n)\nconst pop = queue.shift();\n\n/* Получить длину очереди */\nconst size = queue.length;\n\n/* Проверить, пуста ли очередь */\nconst empty = queue.length === 0;\n
        queue.dart
        /* Инициализация очереди */\n// В Dart класс Queue является двусторонней очередью и может использоваться как обычная очередь\nQueue<int> queue = Queue();\n\n/* Поместить элементы в очередь */\nqueue.add(1);\nqueue.add(3);\nqueue.add(2);\nqueue.add(5);\nqueue.add(4);\n\n/* Просмотреть элемент в голове очереди */\nint peek = queue.first;\n\n/* Извлечь элемент из очереди */\nint pop = queue.removeFirst();\n\n/* Получить длину очереди */\nint size = queue.length;\n\n/* Проверить, пуста ли очередь */\nbool isEmpty = queue.isEmpty;\n
        queue.rs
        /* Инициализация двусторонней очереди */\n// В Rust двусторонняя очередь может использоваться как обычная очередь\nlet mut deque: VecDeque<u32> = VecDeque::new();\n\n/* Поместить элементы в очередь */\ndeque.push_back(1);\ndeque.push_back(3);\ndeque.push_back(2);\ndeque.push_back(5);\ndeque.push_back(4);\n\n/* Просмотреть элемент в голове очереди */\nif let Some(front) = deque.front() {\n}\n\n/* Извлечь элемент из очереди */\nif let Some(pop) = deque.pop_front() {\n}\n\n/* Получить длину очереди */\nlet size = deque.len();\n\n/* Проверить, пуста ли очередь */\nlet is_empty = deque.is_empty();\n
        queue.c
        // В C нет встроенной очереди\n
        queue.kt
        /* Инициализация очереди */\nval queue = LinkedList<Int>()\n\n/* Поместить элементы в очередь */\nqueue.offer(1)\nqueue.offer(3)\nqueue.offer(2)\nqueue.offer(5)\nqueue.offer(4)\n\n/* Просмотреть элемент в голове очереди */\nval peek = queue.peek()\n\n/* Извлечь элемент из очереди */\nval pop = queue.poll()\n\n/* Получить длину очереди */\nval size = queue.size\n\n/* Проверить, пуста ли очередь */\nval isEmpty = queue.isEmpty()\n
        queue.rb
        # Инициализация очереди\n# Встроенная очередь в Ruby (Thread::Queue) не имеет методов peek и traverse, поэтому можно использовать Array как очередь\nqueue = []\n\n# Поместить элементы в очередь\nqueue.push(1)\nqueue.push(3)\nqueue.push(2)\nqueue.push(5)\nqueue.push(4)\n\n# Просмотреть элемент очереди\npeek = queue.first\n\n# Извлечь элемент из очереди\n# Обрати внимание: поскольку это массив, метод Array#shift имеет сложность O(n)\npop = queue.shift\n\n# Получить длину очереди\nsize = queue.length\n\n# Проверить, пуста ли очередь\nis_empty = queue.empty?\n
        Визуализация выполнения

        https://pythontutor.com/render.html#code=from%20collections%20import%20deque%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C%0A%20%20%20%20%23%20%D0%92%20Python%20%D0%B4%D0%B2%D1%83%D1%81%D1%82%D0%BE%D1%80%D0%BE%D0%BD%D0%BD%D1%8E%D1%8E%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C%20deque%20%D0%BE%D0%B1%D1%8B%D1%87%D0%BD%D0%BE%20%D0%B8%D1%81%D0%BF%D0%BE%D0%BB%D1%8C%D0%B7%D1%83%D1%8E%D1%82%20%D0%BA%D0%B0%D0%BA%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C%0A%20%20%20%20%23%20%D0%A5%D0%BE%D1%82%D1%8F%20queue.Queue%28%29%20%D1%8F%D0%B2%D0%BB%D1%8F%D0%B5%D1%82%D1%81%D1%8F%20%D0%BD%D0%B0%D1%81%D1%82%D0%BE%D1%8F%D1%89%D0%B8%D0%BC%20%D0%BA%D0%BB%D0%B0%D1%81%D1%81%D0%BE%D0%BC%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%2C%20%D0%BF%D0%BE%D0%BB%D1%8C%D0%B7%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%D1%81%D1%8F%20%D0%B8%D0%BC%20%D0%BD%D0%B5%20%D1%81%D0%BB%D0%B8%D1%88%D0%BA%D0%BE%D0%BC%20%D1%83%D0%B4%D0%BE%D0%B1%D0%BD%D0%BE%0A%20%20%20%20que%20%3D%20deque%28%29%0A%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BC%D0%B5%D1%81%D1%82%D0%B8%D1%82%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B2%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C%0A%20%20%20%20que.append%281%29%0A%20%20%20%20que.append%283%29%0A%20%20%20%20que.append%282%29%0A%20%20%20%20que.append%285%29%0A%20%20%20%20que.append%284%29%0A%20%20%20%20print%28%22%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C%20que%20%3D%22%2C%20que%29%0A%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B8%D1%82%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B2%20%D0%BD%D0%B0%D1%87%D0%B0%D0%BB%D0%B5%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%0A%20%20%20%20front%20%3D%20que%5B0%5D%0A%20%20%20%20print%28%22%D0%AD%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B2%20%D0%BD%D0%B0%D1%87%D0%B0%D0%BB%D0%B5%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%20front%20%3D%22%2C%20front%29%0A%0A%20%20%20%20%23%20%D0%98%D0%B7%D0%B2%D0%BB%D0%B5%D1%87%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B8%D0%B7%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%0A%20%20%20%20pop%20%3D%20que.popleft%28%29%0A%20%20%20%20print%28%22%D0%98%D0%B7%D0%B2%D0%BB%D0%B5%D1%87%D0%B5%D0%BD%D0%BD%D1%8B%D0%B9%20%D0%B8%D0%B7%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20pop%20%3D%22%2C%20pop%29%0A%20%20%20%20print%28%22que%20%D0%BF%D0%BE%D1%81%D0%BB%D0%B5%20%D0%B8%D0%B7%D0%B2%D0%BB%D0%B5%D1%87%D0%B5%D0%BD%D0%B8%D1%8F%20%3D%22%2C%20que%29%0A%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B8%D1%82%D1%8C%20%D0%B4%D0%BB%D0%B8%D0%BD%D1%83%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%0A%20%20%20%20size%20%3D%20len%28que%29%0A%20%20%20%20print%28%22%D0%94%D0%BB%D0%B8%D0%BD%D0%B0%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%20size%20%3D%22%2C%20size%29%0A%0A%20%20%20%20%23%20%D0%9F%D1%80%D0%BE%D0%B2%D0%B5%D1%80%D0%B8%D1%82%D1%8C%2C%20%D0%BF%D1%83%D1%81%D1%82%D0%B0%20%D0%BB%D0%B8%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C%0A%20%20%20%20is_empty%20%3D%20len%28que%29%20%3D%3D%200%0A%20%20%20%20print%28%22%D0%9F%D1%83%D1%81%D1%82%D0%B0%20%D0%BB%D0%B8%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C%20%3D%22%2C%20is_empty%29&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

        ","path":["Глава 5. Стек и очередь","5.2   Очередь"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#522","level":2,"title":"5.2.2   Реализация очереди","text":"

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

        ","path":["Глава 5. Стек и очередь","5.2   Очередь"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#1","level":3,"title":"1.   Реализация на основе связного списка","text":"

        Как показано на рисунке 5-5, мы можем рассматривать головной узел и хвостовой узел связного списка как голову очереди и хвост очереди соответственно, договорившись, что добавлять узлы можно только в хвост, а удалять - только из головы.

        <1><2><3>

        Рисунок 5-5   Операции enqueue и dequeue в реализации очереди на связном списке

        Ниже приведен код реализации очереди на связном списке:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linkedlist_queue.py
        class LinkedListQueue:\n    \"\"\"Очередь на основе связного списка\"\"\"\n\n    def __init__(self):\n        \"\"\"Конструктор\"\"\"\n        self._front: ListNode | None = None  # Головной узел front\n        self._rear: ListNode | None = None  # Хвостовой узел rear\n        self._size: int = 0\n\n    def size(self) -> int:\n        \"\"\"Получение длины очереди\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"Проверка, пуста ли очередь\"\"\"\n        return self._size == 0\n\n    def push(self, num: int):\n        \"\"\"Поместить в очередь\"\"\"\n        # Добавить num после хвостового узла\n        node = ListNode(num)\n        # Если очередь пуста, сделать так, чтобы и head, и tail указывали на этот узел\n        if self._front is None:\n            self._front = node\n            self._rear = node\n        # Если очередь не пуста, добавить этот узел после хвостового узла\n        else:\n            self._rear.next = node\n            self._rear = node\n        self._size += 1\n\n    def pop(self) -> int:\n        \"\"\"Извлечь из очереди\"\"\"\n        num = self.peek()\n        # Удалить головной узел\n        self._front = self._front.next\n        self._size -= 1\n        return num\n\n    def peek(self) -> int:\n        \"\"\"Доступ к элементу в начале очереди\"\"\"\n        if self.is_empty():\n            raise IndexError(\"очередь пуста\")\n        return self._front.val\n\n    def to_list(self) -> list[int]:\n        \"\"\"Преобразовать в список для вывода\"\"\"\n        queue = []\n        temp = self._front\n        while temp:\n            queue.append(temp.val)\n            temp = temp.next\n        return queue\n
        linkedlist_queue.cpp
        /* Очередь на основе связного списка */\nclass LinkedListQueue {\n  private:\n    ListNode *front, *rear; // Головной узел front, хвостовой узел rear\n    int queSize;\n\n  public:\n    LinkedListQueue() {\n        front = nullptr;\n        rear = nullptr;\n        queSize = 0;\n    }\n\n    ~LinkedListQueue() {\n        // Обходить связный список, удалять узлы и освобождать память\n        freeMemoryLinkedList(front);\n    }\n\n    /* Получение длины очереди */\n    int size() {\n        return queSize;\n    }\n\n    /* Проверка, пуста ли очередь */\n    bool isEmpty() {\n        return queSize == 0;\n    }\n\n    /* Поместить в очередь */\n    void push(int num) {\n        // Добавить num после хвостового узла\n        ListNode *node = new ListNode(num);\n        // Если очередь пуста, сделать так, чтобы и head, и tail указывали на этот узел\n        if (front == nullptr) {\n            front = node;\n            rear = node;\n        }\n        // Если очередь не пуста, добавить этот узел после хвостового узла\n        else {\n            rear->next = node;\n            rear = node;\n        }\n        queSize++;\n    }\n\n    /* Извлечь из очереди */\n    int pop() {\n        int num = peek();\n        // Удалить головной узел\n        ListNode *tmp = front;\n        front = front->next;\n        // Освободить память\n        delete tmp;\n        queSize--;\n        return num;\n    }\n\n    /* Доступ к элементу в начале очереди */\n    int peek() {\n        if (size() == 0)\n            throw out_of_range(\"очередь пуста\");\n        return front->val;\n    }\n\n    /* Преобразовать связный список в Vector и вернуть */\n    vector<int> toVector() {\n        ListNode *node = front;\n        vector<int> res(size());\n        for (int i = 0; i < res.size(); i++) {\n            res[i] = node->val;\n            node = node->next;\n        }\n        return res;\n    }\n};\n
        linkedlist_queue.java
        /* Очередь на основе связного списка */\nclass LinkedListQueue {\n    private ListNode front, rear; // Головной узел front, хвостовой узел rear\n    private int queSize = 0;\n\n    public LinkedListQueue() {\n        front = null;\n        rear = null;\n    }\n\n    /* Получение длины очереди */\n    public int size() {\n        return queSize;\n    }\n\n    /* Проверка, пуста ли очередь */\n    public boolean isEmpty() {\n        return size() == 0;\n    }\n\n    /* Поместить в очередь */\n    public void push(int num) {\n        // Добавить num после хвостового узла\n        ListNode node = new ListNode(num);\n        // Если очередь пуста, сделать так, чтобы и head, и tail указывали на этот узел\n        if (front == null) {\n            front = node;\n            rear = node;\n        // Если очередь не пуста, добавить этот узел после хвостового узла\n        } else {\n            rear.next = node;\n            rear = node;\n        }\n        queSize++;\n    }\n\n    /* Извлечь из очереди */\n    public int pop() {\n        int num = peek();\n        // Удалить головной узел\n        front = front.next;\n        queSize--;\n        return num;\n    }\n\n    /* Доступ к элементу в начале очереди */\n    public int peek() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return front.val;\n    }\n\n    /* Преобразовать связный список в Array и вернуть */\n    public int[] toArray() {\n        ListNode node = front;\n        int[] res = new int[size()];\n        for (int i = 0; i < res.length; i++) {\n            res[i] = node.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
        linkedlist_queue.cs
        /* Очередь на основе связного списка */\nclass LinkedListQueue {\n    ListNode? front, rear;  // Головной узел front, хвостовой узел rear\n    int queSize = 0;\n\n    public LinkedListQueue() {\n        front = null;\n        rear = null;\n    }\n\n    /* Получение длины очереди */\n    public int Size() {\n        return queSize;\n    }\n\n    /* Проверка, пуста ли очередь */\n    public bool IsEmpty() {\n        return Size() == 0;\n    }\n\n    /* Поместить в очередь */\n    public void Push(int num) {\n        // Добавить num после хвостового узла\n        ListNode node = new(num);\n        // Если очередь пуста, сделать так, чтобы и head, и tail указывали на этот узел\n        if (front == null) {\n            front = node;\n            rear = node;\n            // Если очередь не пуста, добавить этот узел после хвостового узла\n        } else if (rear != null) {\n            rear.next = node;\n            rear = node;\n        }\n        queSize++;\n    }\n\n    /* Извлечь из очереди */\n    public int Pop() {\n        int num = Peek();\n        // Удалить головной узел\n        front = front?.next;\n        queSize--;\n        return num;\n    }\n\n    /* Доступ к элементу в начале очереди */\n    public int Peek() {\n        if (IsEmpty())\n            throw new Exception();\n        return front!.val;\n    }\n\n    /* Преобразовать связный список в Array и вернуть */\n    public int[] ToArray() {\n        if (front == null)\n            return [];\n\n        ListNode? node = front;\n        int[] res = new int[Size()];\n        for (int i = 0; i < res.Length; i++) {\n            res[i] = node!.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
        linkedlist_queue.go
        /* Очередь на основе связного списка */\ntype linkedListQueue struct {\n    // Использовать встроенный пакет list для реализации очереди\n    data *list.List\n}\n\n/* Инициализация очереди */\nfunc newLinkedListQueue() *linkedListQueue {\n    return &linkedListQueue{\n        data: list.New(),\n    }\n}\n\n/* Поместить в очередь */\nfunc (s *linkedListQueue) push(value any) {\n    s.data.PushBack(value)\n}\n\n/* Извлечь из очереди */\nfunc (s *linkedListQueue) pop() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Front()\n    s.data.Remove(e)\n    return e.Value\n}\n\n/* Доступ к элементу в начале очереди */\nfunc (s *linkedListQueue) peek() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Front()\n    return e.Value\n}\n\n/* Получение длины очереди */\nfunc (s *linkedListQueue) size() int {\n    return s.data.Len()\n}\n\n/* Проверка, пуста ли очередь */\nfunc (s *linkedListQueue) isEmpty() bool {\n    return s.data.Len() == 0\n}\n\n/* Получить List для вывода */\nfunc (s *linkedListQueue) toList() *list.List {\n    return s.data\n}\n
        linkedlist_queue.swift
        /* Очередь на основе связного списка */\nclass LinkedListQueue {\n    private var front: ListNode? // Головной узел\n    private var rear: ListNode? // Хвостовой узел\n    private var _size: Int\n\n    init() {\n        _size = 0\n    }\n\n    /* Получение длины очереди */\n    func size() -> Int {\n        _size\n    }\n\n    /* Проверка, пуста ли очередь */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* Поместить в очередь */\n    func push(num: Int) {\n        // Добавить num после хвостового узла\n        let node = ListNode(x: num)\n        // Если очередь пуста, сделать так, чтобы и head, и tail указывали на этот узел\n        if front == nil {\n            front = node\n            rear = node\n        }\n        // Если очередь не пуста, добавить этот узел после хвостового узла\n        else {\n            rear?.next = node\n            rear = node\n        }\n        _size += 1\n    }\n\n    /* Извлечь из очереди */\n    @discardableResult\n    func pop() -> Int {\n        let num = peek()\n        // Удалить головной узел\n        front = front?.next\n        _size -= 1\n        return num\n    }\n\n    /* Доступ к элементу в начале очереди */\n    func peek() -> Int {\n        if isEmpty() {\n            fatalError(\"очередь пуста\")\n        }\n        return front!.val\n    }\n\n    /* Преобразовать связный список в Array и вернуть */\n    func toArray() -> [Int] {\n        var node = front\n        var res = Array(repeating: 0, count: size())\n        for i in res.indices {\n            res[i] = node!.val\n            node = node?.next\n        }\n        return res\n    }\n}\n
        linkedlist_queue.js
        /* Очередь на основе связного списка */\nclass LinkedListQueue {\n    #front; // Головной узел #front\n    #rear; // Хвостовой узел #rear\n    #queSize = 0;\n\n    constructor() {\n        this.#front = null;\n        this.#rear = null;\n    }\n\n    /* Получение длины очереди */\n    get size() {\n        return this.#queSize;\n    }\n\n    /* Проверка, пуста ли очередь */\n    isEmpty() {\n        return this.size === 0;\n    }\n\n    /* Поместить в очередь */\n    push(num) {\n        // Добавить num после хвостового узла\n        const node = new ListNode(num);\n        // Если очередь пуста, сделать так, чтобы и head, и tail указывали на этот узел\n        if (!this.#front) {\n            this.#front = node;\n            this.#rear = node;\n            // Если очередь не пуста, добавить этот узел после хвостового узла\n        } else {\n            this.#rear.next = node;\n            this.#rear = node;\n        }\n        this.#queSize++;\n    }\n\n    /* Извлечь из очереди */\n    pop() {\n        const num = this.peek();\n        // Удалить головной узел\n        this.#front = this.#front.next;\n        this.#queSize--;\n        return num;\n    }\n\n    /* Доступ к элементу в начале очереди */\n    peek() {\n        if (this.size === 0) throw new Error('очередь пуста');\n        return this.#front.val;\n    }\n\n    /* Преобразовать связный список в Array и вернуть */\n    toArray() {\n        let node = this.#front;\n        const res = new Array(this.size);\n        for (let i = 0; i < res.length; i++) {\n            res[i] = node.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
        linkedlist_queue.ts
        /* Очередь на основе связного списка */\nclass LinkedListQueue {\n    private front: ListNode | null; // Головной узел front\n    private rear: ListNode | null; // Хвостовой узел rear\n    private queSize: number = 0;\n\n    constructor() {\n        this.front = null;\n        this.rear = null;\n    }\n\n    /* Получение длины очереди */\n    get size(): number {\n        return this.queSize;\n    }\n\n    /* Проверка, пуста ли очередь */\n    isEmpty(): boolean {\n        return this.size === 0;\n    }\n\n    /* Поместить в очередь */\n    push(num: number): void {\n        // Добавить num после хвостового узла\n        const node = new ListNode(num);\n        // Если очередь пуста, сделать так, чтобы и head, и tail указывали на этот узел\n        if (!this.front) {\n            this.front = node;\n            this.rear = node;\n            // Если очередь не пуста, добавить этот узел после хвостового узла\n        } else {\n            this.rear!.next = node;\n            this.rear = node;\n        }\n        this.queSize++;\n    }\n\n    /* Извлечь из очереди */\n    pop(): number {\n        const num = this.peek();\n        if (!this.front) throw new Error('очередь пуста');\n        // Удалить головной узел\n        this.front = this.front.next;\n        this.queSize--;\n        return num;\n    }\n\n    /* Доступ к элементу в начале очереди */\n    peek(): number {\n        if (this.size === 0) throw new Error('очередь пуста');\n        return this.front!.val;\n    }\n\n    /* Преобразовать связный список в Array и вернуть */\n    toArray(): number[] {\n        let node = this.front;\n        const res = new Array<number>(this.size);\n        for (let i = 0; i < res.length; i++) {\n            res[i] = node!.val;\n            node = node!.next;\n        }\n        return res;\n    }\n}\n
        linkedlist_queue.dart
        /* Очередь на основе связного списка */\nclass LinkedListQueue {\n  ListNode? _front; // Головной узел _front\n  ListNode? _rear; // Хвостовой узел _rear\n  int _queSize = 0; // Длина очереди\n\n  LinkedListQueue() {\n    _front = null;\n    _rear = null;\n  }\n\n  /* Получение длины очереди */\n  int size() {\n    return _queSize;\n  }\n\n  /* Проверка, пуста ли очередь */\n  bool isEmpty() {\n    return _queSize == 0;\n  }\n\n  /* Поместить в очередь */\n  void push(int _num) {\n    // Добавить _num после хвостового узла\n    final node = ListNode(_num);\n    // Если очередь пуста, сделать так, чтобы и head, и tail указывали на этот узел\n    if (_front == null) {\n      _front = node;\n      _rear = node;\n    } else {\n      // Если очередь не пуста, добавить этот узел после хвостового узла\n      _rear!.next = node;\n      _rear = node;\n    }\n    _queSize++;\n  }\n\n  /* Извлечь из очереди */\n  int pop() {\n    final int _num = peek();\n    // Удалить головной узел\n    _front = _front!.next;\n    _queSize--;\n    return _num;\n  }\n\n  /* Доступ к элементу в начале очереди */\n  int peek() {\n    if (_queSize == 0) {\n      throw Exception('очередь пуста');\n    }\n    return _front!.val;\n  }\n\n  /* Преобразовать связный список в Array и вернуть */\n  List<int> toArray() {\n    ListNode? node = _front;\n    final List<int> queue = [];\n    while (node != null) {\n      queue.add(node.val);\n      node = node.next;\n    }\n    return queue;\n  }\n}\n
        linkedlist_queue.rs
        /* Очередь на основе связного списка */\n#[allow(dead_code)]\npub struct LinkedListQueue<T> {\n    front: Option<Rc<RefCell<ListNode<T>>>>, // Головной узел front\n    rear: Option<Rc<RefCell<ListNode<T>>>>,  // Хвостовой узел rear\n    que_size: usize,                         // Длина очереди\n}\n\nimpl<T: Copy> LinkedListQueue<T> {\n    pub fn new() -> Self {\n        Self {\n            front: None,\n            rear: None,\n            que_size: 0,\n        }\n    }\n\n    /* Получение длины очереди */\n    pub fn size(&self) -> usize {\n        return self.que_size;\n    }\n\n    /* Проверка, пуста ли очередь */\n    pub fn is_empty(&self) -> bool {\n        return self.que_size == 0;\n    }\n\n    /* Поместить в очередь */\n    pub fn push(&mut self, num: T) {\n        // Добавить num после хвостового узла\n        let new_rear = ListNode::new(num);\n        match self.rear.take() {\n            // Если очередь не пуста, добавить этот узел после хвостового узла\n            Some(old_rear) => {\n                old_rear.borrow_mut().next = Some(new_rear.clone());\n                self.rear = Some(new_rear);\n            }\n            // Если очередь пуста, сделать так, чтобы и head, и tail указывали на этот узел\n            None => {\n                self.front = Some(new_rear.clone());\n                self.rear = Some(new_rear);\n            }\n        }\n        self.que_size += 1;\n    }\n\n    /* Извлечь из очереди */\n    pub fn pop(&mut self) -> Option<T> {\n        self.front.take().map(|old_front| {\n            match old_front.borrow_mut().next.take() {\n                Some(new_front) => {\n                    self.front = Some(new_front);\n                }\n                None => {\n                    self.rear.take();\n                }\n            }\n            self.que_size -= 1;\n            old_front.borrow().val\n        })\n    }\n\n    /* Доступ к элементу в начале очереди */\n    pub fn peek(&self) -> Option<&Rc<RefCell<ListNode<T>>>> {\n        self.front.as_ref()\n    }\n\n    /* Преобразовать связный список в Array и вернуть */\n    pub fn to_array(&self, head: Option<&Rc<RefCell<ListNode<T>>>>) -> Vec<T> {\n        let mut res: Vec<T> = Vec::new();\n\n        fn recur<T: Copy>(cur: Option<&Rc<RefCell<ListNode<T>>>>, res: &mut Vec<T>) {\n            if let Some(cur) = cur {\n                res.push(cur.borrow().val);\n                recur(cur.borrow().next.as_ref(), res);\n            }\n        }\n\n        recur(head, &mut res);\n\n        res\n    }\n}\n
        linkedlist_queue.c
        /* Очередь на основе связного списка */\ntypedef struct {\n    ListNode *front, *rear;\n    int queSize;\n} LinkedListQueue;\n\n/* Конструктор */\nLinkedListQueue *newLinkedListQueue() {\n    LinkedListQueue *queue = (LinkedListQueue *)malloc(sizeof(LinkedListQueue));\n    queue->front = NULL;\n    queue->rear = NULL;\n    queue->queSize = 0;\n    return queue;\n}\n\n/* Деструктор */\nvoid delLinkedListQueue(LinkedListQueue *queue) {\n    // Освободить все узлы\n    while (queue->front != NULL) {\n        ListNode *tmp = queue->front;\n        queue->front = queue->front->next;\n        free(tmp);\n    }\n    // Освободить структуру queue\n    free(queue);\n}\n\n/* Получение длины очереди */\nint size(LinkedListQueue *queue) {\n    return queue->queSize;\n}\n\n/* Проверка, пуста ли очередь */\nbool empty(LinkedListQueue *queue) {\n    return (size(queue) == 0);\n}\n\n/* Поместить в очередь */\nvoid push(LinkedListQueue *queue, int num) {\n    // Добавить node в хвост\n    ListNode *node = newListNode(num);\n    // Если очередь пуста, сделать так, чтобы и head, и tail указывали на этот узел\n    if (queue->front == NULL) {\n        queue->front = node;\n        queue->rear = node;\n    }\n    // Если очередь не пуста, добавить этот узел после хвостового узла\n    else {\n        queue->rear->next = node;\n        queue->rear = node;\n    }\n    queue->queSize++;\n}\n\n/* Доступ к элементу в начале очереди */\nint peek(LinkedListQueue *queue) {\n    assert(size(queue) && queue->front);\n    return queue->front->val;\n}\n\n/* Извлечь из очереди */\nint pop(LinkedListQueue *queue) {\n    int num = peek(queue);\n    ListNode *tmp = queue->front;\n    queue->front = queue->front->next;\n    free(tmp);\n    queue->queSize--;\n    return num;\n}\n\n/* Вывести очередь */\nvoid printLinkedListQueue(LinkedListQueue *queue) {\n    int *arr = malloc(sizeof(int) * queue->queSize);\n    // Скопировать данные связного списка в массив\n    int i;\n    ListNode *node;\n    for (i = 0, node = queue->front; i < queue->queSize; i++) {\n        arr[i] = node->val;\n        node = node->next;\n    }\n    printArray(arr, queue->queSize);\n    free(arr);\n}\n
        linkedlist_queue.kt
        /* Очередь на основе связного списка */\nclass LinkedListQueue(\n    // Головной узел front, хвостовой узел rear\n    private var front: ListNode? = null,\n    private var rear: ListNode? = null,\n    private var queSize: Int = 0\n) {\n\n    /* Получение длины очереди */\n    fun size(): Int {\n        return queSize\n    }\n\n    /* Проверка, пуста ли очередь */\n    fun isEmpty(): Boolean {\n        return size() == 0\n    }\n\n    /* Поместить в очередь */\n    fun push(num: Int) {\n        // Добавить num после хвостового узла\n        val node = ListNode(num)\n        // Если очередь пуста, сделать так, чтобы и head, и tail указывали на этот узел\n        if (front == null) {\n            front = node\n            rear = node\n            // Если очередь не пуста, добавить этот узел после хвостового узла\n        } else {\n            rear?.next = node\n            rear = node\n        }\n        queSize++\n    }\n\n    /* Извлечь из очереди */\n    fun pop(): Int {\n        val num = peek()\n        // Удалить головной узел\n        front = front?.next\n        queSize--\n        return num\n    }\n\n    /* Доступ к элементу в начале очереди */\n    fun peek(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return front!!._val\n    }\n\n    /* Преобразовать связный список в Array и вернуть */\n    fun toArray(): IntArray {\n        var node = front\n        val res = IntArray(size())\n        for (i in res.indices) {\n            res[i] = node!!._val\n            node = node.next\n        }\n        return res\n    }\n}\n
        linkedlist_queue.rb
        ### Очередь на основе связного списка ###\nclass LinkedListQueue\n  ### Получение длины очереди ###\n  attr_reader :size\n\n  ### Конструктор ###\n  def initialize\n    @front = nil  # Головной узел front\n    @rear = nil   # Хвостовой узел rear\n    @size = 0\n  end\n\n  ### Проверка, пуста ли очередь ###\n  def is_empty?\n    @front.nil?\n  end\n\n  ### Добавление в очередь ###\n  def push(num)\n    # Добавить num после хвостового узла\n    node = ListNode.new(num)\n\n    # Если очередь пуста, сделать так, чтобы и head, и tail указывали на этот узел\n    if @front.nil?\n      @front = node\n      @rear = node\n    # Если очередь не пуста, добавить этот узел после хвостового узла\n    else\n      @rear.next = node\n      @rear = node\n    end\n\n    @size += 1\n  end\n\n  ### Извлечение из очереди ###\n  def pop\n    num = peek\n    # Удалить головной узел\n    @front = @front.next\n    @size -= 1\n    num\n  end\n\n  ### Доступ к элементу в начале очереди ###\n  def peek\n    raise IndexError, 'очередь пуста' if is_empty?\n\n    @front.val\n  end\n\n  ### Преобразовать связный список в Array и вернуть ###\n  def to_array\n    queue = []\n    temp = @front\n    while temp\n      queue << temp.val\n      temp = temp.next\n    end\n    queue\n  end\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 5. Стек и очередь","5.2   Очередь"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#2","level":3,"title":"2.   Реализация на основе массива","text":"

        Удаление первого элемента из массива имеет временную сложность \\(O(n)\\) , из-за чего операция dequeue оказывается неэффективной. Однако этого можно избежать с помощью следующего приема.

        Мы можем использовать переменную front , указывающую на индекс элемента в голове очереди, и поддерживать переменную size , которая хранит длину очереди. Определим rear = front + size. Эта формула дает позицию rear, указывающую на ячейку сразу после хвоста очереди.

        Исходя из этого, эффективный диапазон элементов массива равен [front, rear - 1], а различные операции реализуются, как показано на рисунке 5-6.

        • Операция enqueue: записать входной элемент по индексу rear и увеличить size на 1.
        • Операция dequeue: просто увеличить front на 1 и уменьшить size на 1.

        Можно увидеть, что и enqueue , и dequeue требуют всего одной операции, а значит обе имеют временную сложность \\(O(1)\\) .

        <1><2><3>

        Рисунок 5-6   Операции enqueue и dequeue в реализации очереди на массиве

        Ты можешь заметить еще одну проблему: при непрерывных операциях enqueue и dequeue значения front и rear оба движутся вправо, и когда они доходят до конца массива, дальше сдвигаться уже нельзя. Чтобы решить эту проблему, можно рассматривать массив как кольцевой массив, у которого начало и конец соединены.

        Для кольцевого массива нужно сделать так, чтобы front или rear, перешагнув конец массива, сразу возвращались к его началу и продолжали движение. Такую периодичность удобно реализовать с помощью операции взятия остатка, как показано в коде ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_queue.py
        class ArrayQueue:\n    \"\"\"Очередь на основе кольцевого массива\"\"\"\n\n    def __init__(self, size: int):\n        \"\"\"Конструктор\"\"\"\n        self._nums: list[int] = [0] * size  # Массив для хранения элементов очереди\n        self._front: int = 0  # Указатель head, указывающий на первый элемент очереди\n        self._size: int = 0  # Длина очереди\n\n    def capacity(self) -> int:\n        \"\"\"Получить вместимость очереди\"\"\"\n        return len(self._nums)\n\n    def size(self) -> int:\n        \"\"\"Получение длины очереди\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"Проверка, пуста ли очередь\"\"\"\n        return self._size == 0\n\n    def push(self, num: int):\n        \"\"\"Поместить в очередь\"\"\"\n        if self._size == self.capacity():\n            raise IndexError(\"очередь заполнена\")\n        # Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n        # С помощью операции взятия по модулю вернуть rear к началу после выхода за конец массива\n        rear: int = (self._front + self._size) % self.capacity()\n        # Добавить num в хвост очереди\n        self._nums[rear] = num\n        self._size += 1\n\n    def pop(self) -> int:\n        \"\"\"Извлечь из очереди\"\"\"\n        num: int = self.peek()\n        # Указатель head сдвигается на одну позицию назад; если он выходит за конец, то возвращается в начало массива\n        self._front = (self._front + 1) % self.capacity()\n        self._size -= 1\n        return num\n\n    def peek(self) -> int:\n        \"\"\"Доступ к элементу в начале очереди\"\"\"\n        if self.is_empty():\n            raise IndexError(\"очередь пуста\")\n        return self._nums[self._front]\n\n    def to_list(self) -> list[int]:\n        \"\"\"Вернуть список для вывода\"\"\"\n        res = [0] * self.size()\n        j: int = self._front\n        for i in range(self.size()):\n            res[i] = self._nums[(j % self.capacity())]\n            j += 1\n        return res\n
        array_queue.cpp
        /* Очередь на основе кольцевого массива */\nclass ArrayQueue {\n  private:\n    int *nums;       // Массив для хранения элементов очереди\n    int front;       // Указатель head, указывающий на первый элемент очереди\n    int queSize;     // Длина очереди\n    int queCapacity; // Вместимость очереди\n\n  public:\n    ArrayQueue(int capacity) {\n        // Инициализация массива\n        nums = new int[capacity];\n        queCapacity = capacity;\n        front = queSize = 0;\n    }\n\n    ~ArrayQueue() {\n        delete[] nums;\n    }\n\n    /* Получить вместимость очереди */\n    int capacity() {\n        return queCapacity;\n    }\n\n    /* Получение длины очереди */\n    int size() {\n        return queSize;\n    }\n\n    /* Проверка, пуста ли очередь */\n    bool isEmpty() {\n        return size() == 0;\n    }\n\n    /* Поместить в очередь */\n    void push(int num) {\n        if (queSize == queCapacity) {\n            cout << \"Очередь заполнена\" << endl;\n            return;\n        }\n        // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n        // С помощью операции взятия по модулю вернуть rear к началу после выхода за конец массива\n        int rear = (front + queSize) % queCapacity;\n        // Добавить num в хвост очереди\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* Извлечь из очереди */\n    int pop() {\n        int num = peek();\n        // Указатель head сдвигается на одну позицию назад; если он выходит за конец, то возвращается в начало массива\n        front = (front + 1) % queCapacity;\n        queSize--;\n        return num;\n    }\n\n    /* Доступ к элементу в начале очереди */\n    int peek() {\n        if (isEmpty())\n            throw out_of_range(\"очередь пуста\");\n        return nums[front];\n    }\n\n    /* Преобразовать массив в Vector и вернуть */\n    vector<int> toVector() {\n        // Преобразовывать только элементы списка в пределах фактической длины\n        vector<int> arr(queSize);\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            arr[i] = nums[j % queCapacity];\n        }\n        return arr;\n    }\n};\n
        array_queue.java
        /* Очередь на основе кольцевого массива */\nclass ArrayQueue {\n    private int[] nums; // Массив для хранения элементов очереди\n    private int front; // Указатель head, указывающий на первый элемент очереди\n    private int queSize; // Длина очереди\n\n    public ArrayQueue(int capacity) {\n        nums = new int[capacity];\n        front = queSize = 0;\n    }\n\n    /* Получить вместимость очереди */\n    public int capacity() {\n        return nums.length;\n    }\n\n    /* Получение длины очереди */\n    public int size() {\n        return queSize;\n    }\n\n    /* Проверка, пуста ли очередь */\n    public boolean isEmpty() {\n        return queSize == 0;\n    }\n\n    /* Поместить в очередь */\n    public void push(int num) {\n        if (queSize == capacity()) {\n            System.out.println(\"Очередь заполнена\");\n            return;\n        }\n        // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n        // С помощью операции взятия по модулю вернуть rear к началу после выхода за конец массива\n        int rear = (front + queSize) % capacity();\n        // Добавить num в хвост очереди\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* Извлечь из очереди */\n    public int pop() {\n        int num = peek();\n        // Указатель head сдвигается на одну позицию назад; если он выходит за конец, то возвращается в начало массива\n        front = (front + 1) % capacity();\n        queSize--;\n        return num;\n    }\n\n    /* Доступ к элементу в начале очереди */\n    public int peek() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return nums[front];\n    }\n\n    /* Вернуть массив */\n    public int[] toArray() {\n        // Преобразовывать только элементы списка в пределах фактической длины\n        int[] res = new int[queSize];\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            res[i] = nums[j % capacity()];\n        }\n        return res;\n    }\n}\n
        array_queue.cs
        /* Очередь на основе кольцевого массива */\nclass ArrayQueue {\n    int[] nums;  // Массив для хранения элементов очереди\n    int front;   // Указатель head, указывающий на первый элемент очереди\n    int queSize; // Длина очереди\n\n    public ArrayQueue(int capacity) {\n        nums = new int[capacity];\n        front = queSize = 0;\n    }\n\n    /* Получить вместимость очереди */\n    int Capacity() {\n        return nums.Length;\n    }\n\n    /* Получение длины очереди */\n    public int Size() {\n        return queSize;\n    }\n\n    /* Проверка, пуста ли очередь */\n    public bool IsEmpty() {\n        return queSize == 0;\n    }\n\n    /* Поместить в очередь */\n    public void Push(int num) {\n        if (queSize == Capacity()) {\n            Console.WriteLine(\"Очередь заполнена\");\n            return;\n        }\n        // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n        // С помощью операции взятия по модулю вернуть rear к началу после выхода за конец массива\n        int rear = (front + queSize) % Capacity();\n        // Добавить num в хвост очереди\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* Извлечь из очереди */\n    public int Pop() {\n        int num = Peek();\n        // Указатель head сдвигается на одну позицию назад; если он выходит за конец, то возвращается в начало массива\n        front = (front + 1) % Capacity();\n        queSize--;\n        return num;\n    }\n\n    /* Доступ к элементу в начале очереди */\n    public int Peek() {\n        if (IsEmpty())\n            throw new Exception();\n        return nums[front];\n    }\n\n    /* Вернуть массив */\n    public int[] ToArray() {\n        // Преобразовывать только элементы списка в пределах фактической длины\n        int[] res = new int[queSize];\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            res[i] = nums[j % this.Capacity()];\n        }\n        return res;\n    }\n}\n
        array_queue.go
        /* Очередь на основе кольцевого массива */\ntype arrayQueue struct {\n    nums        []int // Массив для хранения элементов очереди\n    front       int   // Указатель head, указывающий на первый элемент очереди\n    queSize     int   // Длина очереди\n    queCapacity int   // Вместимость очереди (то есть максимальное число элементов)\n}\n\n/* Инициализация очереди */\nfunc newArrayQueue(queCapacity int) *arrayQueue {\n    return &arrayQueue{\n        nums:        make([]int, queCapacity),\n        queCapacity: queCapacity,\n        front:       0,\n        queSize:     0,\n    }\n}\n\n/* Получение длины очереди */\nfunc (q *arrayQueue) size() int {\n    return q.queSize\n}\n\n/* Проверка, пуста ли очередь */\nfunc (q *arrayQueue) isEmpty() bool {\n    return q.queSize == 0\n}\n\n/* Поместить в очередь */\nfunc (q *arrayQueue) push(num int) {\n    // Когда rear == queCapacity, очередь заполнена\n    if q.queSize == q.queCapacity {\n        return\n    }\n    // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n    // С помощью операции взятия по модулю вернуть rear к началу после выхода за конец массива\n    rear := (q.front + q.queSize) % q.queCapacity\n    // Добавить num в хвост очереди\n    q.nums[rear] = num\n    q.queSize++\n}\n\n/* Извлечь из очереди */\nfunc (q *arrayQueue) pop() any {\n    num := q.peek()\n    if num == nil {\n        return nil\n    }\n\n    // Указатель head сдвигается на одну позицию назад; если он выходит за конец, то возвращается в начало массива\n    q.front = (q.front + 1) % q.queCapacity\n    q.queSize--\n    return num\n}\n\n/* Доступ к элементу в начале очереди */\nfunc (q *arrayQueue) peek() any {\n    if q.isEmpty() {\n        return nil\n    }\n    return q.nums[q.front]\n}\n\n/* Получить Slice для вывода */\nfunc (q *arrayQueue) toSlice() []int {\n    rear := (q.front + q.queSize)\n    if rear >= q.queCapacity {\n        rear %= q.queCapacity\n        return append(q.nums[q.front:], q.nums[:rear]...)\n    }\n    return q.nums[q.front:rear]\n}\n
        array_queue.swift
        /* Очередь на основе кольцевого массива */\nclass ArrayQueue {\n    private var nums: [Int] // Массив для хранения элементов очереди\n    private var front: Int // Указатель head, указывающий на первый элемент очереди\n    private var _size: Int // Длина очереди\n\n    init(capacity: Int) {\n        // Инициализация массива\n        nums = Array(repeating: 0, count: capacity)\n        front = 0\n        _size = 0\n    }\n\n    /* Получить вместимость очереди */\n    func capacity() -> Int {\n        nums.count\n    }\n\n    /* Получение длины очереди */\n    func size() -> Int {\n        _size\n    }\n\n    /* Проверка, пуста ли очередь */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* Поместить в очередь */\n    func push(num: Int) {\n        if size() == capacity() {\n            print(\"Очередь заполнена\")\n            return\n        }\n        // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n        // С помощью операции взятия по модулю вернуть rear к началу после выхода за конец массива\n        let rear = (front + size()) % capacity()\n        // Добавить num в хвост очереди\n        nums[rear] = num\n        _size += 1\n    }\n\n    /* Извлечь из очереди */\n    @discardableResult\n    func pop() -> Int {\n        let num = peek()\n        // Указатель head сдвигается на одну позицию назад; если он выходит за конец, то возвращается в начало массива\n        front = (front + 1) % capacity()\n        _size -= 1\n        return num\n    }\n\n    /* Доступ к элементу в начале очереди */\n    func peek() -> Int {\n        if isEmpty() {\n            fatalError(\"очередь пуста\")\n        }\n        return nums[front]\n    }\n\n    /* Вернуть массив */\n    func toArray() -> [Int] {\n        // Преобразовывать только элементы списка в пределах фактической длины\n        (front ..< front + size()).map { nums[$0 % capacity()] }\n    }\n}\n
        array_queue.js
        /* Очередь на основе кольцевого массива */\nclass ArrayQueue {\n    #nums; // Массив для хранения элементов очереди\n    #front = 0; // Указатель head, указывающий на первый элемент очереди\n    #queSize = 0; // Длина очереди\n\n    constructor(capacity) {\n        this.#nums = new Array(capacity);\n    }\n\n    /* Получить вместимость очереди */\n    get capacity() {\n        return this.#nums.length;\n    }\n\n    /* Получение длины очереди */\n    get size() {\n        return this.#queSize;\n    }\n\n    /* Проверка, пуста ли очередь */\n    isEmpty() {\n        return this.#queSize === 0;\n    }\n\n    /* Поместить в очередь */\n    push(num) {\n        if (this.size === this.capacity) {\n            console.log('Очередь заполнена');\n            return;\n        }\n        // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n        // С помощью операции взятия по модулю вернуть rear к началу после выхода за конец массива\n        const rear = (this.#front + this.size) % this.capacity;\n        // Добавить num в хвост очереди\n        this.#nums[rear] = num;\n        this.#queSize++;\n    }\n\n    /* Извлечь из очереди */\n    pop() {\n        const num = this.peek();\n        // Указатель head сдвигается на одну позицию назад; если он выходит за конец, то возвращается в начало массива\n        this.#front = (this.#front + 1) % this.capacity;\n        this.#queSize--;\n        return num;\n    }\n\n    /* Доступ к элементу в начале очереди */\n    peek() {\n        if (this.isEmpty()) throw new Error('очередь пуста');\n        return this.#nums[this.#front];\n    }\n\n    /* Вернуть Array */\n    toArray() {\n        // Преобразовывать только элементы списка в пределах фактической длины\n        const arr = new Array(this.size);\n        for (let i = 0, j = this.#front; i < this.size; i++, j++) {\n            arr[i] = this.#nums[j % this.capacity];\n        }\n        return arr;\n    }\n}\n
        array_queue.ts
        /* Очередь на основе кольцевого массива */\nclass ArrayQueue {\n    private nums: number[]; // Массив для хранения элементов очереди\n    private front: number; // Указатель head, указывающий на первый элемент очереди\n    private queSize: number; // Длина очереди\n\n    constructor(capacity: number) {\n        this.nums = new Array(capacity);\n        this.front = this.queSize = 0;\n    }\n\n    /* Получить вместимость очереди */\n    get capacity(): number {\n        return this.nums.length;\n    }\n\n    /* Получение длины очереди */\n    get size(): number {\n        return this.queSize;\n    }\n\n    /* Проверка, пуста ли очередь */\n    isEmpty(): boolean {\n        return this.queSize === 0;\n    }\n\n    /* Поместить в очередь */\n    push(num: number): void {\n        if (this.size === this.capacity) {\n            console.log('Очередь заполнена');\n            return;\n        }\n        // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n        // С помощью операции взятия по модулю вернуть rear к началу после выхода за конец массива\n        const rear = (this.front + this.queSize) % this.capacity;\n        // Добавить num в хвост очереди\n        this.nums[rear] = num;\n        this.queSize++;\n    }\n\n    /* Извлечь из очереди */\n    pop(): number {\n        const num = this.peek();\n        // Указатель head сдвигается на одну позицию назад; если он выходит за конец, то возвращается в начало массива\n        this.front = (this.front + 1) % this.capacity;\n        this.queSize--;\n        return num;\n    }\n\n    /* Доступ к элементу в начале очереди */\n    peek(): number {\n        if (this.isEmpty()) throw new Error('очередь пуста');\n        return this.nums[this.front];\n    }\n\n    /* Вернуть Array */\n    toArray(): number[] {\n        // Преобразовывать только элементы списка в пределах фактической длины\n        const arr = new Array(this.size);\n        for (let i = 0, j = this.front; i < this.size; i++, j++) {\n            arr[i] = this.nums[j % this.capacity];\n        }\n        return arr;\n    }\n}\n
        array_queue.dart
        /* Очередь на основе кольцевого массива */\nclass ArrayQueue {\n  late List<int> _nums; // Массив для хранения элементов очереди\n  late int _front; // Указатель head, указывающий на первый элемент очереди\n  late int _queSize; // Длина очереди\n\n  ArrayQueue(int capacity) {\n    _nums = List.filled(capacity, 0);\n    _front = _queSize = 0;\n  }\n\n  /* Получить вместимость очереди */\n  int capaCity() {\n    return _nums.length;\n  }\n\n  /* Получение длины очереди */\n  int size() {\n    return _queSize;\n  }\n\n  /* Проверка, пуста ли очередь */\n  bool isEmpty() {\n    return _queSize == 0;\n  }\n\n  /* Поместить в очередь */\n  void push(int _num) {\n    if (_queSize == capaCity()) {\n      throw Exception(\"Очередь заполнена\");\n    }\n    // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n    // С помощью операции взятия по модулю вернуть rear к началу после выхода за конец массива\n    int rear = (_front + _queSize) % capaCity();\n    // Добавить _num в хвост очереди\n    _nums[rear] = _num;\n    _queSize++;\n  }\n\n  /* Извлечь из очереди */\n  int pop() {\n    int _num = peek();\n    // Указатель head сдвигается на одну позицию назад; если он выходит за конец, то возвращается в начало массива\n    _front = (_front + 1) % capaCity();\n    _queSize--;\n    return _num;\n  }\n\n  /* Доступ к элементу в начале очереди */\n  int peek() {\n    if (isEmpty()) {\n      throw Exception(\"очередь пуста\");\n    }\n    return _nums[_front];\n  }\n\n  /* Вернуть Array */\n  List<int> toArray() {\n    // Преобразовывать только элементы списка в пределах фактической длины\n    final List<int> res = List.filled(_queSize, 0);\n    for (int i = 0, j = _front; i < _queSize; i++, j++) {\n      res[i] = _nums[j % capaCity()];\n    }\n    return res;\n  }\n}\n
        array_queue.rs
        /* Очередь на основе кольцевого массива */\nstruct ArrayQueue<T> {\n    nums: Vec<T>,      // Массив для хранения элементов очереди\n    front: i32,        // Указатель head, указывающий на первый элемент очереди\n    que_size: i32,     // Длина очереди\n    que_capacity: i32, // Вместимость очереди\n}\n\nimpl<T: Copy + Default> ArrayQueue<T> {\n    /* Конструктор */\n    fn new(capacity: i32) -> ArrayQueue<T> {\n        ArrayQueue {\n            nums: vec![T::default(); capacity as usize],\n            front: 0,\n            que_size: 0,\n            que_capacity: capacity,\n        }\n    }\n\n    /* Получить вместимость очереди */\n    fn capacity(&self) -> i32 {\n        self.que_capacity\n    }\n\n    /* Получение длины очереди */\n    fn size(&self) -> i32 {\n        self.que_size\n    }\n\n    /* Проверка, пуста ли очередь */\n    fn is_empty(&self) -> bool {\n        self.que_size == 0\n    }\n\n    /* Поместить в очередь */\n    fn push(&mut self, num: T) {\n        if self.que_size == self.capacity() {\n            println!(\"Очередь заполнена\");\n            return;\n        }\n        // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n        // С помощью операции взятия по модулю вернуть rear к началу после выхода за конец массива\n        let rear = (self.front + self.que_size) % self.que_capacity;\n        // Добавить num в хвост очереди\n        self.nums[rear as usize] = num;\n        self.que_size += 1;\n    }\n\n    /* Извлечь из очереди */\n    fn pop(&mut self) -> T {\n        let num = self.peek();\n        // Указатель head сдвигается на одну позицию назад; если он выходит за конец, то возвращается в начало массива\n        self.front = (self.front + 1) % self.que_capacity;\n        self.que_size -= 1;\n        num\n    }\n\n    /* Доступ к элементу в начале очереди */\n    fn peek(&self) -> T {\n        if self.is_empty() {\n            panic!(\"index out of bounds\");\n        }\n        self.nums[self.front as usize]\n    }\n\n    /* Вернуть массив */\n    fn to_vector(&self) -> Vec<T> {\n        let cap = self.que_capacity;\n        let mut j = self.front;\n        let mut arr = vec![T::default(); cap as usize];\n        for i in 0..self.que_size {\n            arr[i as usize] = self.nums[(j % cap) as usize];\n            j += 1;\n        }\n        arr\n    }\n}\n
        array_queue.c
        /* Очередь на основе кольцевого массива */\ntypedef struct {\n    int *nums;       // Массив для хранения элементов очереди\n    int front;       // Указатель head, указывающий на первый элемент очереди\n    int queSize;     // Текущее количество элементов в очереди\n    int queCapacity; // Вместимость очереди\n} ArrayQueue;\n\n/* Конструктор */\nArrayQueue *newArrayQueue(int capacity) {\n    ArrayQueue *queue = (ArrayQueue *)malloc(sizeof(ArrayQueue));\n    // Инициализация массива\n    queue->queCapacity = capacity;\n    queue->nums = (int *)malloc(sizeof(int) * queue->queCapacity);\n    queue->front = queue->queSize = 0;\n    return queue;\n}\n\n/* Деструктор */\nvoid delArrayQueue(ArrayQueue *queue) {\n    free(queue->nums);\n    free(queue);\n}\n\n/* Получить вместимость очереди */\nint capacity(ArrayQueue *queue) {\n    return queue->queCapacity;\n}\n\n/* Получение длины очереди */\nint size(ArrayQueue *queue) {\n    return queue->queSize;\n}\n\n/* Проверка, пуста ли очередь */\nbool empty(ArrayQueue *queue) {\n    return queue->queSize == 0;\n}\n\n/* Доступ к элементу в начале очереди */\nint peek(ArrayQueue *queue) {\n    assert(size(queue) != 0);\n    return queue->nums[queue->front];\n}\n\n/* Поместить в очередь */\nvoid push(ArrayQueue *queue, int num) {\n    if (size(queue) == capacity(queue)) {\n        printf(\"Очередь заполнена\\r\\n\");\n        return;\n    }\n    // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n    // С помощью операции взятия по модулю вернуть rear к началу после выхода за конец массива\n    int rear = (queue->front + queue->queSize) % queue->queCapacity;\n    // Добавить num в хвост очереди\n    queue->nums[rear] = num;\n    queue->queSize++;\n}\n\n/* Извлечь из очереди */\nint pop(ArrayQueue *queue) {\n    int num = peek(queue);\n    // Указатель head сдвигается на одну позицию назад; если он выходит за конец, то возвращается в начало массива\n    queue->front = (queue->front + 1) % queue->queCapacity;\n    queue->queSize--;\n    return num;\n}\n\n/* Вернуть массив для вывода */\nint *toArray(ArrayQueue *queue, int *queSize) {\n    *queSize = queue->queSize;\n    int *res = (int *)calloc(queue->queSize, sizeof(int));\n    int j = queue->front;\n    for (int i = 0; i < queue->queSize; i++) {\n        res[i] = queue->nums[j % queue->queCapacity];\n        j++;\n    }\n    return res;\n}\n
        array_queue.kt
        /* Очередь на основе кольцевого массива */\nclass ArrayQueue(capacity: Int) {\n    private val nums: IntArray = IntArray(capacity) // Массив для хранения элементов очереди\n    private var front: Int = 0 // Указатель head, указывающий на первый элемент очереди\n    private var queSize: Int = 0 // Длина очереди\n\n    /* Получить вместимость очереди */\n    fun capacity(): Int {\n        return nums.size\n    }\n\n    /* Получение длины очереди */\n    fun size(): Int {\n        return queSize\n    }\n\n    /* Проверка, пуста ли очередь */\n    fun isEmpty(): Boolean {\n        return queSize == 0\n    }\n\n    /* Поместить в очередь */\n    fun push(num: Int) {\n        if (queSize == capacity()) {\n            println(\"Очередь заполнена\")\n            return\n        }\n        // Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n        // С помощью операции взятия по модулю вернуть rear к началу после выхода за конец массива\n        val rear = (front + queSize) % capacity()\n        // Добавить num в хвост очереди\n        nums[rear] = num\n        queSize++\n    }\n\n    /* Извлечь из очереди */\n    fun pop(): Int {\n        val num = peek()\n        // Указатель head сдвигается на одну позицию назад; если он выходит за конец, то возвращается в начало массива\n        front = (front + 1) % capacity()\n        queSize--\n        return num\n    }\n\n    /* Доступ к элементу в начале очереди */\n    fun peek(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return nums[front]\n    }\n\n    /* Вернуть массив */\n    fun toArray(): IntArray {\n        // Преобразовывать только элементы списка в пределах фактической длины\n        val res = IntArray(queSize)\n        var i = 0\n        var j = front\n        while (i < queSize) {\n            res[i] = nums[j % capacity()]\n            i++\n            j++\n        }\n        return res\n    }\n}\n
        array_queue.rb
        ### Очередь на основе кольцевого массива ###\nclass ArrayQueue\n  ### Получение длины очереди ###\n  attr_reader :size\n\n  ### Конструктор ###\n  def initialize(size)\n    @nums = Array.new(size, 0) # Массив для хранения элементов очереди\n    @front = 0 # Указатель head, указывающий на первый элемент очереди\n    @size = 0 # Длина очереди\n  end\n\n  ### Получить вместимость очереди ###\n  def capacity\n    @nums.length\n  end\n\n  ### Проверка, пуста ли очередь ###\n  def is_empty?\n    size.zero?\n  end\n\n  ### Добавление в очередь ###\n  def push(num)\n    raise IndexError, 'очередь заполнена' if size == capacity\n\n    # Вычислить указатель хвоста, указывающий на индекс хвоста + 1\n    # С помощью операции взятия по модулю вернуть rear к началу после выхода за конец массива\n    rear = (@front + size) % capacity\n    # Добавить num в хвост очереди\n    @nums[rear] = num\n    @size += 1\n  end\n\n  ### Извлечение из очереди ###\n  def pop\n    num = peek\n    # Указатель head сдвигается на одну позицию назад; если он выходит за конец, то возвращается в начало массива\n    @front = (@front + 1) % capacity\n    @size -= 1\n    num\n  end\n\n  ### Доступ к элементу в начале очереди ###\n  def peek\n    raise IndexError, 'очередь пуста' if is_empty?\n\n    @nums[@front]\n  end\n\n  ### Вернуть список для вывода ###\n  def to_array\n    res = Array.new(size, 0)\n    j = @front\n\n    for i in 0...size\n      res[i] = @nums[j % capacity]\n      j += 1\n    end\n\n    res\n  end\nend\n
        Визуализация кода

        Во весь экран >

        Даже такая реализация очереди остается ограниченной: ее длина неизменяема. Однако это несложно исправить, заменив массив на динамический массив и тем самым введя механизм расширения. Заинтересованные читатели могут попробовать реализовать это самостоятельно.

        Выводы сравнения двух реализаций в целом такие же, как и для стека, поэтому здесь мы не будем повторяться.

        ","path":["Глава 5. Стек и очередь","5.2   Очередь"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#523","level":2,"title":"5.2.3   Типичные применения очереди","text":"
        • Очереди заказов. После оформления заказа покупателем заказ попадает в очередь, а затем система обрабатывает заказы по порядку. Во время крупных распродаж за короткое время возникает огромный поток заказов, и высокая конкурентная нагрузка становится ключевой инженерной проблемой.
        • Различные отложенные задачи. Любой сценарий, где нужно реализовать принцип «кто раньше пришел, тот раньше обслуживается», например очередь заданий принтера или очередь блюд на кухне ресторана, хорошо моделируется очередью, которая эффективно поддерживает нужный порядок обработки.
        ","path":["Глава 5. Стек и очередь","5.2   Очередь"],"tags":[]},{"location":"chapter_stack_and_queue/stack/","level":1,"title":"5.1   Стек","text":"

        Стек (stack) - это линейная структура данных, подчиняющаяся логике «последним пришел - первым вышел».

        Стек можно сравнить со стопкой тарелок на столе. Если разрешено перемещать только одну тарелку за раз, то, чтобы достать тарелку снизу, сначала придется по одной убрать все тарелки сверху. Если заменить тарелки различными элементами, например целыми числами, символами, объектами и т.д., получится структура данных «стек».

        Как показано на рисунке 5-1, верхнюю часть стопки элементов мы называем вершиной стека, а нижнюю - основанием стека. Операция добавления элемента на вершину называется push, а операция удаления верхнего элемента - pop.

        Рисунок 5-1   Правило LIFO для стека

        ","path":["Глава 5. Стек и очередь","5.1   Стек"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#511","level":2,"title":"5.1.1   Основные операции со стеком","text":"

        Основные операции со стеком показаны в таблице 5-1. Конкретные имена методов зависят от используемого языка программирования. Здесь в качестве примера используются распространенные названия push() , pop() и peek() .

        Таблица 5-1   Эффективность операций со стеком

        Метод Описание Временная сложность push() Поместить элемент в стек (на вершину) \\(O(1)\\) pop() Извлечь верхний элемент стека \\(O(1)\\) peek() Просмотреть верхний элемент \\(O(1)\\)

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

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby stack.py
        # Инициализация стека\n# В Python нет встроенного класса стека, поэтому можно использовать list как стек\nstack: list[int] = []\n\n# Поместить элементы в стек\nstack.append(1)\nstack.append(3)\nstack.append(2)\nstack.append(5)\nstack.append(4)\n\n# Просмотреть верхний элемент\npeek: int = stack[-1]\n\n# Извлечь элемент\npop: int = stack.pop()\n\n# Получить длину стека\nsize: int = len(stack)\n\n# Проверить, пуст ли стек\nis_empty: bool = len(stack) == 0\n
        stack.cpp
        /* Инициализация стека */\nstack<int> stack;\n\n/* Поместить элементы в стек */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* Просмотреть верхний элемент */\nint top = stack.top();\n\n/* Извлечь элемент */\nstack.pop(); // Без возвращаемого значения\n\n/* Получить длину стека */\nint size = stack.size();\n\n/* Проверить, пуст ли стек */\nbool empty = stack.empty();\n
        stack.java
        /* Инициализация стека */\nStack<Integer> stack = new Stack<>();\n\n/* Поместить элементы в стек */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* Просмотреть верхний элемент */\nint peek = stack.peek();\n\n/* Извлечь элемент */\nint pop = stack.pop();\n\n/* Получить длину стека */\nint size = stack.size();\n\n/* Проверить, пуст ли стек */\nboolean isEmpty = stack.isEmpty();\n
        stack.cs
        /* Инициализация стека */\nStack<int> stack = new();\n\n/* Поместить элементы в стек */\nstack.Push(1);\nstack.Push(3);\nstack.Push(2);\nstack.Push(5);\nstack.Push(4);\n\n/* Просмотреть верхний элемент */\nint peek = stack.Peek();\n\n/* Извлечь элемент */\nint pop = stack.Pop();\n\n/* Получить длину стека */\nint size = stack.Count;\n\n/* Проверить, пуст ли стек */\nbool isEmpty = stack.Count == 0;\n
        stack_test.go
        /* Инициализация стека */\n// В Go рекомендуется использовать Slice как стек\nvar stack []int\n\n/* Поместить элементы в стек */\nstack = append(stack, 1)\nstack = append(stack, 3)\nstack = append(stack, 2)\nstack = append(stack, 5)\nstack = append(stack, 4)\n\n/* Просмотреть верхний элемент */\npeek := stack[len(stack)-1]\n\n/* Извлечь элемент */\npop := stack[len(stack)-1]\nstack = stack[:len(stack)-1]\n\n/* Получить длину стека */\nsize := len(stack)\n\n/* Проверить, пуст ли стек */\nisEmpty := len(stack) == 0\n
        stack.swift
        /* Инициализация стека */\n// В Swift нет встроенного класса стека, поэтому можно использовать Array как стек\nvar stack: [Int] = []\n\n/* Поместить элементы в стек */\nstack.append(1)\nstack.append(3)\nstack.append(2)\nstack.append(5)\nstack.append(4)\n\n/* Просмотреть верхний элемент */\nlet peek = stack.last!\n\n/* Извлечь элемент */\nlet pop = stack.removeLast()\n\n/* Получить длину стека */\nlet size = stack.count\n\n/* Проверить, пуст ли стек */\nlet isEmpty = stack.isEmpty\n
        stack.js
        /* Инициализация стека */\n// В JavaScript нет встроенного класса стека, поэтому можно использовать Array как стек\nconst stack = [];\n\n/* Поместить элементы в стек */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* Просмотреть верхний элемент */\nconst peek = stack[stack.length-1];\n\n/* Извлечь элемент */\nconst pop = stack.pop();\n\n/* Получить длину стека */\nconst size = stack.length;\n\n/* Проверить, пуст ли стек */\nconst is_empty = stack.length === 0;\n
        stack.ts
        /* Инициализация стека */\n// В TypeScript нет встроенного класса стека, поэтому можно использовать Array как стек\nconst stack: number[] = [];\n\n/* Поместить элементы в стек */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* Просмотреть верхний элемент */\nconst peek = stack[stack.length - 1];\n\n/* Извлечь элемент */\nconst pop = stack.pop();\n\n/* Получить длину стека */\nconst size = stack.length;\n\n/* Проверить, пуст ли стек */\nconst is_empty = stack.length === 0;\n
        stack.dart
        /* Инициализация стека */\n// В Dart нет встроенного класса стека, поэтому можно использовать List как стек\nList<int> stack = [];\n\n/* Поместить элементы в стек */\nstack.add(1);\nstack.add(3);\nstack.add(2);\nstack.add(5);\nstack.add(4);\n\n/* Просмотреть верхний элемент */\nint peek = stack.last;\n\n/* Извлечь элемент */\nint pop = stack.removeLast();\n\n/* Получить длину стека */\nint size = stack.length;\n\n/* Проверить, пуст ли стек */\nbool isEmpty = stack.isEmpty;\n
        stack.rs
        /* Инициализация стека */\n// Используем Vec как стек\nlet mut stack: Vec<i32> = Vec::new();\n\n/* Поместить элементы в стек */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* Просмотреть верхний элемент */\nlet top = stack.last().unwrap();\n\n/* Извлечь элемент */\nlet pop = stack.pop().unwrap();\n\n/* Получить длину стека */\nlet size = stack.len();\n\n/* Проверить, пуст ли стек */\nlet is_empty = stack.is_empty();\n
        stack.c
        // В C нет встроенного стека\n
        stack.kt
        /* Инициализация стека */\nval stack = Stack<Int>()\n\n/* Поместить элементы в стек */\nstack.push(1)\nstack.push(3)\nstack.push(2)\nstack.push(5)\nstack.push(4)\n\n/* Просмотреть верхний элемент */\nval peek = stack.peek()\n\n/* Извлечь элемент */\nval pop = stack.pop()\n\n/* Получить длину стека */\nval size = stack.size\n\n/* Проверить, пуст ли стек */\nval isEmpty = stack.isEmpty()\n
        stack.rb
        # Инициализация стека\n# В Ruby нет встроенного класса стека, поэтому можно использовать Array как стек\nstack = []\n\n# Поместить элементы в стек\nstack << 1\nstack << 3\nstack << 2\nstack << 5\nstack << 4\n\n# Просмотреть верхний элемент\npeek = stack.last\n\n# Извлечь элемент\npop = stack.pop\n\n# Получить длину стека\nsize = stack.length\n\n# Проверить, пуст ли стек\nis_empty = stack.empty?\n
        Визуализация выполнения

        https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%81%D1%82%D0%B5%D0%BA%0A%20%20%20%20%23%20%D0%92%20Python%20%D0%BD%D0%B5%D1%82%20%D0%B2%D1%81%D1%82%D1%80%D0%BE%D0%B5%D0%BD%D0%BD%D0%BE%D0%B3%D0%BE%20%D0%BA%D0%BB%D0%B0%D1%81%D1%81%D0%B0%20%D1%81%D1%82%D0%B5%D0%BA%D0%B0%2C%20%D0%BF%D0%BE%D1%8D%D1%82%D0%BE%D0%BC%D1%83%20list%20%D0%BC%D0%BE%D0%B6%D0%BD%D0%BE%20%D0%B8%D1%81%D0%BF%D0%BE%D0%BB%D1%8C%D0%B7%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D0%BA%D0%B0%D0%BA%20%D1%81%D1%82%D0%B5%D0%BA%0A%20%20%20%20stack%20%3D%20%5B%5D%0A%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BC%D0%B5%D1%81%D1%82%D0%B8%D1%82%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B2%20%D1%81%D1%82%D0%B5%D0%BA%0A%20%20%20%20stack.append%281%29%0A%20%20%20%20stack.append%283%29%0A%20%20%20%20stack.append%282%29%0A%20%20%20%20stack.append%285%29%0A%20%20%20%20stack.append%284%29%0A%20%20%20%20print%28%22%D1%81%D1%82%D0%B5%D0%BA%20stack%20%3D%22%2C%20stack%29%0A%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B8%D1%82%D1%8C%20%D0%B2%D0%B5%D1%80%D1%85%D0%BD%D0%B8%D0%B9%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D1%81%D1%82%D0%B5%D0%BA%D0%B0%0A%20%20%20%20peek%20%3D%20stack%5B-1%5D%0A%20%20%20%20print%28%22%D0%92%D0%B5%D1%80%D1%85%D0%BD%D0%B8%D0%B9%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D1%81%D1%82%D0%B5%D0%BA%D0%B0%20peek%20%3D%22%2C%20peek%29%0A%0A%20%20%20%20%23%20%D0%98%D0%B7%D0%B2%D0%BB%D0%B5%D1%87%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B8%D0%B7%20%D1%81%D1%82%D0%B5%D0%BA%D0%B0%0A%20%20%20%20pop%20%3D%20stack.pop%28%29%0A%20%20%20%20print%28%22%D0%98%D0%B7%D0%B2%D0%BB%D0%B5%D1%87%D0%B5%D0%BD%D0%BD%D1%8B%D0%B9%20%D0%B8%D0%B7%20%D1%81%D1%82%D0%B5%D0%BA%D0%B0%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20pop%20%3D%22%2C%20pop%29%0A%20%20%20%20print%28%22%D0%9F%D0%BE%D1%81%D0%BB%D0%B5%20%D0%B8%D0%B7%D0%B2%D0%BB%D0%B5%D1%87%D0%B5%D0%BD%D0%B8%D1%8F%20stack%20%3D%22%2C%20stack%29%0A%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B8%D1%82%D1%8C%20%D0%B4%D0%BB%D0%B8%D0%BD%D1%83%20%D1%81%D1%82%D0%B5%D0%BA%D0%B0%0A%20%20%20%20size%20%3D%20len%28stack%29%0A%20%20%20%20print%28%22%D0%94%D0%BB%D0%B8%D0%BD%D0%B0%20%D1%81%D1%82%D0%B5%D0%BA%D0%B0%20size%20%3D%22%2C%20size%29%0A%0A%20%20%20%20%23%20%D0%9F%D1%80%D0%BE%D0%B2%D0%B5%D1%80%D0%B8%D1%82%D1%8C%2C%20%D0%BF%D1%83%D1%81%D1%82%D0%B0%20%D0%BB%D0%B8%20%D1%81%D1%82%D1%80%D1%83%D0%BA%D1%82%D1%83%D1%80%D0%B0%0A%20%20%20%20is_empty%20%3D%20len%28stack%29%20%3D%3D%200%0A%20%20%20%20print%28%22%D0%9F%D1%83%D1%81%D1%82%20%D0%BB%D0%B8%20%D1%81%D1%82%D0%B5%D0%BA%20%3D%22%2C%20is_empty%29&cumulative=false&curInstr=2&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

        ","path":["Глава 5. Стек и очередь","5.1   Стек"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#512","level":2,"title":"5.1.2   Реализация стека","text":"

        Чтобы глубже понять механизм работы стека, попробуем самостоятельно реализовать класс стека.

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

        ","path":["Глава 5. Стек и очередь","5.1   Стек"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#1","level":3,"title":"1.   Реализация на основе связного списка","text":"

        Если реализовывать стек на основе связного списка, то головной узел списка можно рассматривать как вершину стека, а хвостовой - как основание.

        Как показано на рисунке 5-2, для операции push достаточно вставить элемент в голову связного списка. Такой способ вставки называется вставкой в голову. Для операции pop достаточно удалить головной узел из списка.

        <1><2><3>

        Рисунок 5-2   Операции push и pop в реализации стека на связном списке

        Ниже приведен пример кода реализации стека на основе связного списка:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linkedlist_stack.py
        class LinkedListStack:\n    \"\"\"Стек на основе связного списка\"\"\"\n\n    def __init__(self):\n        \"\"\"Конструктор\"\"\"\n        self._peek: ListNode | None = None\n        self._size: int = 0\n\n    def size(self) -> int:\n        \"\"\"Получение длины стека\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"Проверка, пуст ли стек\"\"\"\n        return self._size == 0\n\n    def push(self, val: int):\n        \"\"\"Поместить в стек\"\"\"\n        node = ListNode(val)\n        node.next = self._peek\n        self._peek = node\n        self._size += 1\n\n    def pop(self) -> int:\n        \"\"\"Извлечь из стека\"\"\"\n        num = self.peek()\n        self._peek = self._peek.next\n        self._size -= 1\n        return num\n\n    def peek(self) -> int:\n        \"\"\"Доступ к верхнему элементу стека\"\"\"\n        if self.is_empty():\n            raise IndexError(\"стек пуст\")\n        return self._peek.val\n\n    def to_list(self) -> list[int]:\n        \"\"\"Преобразовать в список для вывода\"\"\"\n        arr = []\n        node = self._peek\n        while node:\n            arr.append(node.val)\n            node = node.next\n        arr.reverse()\n        return arr\n
        linkedlist_stack.cpp
        /* Стек на основе связного списка */\nclass LinkedListStack {\n  private:\n    ListNode *stackTop; // Использовать головной узел как вершину стека\n    int stkSize;        // Длина стека\n\n  public:\n    LinkedListStack() {\n        stackTop = nullptr;\n        stkSize = 0;\n    }\n\n    ~LinkedListStack() {\n        // Обходить связный список, удалять узлы и освобождать память\n        freeMemoryLinkedList(stackTop);\n    }\n\n    /* Получение длины стека */\n    int size() {\n        return stkSize;\n    }\n\n    /* Проверка, пуст ли стек */\n    bool isEmpty() {\n        return size() == 0;\n    }\n\n    /* Поместить в стек */\n    void push(int num) {\n        ListNode *node = new ListNode(num);\n        node->next = stackTop;\n        stackTop = node;\n        stkSize++;\n    }\n\n    /* Извлечь из стека */\n    int pop() {\n        int num = top();\n        ListNode *tmp = stackTop;\n        stackTop = stackTop->next;\n        // Освободить память\n        delete tmp;\n        stkSize--;\n        return num;\n    }\n\n    /* Доступ к верхнему элементу стека */\n    int top() {\n        if (isEmpty())\n            throw out_of_range(\"стек пуст\");\n        return stackTop->val;\n    }\n\n    /* Преобразовать List в Array и вернуть */\n    vector<int> toVector() {\n        ListNode *node = stackTop;\n        vector<int> res(size());\n        for (int i = res.size() - 1; i >= 0; i--) {\n            res[i] = node->val;\n            node = node->next;\n        }\n        return res;\n    }\n};\n
        linkedlist_stack.java
        /* Стек на основе связного списка */\nclass LinkedListStack {\n    private ListNode stackPeek; // Использовать головной узел как вершину стека\n    private int stkSize = 0; // Длина стека\n\n    public LinkedListStack() {\n        stackPeek = null;\n    }\n\n    /* Получение длины стека */\n    public int size() {\n        return stkSize;\n    }\n\n    /* Проверка, пуст ли стек */\n    public boolean isEmpty() {\n        return size() == 0;\n    }\n\n    /* Поместить в стек */\n    public void push(int num) {\n        ListNode node = new ListNode(num);\n        node.next = stackPeek;\n        stackPeek = node;\n        stkSize++;\n    }\n\n    /* Извлечь из стека */\n    public int pop() {\n        int num = peek();\n        stackPeek = stackPeek.next;\n        stkSize--;\n        return num;\n    }\n\n    /* Доступ к верхнему элементу стека */\n    public int peek() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return stackPeek.val;\n    }\n\n    /* Преобразовать List в Array и вернуть */\n    public int[] toArray() {\n        ListNode node = stackPeek;\n        int[] res = new int[size()];\n        for (int i = res.length - 1; i >= 0; i--) {\n            res[i] = node.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
        linkedlist_stack.cs
        /* Стек на основе связного списка */\nclass LinkedListStack {\n    ListNode? stackPeek;  // Использовать головной узел как вершину стека\n    int stkSize = 0;   // Длина стека\n\n    public LinkedListStack() {\n        stackPeek = null;\n    }\n\n    /* Получение длины стека */\n    public int Size() {\n        return stkSize;\n    }\n\n    /* Проверка, пуст ли стек */\n    public bool IsEmpty() {\n        return Size() == 0;\n    }\n\n    /* Поместить в стек */\n    public void Push(int num) {\n        ListNode node = new(num) {\n            next = stackPeek\n        };\n        stackPeek = node;\n        stkSize++;\n    }\n\n    /* Извлечь из стека */\n    public int Pop() {\n        int num = Peek();\n        stackPeek = stackPeek!.next;\n        stkSize--;\n        return num;\n    }\n\n    /* Доступ к верхнему элементу стека */\n    public int Peek() {\n        if (IsEmpty())\n            throw new Exception();\n        return stackPeek!.val;\n    }\n\n    /* Преобразовать List в Array и вернуть */\n    public int[] ToArray() {\n        if (stackPeek == null)\n            return [];\n\n        ListNode? node = stackPeek;\n        int[] res = new int[Size()];\n        for (int i = res.Length - 1; i >= 0; i--) {\n            res[i] = node!.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
        linkedlist_stack.go
        /* Стек на основе связного списка */\ntype linkedListStack struct {\n    // Использовать встроенный пакет list для реализации стека\n    data *list.List\n}\n\n/* Инициализация стека */\nfunc newLinkedListStack() *linkedListStack {\n    return &linkedListStack{\n        data: list.New(),\n    }\n}\n\n/* Поместить в стек */\nfunc (s *linkedListStack) push(value int) {\n    s.data.PushBack(value)\n}\n\n/* Извлечь из стека */\nfunc (s *linkedListStack) pop() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Back()\n    s.data.Remove(e)\n    return e.Value\n}\n\n/* Доступ к верхнему элементу стека */\nfunc (s *linkedListStack) peek() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Back()\n    return e.Value\n}\n\n/* Получение длины стека */\nfunc (s *linkedListStack) size() int {\n    return s.data.Len()\n}\n\n/* Проверка, пуст ли стек */\nfunc (s *linkedListStack) isEmpty() bool {\n    return s.data.Len() == 0\n}\n\n/* Получить List для вывода */\nfunc (s *linkedListStack) toList() *list.List {\n    return s.data\n}\n
        linkedlist_stack.swift
        /* Стек на основе связного списка */\nclass LinkedListStack {\n    private var _peek: ListNode? // Использовать головной узел как вершину стека\n    private var _size: Int // Длина стека\n\n    init() {\n        _size = 0\n    }\n\n    /* Получение длины стека */\n    func size() -> Int {\n        _size\n    }\n\n    /* Проверка, пуст ли стек */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* Поместить в стек */\n    func push(num: Int) {\n        let node = ListNode(x: num)\n        node.next = _peek\n        _peek = node\n        _size += 1\n    }\n\n    /* Извлечь из стека */\n    @discardableResult\n    func pop() -> Int {\n        let num = peek()\n        _peek = _peek?.next\n        _size -= 1\n        return num\n    }\n\n    /* Доступ к верхнему элементу стека */\n    func peek() -> Int {\n        if isEmpty() {\n            fatalError(\"стек пуст\")\n        }\n        return _peek!.val\n    }\n\n    /* Преобразовать List в Array и вернуть */\n    func toArray() -> [Int] {\n        var node = _peek\n        var res = Array(repeating: 0, count: size())\n        for i in res.indices.reversed() {\n            res[i] = node!.val\n            node = node?.next\n        }\n        return res\n    }\n}\n
        linkedlist_stack.js
        /* Стек на основе связного списка */\nclass LinkedListStack {\n    #stackPeek; // Использовать головной узел как вершину стека\n    #stkSize = 0; // Длина стека\n\n    constructor() {\n        this.#stackPeek = null;\n    }\n\n    /* Получение длины стека */\n    get size() {\n        return this.#stkSize;\n    }\n\n    /* Проверка, пуст ли стек */\n    isEmpty() {\n        return this.size === 0;\n    }\n\n    /* Поместить в стек */\n    push(num) {\n        const node = new ListNode(num);\n        node.next = this.#stackPeek;\n        this.#stackPeek = node;\n        this.#stkSize++;\n    }\n\n    /* Извлечь из стека */\n    pop() {\n        const num = this.peek();\n        this.#stackPeek = this.#stackPeek.next;\n        this.#stkSize--;\n        return num;\n    }\n\n    /* Доступ к верхнему элементу стека */\n    peek() {\n        if (!this.#stackPeek) throw new Error('стек пуст');\n        return this.#stackPeek.val;\n    }\n\n    /* Преобразовать связный список в Array и вернуть */\n    toArray() {\n        let node = this.#stackPeek;\n        const res = new Array(this.size);\n        for (let i = res.length - 1; i >= 0; i--) {\n            res[i] = node.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
        linkedlist_stack.ts
        /* Стек на основе связного списка */\nclass LinkedListStack {\n    private stackPeek: ListNode | null; // Использовать головной узел как вершину стека\n    private stkSize: number = 0; // Длина стека\n\n    constructor() {\n        this.stackPeek = null;\n    }\n\n    /* Получение длины стека */\n    get size(): number {\n        return this.stkSize;\n    }\n\n    /* Проверка, пуст ли стек */\n    isEmpty(): boolean {\n        return this.size === 0;\n    }\n\n    /* Поместить в стек */\n    push(num: number): void {\n        const node = new ListNode(num);\n        node.next = this.stackPeek;\n        this.stackPeek = node;\n        this.stkSize++;\n    }\n\n    /* Извлечь из стека */\n    pop(): number {\n        const num = this.peek();\n        if (!this.stackPeek) throw new Error('стек пуст');\n        this.stackPeek = this.stackPeek.next;\n        this.stkSize--;\n        return num;\n    }\n\n    /* Доступ к верхнему элементу стека */\n    peek(): number {\n        if (!this.stackPeek) throw new Error('стек пуст');\n        return this.stackPeek.val;\n    }\n\n    /* Преобразовать связный список в Array и вернуть */\n    toArray(): number[] {\n        let node = this.stackPeek;\n        const res = new Array<number>(this.size);\n        for (let i = res.length - 1; i >= 0; i--) {\n            res[i] = node!.val;\n            node = node!.next;\n        }\n        return res;\n    }\n}\n
        linkedlist_stack.dart
        /* Стек на основе класса связного списка */\nclass LinkedListStack {\n  ListNode? _stackPeek; // Использовать головной узел как вершину стека\n  int _stkSize = 0; // Длина стека\n\n  LinkedListStack() {\n    _stackPeek = null;\n  }\n\n  /* Получение длины стека */\n  int size() {\n    return _stkSize;\n  }\n\n  /* Проверка, пуст ли стек */\n  bool isEmpty() {\n    return _stkSize == 0;\n  }\n\n  /* Поместить в стек */\n  void push(int _num) {\n    final ListNode node = ListNode(_num);\n    node.next = _stackPeek;\n    _stackPeek = node;\n    _stkSize++;\n  }\n\n  /* Извлечь из стека */\n  int pop() {\n    final int _num = peek();\n    _stackPeek = _stackPeek!.next;\n    _stkSize--;\n    return _num;\n  }\n\n  /* Доступ к верхнему элементу стека */\n  int peek() {\n    if (_stackPeek == null) {\n      throw Exception(\"стек пуст\");\n    }\n    return _stackPeek!.val;\n  }\n\n  /* Преобразовать связный список в List и вернуть */\n  List<int> toList() {\n    ListNode? node = _stackPeek;\n    List<int> list = [];\n    while (node != null) {\n      list.add(node.val);\n      node = node.next;\n    }\n    list = list.reversed.toList();\n    return list;\n  }\n}\n
        linkedlist_stack.rs
        /* Стек на основе связного списка */\n#[allow(dead_code)]\npub struct LinkedListStack<T> {\n    stack_peek: Option<Rc<RefCell<ListNode<T>>>>, // Использовать головной узел как вершину стека\n    stk_size: usize,                              // Длина стека\n}\n\nimpl<T: Copy> LinkedListStack<T> {\n    pub fn new() -> Self {\n        Self {\n            stack_peek: None,\n            stk_size: 0,\n        }\n    }\n\n    /* Получение длины стека */\n    pub fn size(&self) -> usize {\n        return self.stk_size;\n    }\n\n    /* Проверка, пуст ли стек */\n    pub fn is_empty(&self) -> bool {\n        return self.size() == 0;\n    }\n\n    /* Поместить в стек */\n    pub fn push(&mut self, num: T) {\n        let node = ListNode::new(num);\n        node.borrow_mut().next = self.stack_peek.take();\n        self.stack_peek = Some(node);\n        self.stk_size += 1;\n    }\n\n    /* Извлечь из стека */\n    pub fn pop(&mut self) -> Option<T> {\n        self.stack_peek.take().map(|old_head| {\n            self.stack_peek = old_head.borrow_mut().next.take();\n            self.stk_size -= 1;\n\n            old_head.borrow().val\n        })\n    }\n\n    /* Доступ к верхнему элементу стека */\n    pub fn peek(&self) -> Option<&Rc<RefCell<ListNode<T>>>> {\n        self.stack_peek.as_ref()\n    }\n\n    /* Преобразовать List в Array и вернуть */\n    pub fn to_array(&self) -> Vec<T> {\n        fn _to_array<T: Sized + Copy>(head: Option<&Rc<RefCell<ListNode<T>>>>) -> Vec<T> {\n            if let Some(node) = head {\n                let mut nums = _to_array(node.borrow().next.as_ref());\n                nums.push(node.borrow().val);\n                return nums;\n            }\n            return Vec::new();\n        }\n\n        _to_array(self.peek())\n    }\n}\n
        linkedlist_stack.c
        /* Стек на основе связного списка */\ntypedef struct {\n    ListNode *top; // Использовать головной узел как вершину стека\n    int size;      // Длина стека\n} LinkedListStack;\n\n/* Конструктор */\nLinkedListStack *newLinkedListStack() {\n    LinkedListStack *s = malloc(sizeof(LinkedListStack));\n    s->top = NULL;\n    s->size = 0;\n    return s;\n}\n\n/* Деструктор */\nvoid delLinkedListStack(LinkedListStack *s) {\n    while (s->top) {\n        ListNode *n = s->top->next;\n        free(s->top);\n        s->top = n;\n    }\n    free(s);\n}\n\n/* Получение длины стека */\nint size(LinkedListStack *s) {\n    return s->size;\n}\n\n/* Проверка, пуст ли стек */\nbool isEmpty(LinkedListStack *s) {\n    return size(s) == 0;\n}\n\n/* Поместить в стек */\nvoid push(LinkedListStack *s, int num) {\n    ListNode *node = (ListNode *)malloc(sizeof(ListNode));\n    node->next = s->top; // Обновить поле указателя нового узла\n    node->val = num;     // Обновить поле данных нового узла\n    s->top = node;       // Обновить вершину стека\n    s->size++;           // Обновить размер стека\n}\n\n/* Доступ к верхнему элементу стека */\nint peek(LinkedListStack *s) {\n    if (s->size == 0) {\n        printf(\"стек пуст\\n\");\n        return INT_MAX;\n    }\n    return s->top->val;\n}\n\n/* Извлечь из стека */\nint pop(LinkedListStack *s) {\n    int val = peek(s);\n    ListNode *tmp = s->top;\n    s->top = s->top->next;\n    // Освободить память\n    free(tmp);\n    s->size--;\n    return val;\n}\n
        linkedlist_stack.kt
        /* Стек на основе связного списка */\nclass LinkedListStack(\n    private var stackPeek: ListNode? = null, // Использовать головной узел как вершину стека\n    private var stkSize: Int = 0 // Длина стека\n) {\n\n    /* Получение длины стека */\n    fun size(): Int {\n        return stkSize\n    }\n\n    /* Проверка, пуст ли стек */\n    fun isEmpty(): Boolean {\n        return size() == 0\n    }\n\n    /* Поместить в стек */\n    fun push(num: Int) {\n        val node = ListNode(num)\n        node.next = stackPeek\n        stackPeek = node\n        stkSize++\n    }\n\n    /* Извлечь из стека */\n    fun pop(): Int? {\n        val num = peek()\n        stackPeek = stackPeek?.next\n        stkSize--\n        return num\n    }\n\n    /* Доступ к верхнему элементу стека */\n    fun peek(): Int? {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return stackPeek?._val\n    }\n\n    /* Преобразовать List в Array и вернуть */\n    fun toArray(): IntArray {\n        var node = stackPeek\n        val res = IntArray(size())\n        for (i in res.size - 1 downTo 0) {\n            res[i] = node?._val!!\n            node = node.next\n        }\n        return res\n    }\n}\n
        linkedlist_stack.rb
        ### Стек на основе связного списка ###\nclass LinkedListStack\n  attr_reader :size\n\n  ### Конструктор ###\n  def initialize\n    @size = 0\n  end\n\n  ### Проверка, пуст ли стек ###\n  def is_empty?\n    @peek.nil?\n  end\n\n  ### Помещение в стек ###\n  def push(val)\n    node = ListNode.new(val)\n    node.next = @peek\n    @peek = node\n    @size += 1\n  end\n\n  ### Извлечение из стека ###\n  def pop\n    num = peek\n    @peek = @peek.next\n    @size -= 1\n    num\n  end\n\n  ### Доступ к верхнему элементу стека ###\n  def peek\n    raise IndexError, 'стек пуст' if is_empty?\n\n    @peek.val\n  end\n\n  ### Преобразовать связный список в Array и вернуть ###\n  def to_array\n    arr = []\n    node = @peek\n    while node\n      arr << node.val\n      node = node.next\n    end\n    arr.reverse\n  end\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 5. Стек и очередь","5.1   Стек"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#2","level":3,"title":"2.   Реализация на основе массива","text":"

        Если реализовывать стек на основе массива, то хвост массива можно рассматривать как вершину стека. Как показано на рисунке 5-3, операции push и pop соответствуют добавлению элемента в конец массива и удалению элемента из конца, обе имеют временную сложность \\(O(1)\\) .

        <1><2><3>

        Рисунок 5-3   Операции push и pop в реализации стека на массиве

        Поскольку количество элементов, помещаемых в стек, может непрерывно расти, мы можем использовать динамический массив и тем самым не заниматься расширением массива вручную. Ниже приведен пример кода:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_stack.py
        class ArrayStack:\n    \"\"\"Стек на основе массива\"\"\"\n\n    def __init__(self):\n        \"\"\"Конструктор\"\"\"\n        self._stack: list[int] = []\n\n    def size(self) -> int:\n        \"\"\"Получение длины стека\"\"\"\n        return len(self._stack)\n\n    def is_empty(self) -> bool:\n        \"\"\"Проверка, пуст ли стек\"\"\"\n        return self.size() == 0\n\n    def push(self, item: int):\n        \"\"\"Поместить в стек\"\"\"\n        self._stack.append(item)\n\n    def pop(self) -> int:\n        \"\"\"Извлечь из стека\"\"\"\n        if self.is_empty():\n            raise IndexError(\"стек пуст\")\n        return self._stack.pop()\n\n    def peek(self) -> int:\n        \"\"\"Доступ к верхнему элементу стека\"\"\"\n        if self.is_empty():\n            raise IndexError(\"стек пуст\")\n        return self._stack[-1]\n\n    def to_list(self) -> list[int]:\n        \"\"\"Вернуть список для вывода\"\"\"\n        return self._stack\n
        array_stack.cpp
        /* Стек на основе массива */\nclass ArrayStack {\n  private:\n    vector<int> stack;\n\n  public:\n    /* Получение длины стека */\n    int size() {\n        return stack.size();\n    }\n\n    /* Проверка, пуст ли стек */\n    bool isEmpty() {\n        return stack.size() == 0;\n    }\n\n    /* Поместить в стек */\n    void push(int num) {\n        stack.push_back(num);\n    }\n\n    /* Извлечь из стека */\n    int pop() {\n        int num = top();\n        stack.pop_back();\n        return num;\n    }\n\n    /* Доступ к верхнему элементу стека */\n    int top() {\n        if (isEmpty())\n            throw out_of_range(\"стек пуст\");\n        return stack.back();\n    }\n\n    /* Вернуть Vector */\n    vector<int> toVector() {\n        return stack;\n    }\n};\n
        array_stack.java
        /* Стек на основе массива */\nclass ArrayStack {\n    private ArrayList<Integer> stack;\n\n    public ArrayStack() {\n        // Инициализация списка (динамического массива)\n        stack = new ArrayList<>();\n    }\n\n    /* Получение длины стека */\n    public int size() {\n        return stack.size();\n    }\n\n    /* Проверка, пуст ли стек */\n    public boolean isEmpty() {\n        return size() == 0;\n    }\n\n    /* Поместить в стек */\n    public void push(int num) {\n        stack.add(num);\n    }\n\n    /* Извлечь из стека */\n    public int pop() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return stack.remove(size() - 1);\n    }\n\n    /* Доступ к верхнему элементу стека */\n    public int peek() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return stack.get(size() - 1);\n    }\n\n    /* Преобразовать List в Array и вернуть */\n    public Object[] toArray() {\n        return stack.toArray();\n    }\n}\n
        array_stack.cs
        /* Стек на основе массива */\nclass ArrayStack {\n    List<int> stack;\n    public ArrayStack() {\n        // Инициализация списка (динамического массива)\n        stack = [];\n    }\n\n    /* Получение длины стека */\n    public int Size() {\n        return stack.Count;\n    }\n\n    /* Проверка, пуст ли стек */\n    public bool IsEmpty() {\n        return Size() == 0;\n    }\n\n    /* Поместить в стек */\n    public void Push(int num) {\n        stack.Add(num);\n    }\n\n    /* Извлечь из стека */\n    public int Pop() {\n        if (IsEmpty())\n            throw new Exception();\n        var val = Peek();\n        stack.RemoveAt(Size() - 1);\n        return val;\n    }\n\n    /* Доступ к верхнему элементу стека */\n    public int Peek() {\n        if (IsEmpty())\n            throw new Exception();\n        return stack[Size() - 1];\n    }\n\n    /* Преобразовать List в Array и вернуть */\n    public int[] ToArray() {\n        return [.. stack];\n    }\n}\n
        array_stack.go
        /* Стек на основе массива */\ntype arrayStack struct {\n    data []int // Данные\n}\n\n/* Инициализация стека */\nfunc newArrayStack() *arrayStack {\n    return &arrayStack{\n        // Установить длину стека равной 0, а емкость равной 16\n        data: make([]int, 0, 16),\n    }\n}\n\n/* Длина стека */\nfunc (s *arrayStack) size() int {\n    return len(s.data)\n}\n\n/* Пуст ли стек */\nfunc (s *arrayStack) isEmpty() bool {\n    return s.size() == 0\n}\n\n/* Поместить в стек */\nfunc (s *arrayStack) push(v int) {\n    // Срез автоматически расширяется\n    s.data = append(s.data, v)\n}\n\n/* Извлечь из стека */\nfunc (s *arrayStack) pop() any {\n    val := s.peek()\n    s.data = s.data[:len(s.data)-1]\n    return val\n}\n\n/* Получить элемент на вершине стека */\nfunc (s *arrayStack) peek() any {\n    if s.isEmpty() {\n        return nil\n    }\n    val := s.data[len(s.data)-1]\n    return val\n}\n\n/* Получить Slice для вывода */\nfunc (s *arrayStack) toSlice() []int {\n    return s.data\n}\n
        array_stack.swift
        /* Стек на основе массива */\nclass ArrayStack {\n    private var stack: [Int]\n\n    init() {\n        // Инициализация списка (динамического массива)\n        stack = []\n    }\n\n    /* Получение длины стека */\n    func size() -> Int {\n        stack.count\n    }\n\n    /* Проверка, пуст ли стек */\n    func isEmpty() -> Bool {\n        stack.isEmpty\n    }\n\n    /* Поместить в стек */\n    func push(num: Int) {\n        stack.append(num)\n    }\n\n    /* Извлечь из стека */\n    @discardableResult\n    func pop() -> Int {\n        if isEmpty() {\n            fatalError(\"стек пуст\")\n        }\n        return stack.removeLast()\n    }\n\n    /* Доступ к верхнему элементу стека */\n    func peek() -> Int {\n        if isEmpty() {\n            fatalError(\"стек пуст\")\n        }\n        return stack.last!\n    }\n\n    /* Преобразовать List в Array и вернуть */\n    func toArray() -> [Int] {\n        stack\n    }\n}\n
        array_stack.js
        /* Стек на основе массива */\nclass ArrayStack {\n    #stack;\n    constructor() {\n        this.#stack = [];\n    }\n\n    /* Получение длины стека */\n    get size() {\n        return this.#stack.length;\n    }\n\n    /* Проверка, пуст ли стек */\n    isEmpty() {\n        return this.#stack.length === 0;\n    }\n\n    /* Поместить в стек */\n    push(num) {\n        this.#stack.push(num);\n    }\n\n    /* Извлечь из стека */\n    pop() {\n        if (this.isEmpty()) throw new Error('стек пуст');\n        return this.#stack.pop();\n    }\n\n    /* Доступ к верхнему элементу стека */\n    top() {\n        if (this.isEmpty()) throw new Error('стек пуст');\n        return this.#stack[this.#stack.length - 1];\n    }\n\n    /* Вернуть Array */\n    toArray() {\n        return this.#stack;\n    }\n}\n
        array_stack.ts
        /* Стек на основе массива */\nclass ArrayStack {\n    private stack: number[];\n    constructor() {\n        this.stack = [];\n    }\n\n    /* Получение длины стека */\n    get size(): number {\n        return this.stack.length;\n    }\n\n    /* Проверка, пуст ли стек */\n    isEmpty(): boolean {\n        return this.stack.length === 0;\n    }\n\n    /* Поместить в стек */\n    push(num: number): void {\n        this.stack.push(num);\n    }\n\n    /* Извлечь из стека */\n    pop(): number | undefined {\n        if (this.isEmpty()) throw new Error('стек пуст');\n        return this.stack.pop();\n    }\n\n    /* Доступ к верхнему элементу стека */\n    top(): number | undefined {\n        if (this.isEmpty()) throw new Error('стек пуст');\n        return this.stack[this.stack.length - 1];\n    }\n\n    /* Вернуть Array */\n    toArray() {\n        return this.stack;\n    }\n}\n
        array_stack.dart
        /* Стек на основе массива */\nclass ArrayStack {\n  late List<int> _stack;\n  ArrayStack() {\n    _stack = [];\n  }\n\n  /* Получение длины стека */\n  int size() {\n    return _stack.length;\n  }\n\n  /* Проверка, пуст ли стек */\n  bool isEmpty() {\n    return _stack.isEmpty;\n  }\n\n  /* Поместить в стек */\n  void push(int _num) {\n    _stack.add(_num);\n  }\n\n  /* Извлечь из стека */\n  int pop() {\n    if (isEmpty()) {\n      throw Exception(\"стек пуст\");\n    }\n    return _stack.removeLast();\n  }\n\n  /* Доступ к верхнему элементу стека */\n  int peek() {\n    if (isEmpty()) {\n      throw Exception(\"стек пуст\");\n    }\n    return _stack.last;\n  }\n\n  /* Преобразовать стек в Array и вернуть */\n  List<int> toArray() => _stack;\n}\n
        array_stack.rs
        /* Стек на основе массива */\nstruct ArrayStack<T> {\n    stack: Vec<T>,\n}\n\nimpl<T> ArrayStack<T> {\n    /* Инициализация стека */\n    fn new() -> ArrayStack<T> {\n        ArrayStack::<T> {\n            stack: Vec::<T>::new(),\n        }\n    }\n\n    /* Получение длины стека */\n    fn size(&self) -> usize {\n        self.stack.len()\n    }\n\n    /* Проверка, пуст ли стек */\n    fn is_empty(&self) -> bool {\n        self.size() == 0\n    }\n\n    /* Поместить в стек */\n    fn push(&mut self, num: T) {\n        self.stack.push(num);\n    }\n\n    /* Извлечь из стека */\n    fn pop(&mut self) -> Option<T> {\n        self.stack.pop()\n    }\n\n    /* Доступ к верхнему элементу стека */\n    fn peek(&self) -> Option<&T> {\n        if self.is_empty() {\n            panic!(\"стек пуст\")\n        };\n        self.stack.last()\n    }\n\n    /* Вернуть &Vec */\n    fn to_array(&self) -> &Vec<T> {\n        &self.stack\n    }\n}\n
        array_stack.c
        /* Стек на основе массива */\ntypedef struct {\n    int *data;\n    int size;\n} ArrayStack;\n\n/* Конструктор */\nArrayStack *newArrayStack() {\n    ArrayStack *stack = malloc(sizeof(ArrayStack));\n    // Инициализировать большую вместимость, чтобы избежать расширения\n    stack->data = malloc(sizeof(int) * MAX_SIZE);\n    stack->size = 0;\n    return stack;\n}\n\n/* Деструктор */\nvoid delArrayStack(ArrayStack *stack) {\n    free(stack->data);\n    free(stack);\n}\n\n/* Получение длины стека */\nint size(ArrayStack *stack) {\n    return stack->size;\n}\n\n/* Проверка, пуст ли стек */\nbool isEmpty(ArrayStack *stack) {\n    return stack->size == 0;\n}\n\n/* Поместить в стек */\nvoid push(ArrayStack *stack, int num) {\n    if (stack->size == MAX_SIZE) {\n        printf(\"Стек заполнен\\n\");\n        return;\n    }\n    stack->data[stack->size] = num;\n    stack->size++;\n}\n\n/* Доступ к верхнему элементу стека */\nint peek(ArrayStack *stack) {\n    if (stack->size == 0) {\n        printf(\"стек пуст\\n\");\n        return INT_MAX;\n    }\n    return stack->data[stack->size - 1];\n}\n\n/* Извлечь из стека */\nint pop(ArrayStack *stack) {\n    int val = peek(stack);\n    stack->size--;\n    return val;\n}\n
        array_stack.kt
        /* Стек на основе массива */\nclass ArrayStack {\n    // Инициализация списка (динамического массива)\n    private val stack = mutableListOf<Int>()\n\n    /* Получение длины стека */\n    fun size(): Int {\n        return stack.size\n    }\n\n    /* Проверка, пуст ли стек */\n    fun isEmpty(): Boolean {\n        return size() == 0\n    }\n\n    /* Поместить в стек */\n    fun push(num: Int) {\n        stack.add(num)\n    }\n\n    /* Извлечь из стека */\n    fun pop(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return stack.removeAt(size() - 1)\n    }\n\n    /* Доступ к верхнему элементу стека */\n    fun peek(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return stack[size() - 1]\n    }\n\n    /* Преобразовать List в Array и вернуть */\n    fun toArray(): Array<Any> {\n        return stack.toTypedArray()\n    }\n}\n
        array_stack.rb
        ### Стек на основе массива ###\nclass ArrayStack\n  ### Конструктор ###\n  def initialize\n    @stack = []\n  end\n\n  ### Получить длину стека ###\n  def size\n    @stack.length\n  end\n\n  ### Проверка, пуст ли стек ###\n  def is_empty?\n    @stack.empty?\n  end\n\n  ### Помещение в стек ###\n  def push(item)\n    @stack << item\n  end\n\n  ### Извлечение из стека ###\n  def pop\n    raise IndexError, 'стек пуст' if is_empty?\n\n    @stack.pop\n  end\n\n  ### Доступ к верхнему элементу стека ###\n  def peek\n    raise IndexError, 'стек пуст' if is_empty?\n\n    @stack.last\n  end\n\n  ### Вернуть список для вывода ###\n  def to_array\n    @stack\n  end\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 5. Стек и очередь","5.1   Стек"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#513","level":2,"title":"5.1.3   Сравнение двух реализаций","text":"

        Поддерживаемые операции

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

        Временная эффективность

        В реализации на массиве и push , и pop выполняются в заранее выделенной непрерывной памяти, которая хорошо использует локальность кэша, поэтому такие операции обычно эффективнее. Однако если при push емкость массива оказывается превышена, включается механизм расширения, и временная сложность именно этой операции становится \\(O(n)\\) .

        В реализации на связном списке расширение выполняется очень гибко, и проблемы падения эффективности из-за расширения массива здесь нет. Но сама операция push требует инициализации объекта-узла и изменения указателей, поэтому в среднем она немного менее эффективна. Впрочем, если помещаемые в стек элементы уже сами являются объектами-узлами, шаг инициализации можно пропустить и тем самым повысить эффективность.

        Итак, когда элементами, помещаемыми и извлекаемыми из стека, являются базовые типы данных, например int или double , можно сделать следующие выводы.

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

        Пространственная эффективность

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

        Однако, поскольку узлы связного списка должны дополнительно хранить указатели, узлы списка сами по себе занимают больше пространства.

        В итоге нельзя просто сказать, какая из реализаций более экономна по памяти. Это нужно анализировать в контексте конкретной задачи.

        ","path":["Глава 5. Стек и очередь","5.1   Стек"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#514","level":2,"title":"5.1.4   Типичные применения стека","text":"
        • Кнопки «назад» и «вперед» в браузере, undo и redo в программах. Каждый раз, когда мы открываем новую страницу, браузер помещает предыдущую страницу в стек, чтобы по операции «назад» можно было вернуться к ней. Операция «назад» по сути является pop . Если нужно одновременно поддерживать и «назад», и «вперед», то обычно используются два стека.
        • Управление памятью программы. Каждый раз при вызове функции система помещает на вершину стека стековый кадр, в котором хранится контекст функции. В рекурсивной функции на этапе углубления рекурсии непрерывно выполняются операции push , а на этапе возврата - операции pop .
        ","path":["Глава 5. Стек и очередь","5.1   Стек"],"tags":[]},{"location":"chapter_stack_and_queue/summary/","level":1,"title":"5.4   Резюме","text":"","path":["Глава 5. Стек и очередь","5.4   Резюме"],"tags":[]},{"location":"chapter_stack_and_queue/summary/#1","level":3,"title":"1.   Основные выводы","text":"
        • Стек - это структура данных, следующая правилу «последним пришел - первым вышел», и его можно реализовать с помощью массива или связного списка.
        • С точки зрения временной эффективности реализация стека на массиве обычно работает быстрее в среднем, но во время расширения емкости временная сложность отдельной операции push может ухудшаться до \\(O(n)\\) . Напротив, реализация стека на связном списке дает более стабильные характеристики.
        • С точки зрения использования памяти реализация стека на массиве может приводить к некоторой потере пространства. Однако следует учитывать, что узлы связного списка занимают больше памяти, чем элементы массива.
        • Очередь - это структура данных, следующая правилу «первым пришел - первым вышел», и ее также можно реализовать с помощью массива или связного списка. Сравнение временной и пространственной эффективности для очереди в целом приводит к тем же выводам, что и для стека.
        • Двусторонняя очередь - это очередь с более высокой степенью свободы, которая позволяет добавлять и удалять элементы с обоих концов.
        ","path":["Глава 5. Стек и очередь","5.4   Резюме"],"tags":[]},{"location":"chapter_stack_and_queue/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

        Q: Реализованы ли кнопки «вперед» и «назад» в браузере с помощью двусвязного списка?

        По сути, функция переходов «вперед/назад» в браузере отражает логику стека. Когда пользователь открывает новую страницу, она помещается на вершину стека. Когда пользователь нажимает кнопку «назад», эта страница снимается с вершины стека. Двусторонняя очередь позволяет удобно реализовать некоторые дополнительные операции, об этом уже упоминалось в разделе «Двусторонняя очередь».

        Q: Нужно ли освобождать память узла после извлечения его из стека?

        Если извлеченный узел еще понадобится, память освобождать не нужно. Если он больше не нужен, то в языках Java и Python есть автоматический сборщик мусора, поэтому ручное освобождение памяти не требуется. В C и C++ память нужно освобождать вручную.

        Q: Двусторонняя очередь выглядит как два соединенных стека. Для чего она нужна?

        Двусторонняя очередь похожа на комбинацию стека и очереди или на два соединенных стека. Она объединяет логику обеих структур, поэтому может покрыть все их применения и при этом остается более гибкой.

        Q: Как именно реализуются отмена (undo) и повтор (redo)?

        Используются два стека: стек A для отмены и стек B для повтора.

        1. Каждый раз, когда пользователь выполняет действие, это действие помещается в стек A , а стек B очищается.
        2. Когда пользователь выполняет «undo», последнее действие извлекается из стека A и помещается в стек B .
        3. Когда пользователь выполняет «redo», последнее действие извлекается из стека B и помещается обратно в стек A .
        ","path":["Глава 5. Стек и очередь","5.4   Резюме"],"tags":[]},{"location":"chapter_tree/","level":1,"title":"Глава 7.   Деревья","text":"

        Abstract

        Высокое дерево полно жизни: мощные корни, густая листва и раскидистые ветви.

        Оно наглядно показывает нам живую форму данных, построенную на принципе «разделяй и властвуй».

        ","path":["Глава 7. Деревья","Глава 7.   Деревья"],"tags":[]},{"location":"chapter_tree/#_1","level":2,"title":"Содержание главы","text":"
        • 7.1   Двоичное дерево
        • 7.2   Обход двоичного дерева
        • 7.3   Представление двоичного дерева массивом
        • 7.4   Двоичное дерево поиска
        • 7.5   AVL-дерево *
        • 7.6   Краткие итоги
        ","path":["Глава 7. Деревья","Глава 7.   Деревья"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/","level":1,"title":"7.3   Представление двоичного дерева массивом","text":"

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

        Возникает вопрос: можно ли представить двоичное дерево с помощью массива? Ответ: да.

        ","path":["Глава 7. Деревья","7.3   Представление двоичного дерева массивом"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/#731","level":2,"title":"7.3.1   Представление идеального двоичного дерева","text":"

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

        Из свойств обхода по уровням можно вывести формулу соответствия между индексом родителя и индексами дочерних узлов: если индекс некоторого узла равен \\(i\\) , то индекс его левого дочернего узла равен \\(2i + 1\\) , а правого - \\(2i + 2\\) . На рисунке 7-12 показано соответствие между индексами разных узлов.

        Рисунок 7-12   Представление идеального двоичного дерева массивом

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

        ","path":["Глава 7. Деревья","7.3   Представление двоичного дерева массивом"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/#732","level":2,"title":"7.3.2   Представление произвольного двоичного дерева","text":"

        Идеальное двоичное дерево - лишь частный случай. В обычной двоичной структуре на промежуточных уровнях часто существует множество None . Поскольку последовательность обхода по уровням не содержит этих None , мы не можем по одной лишь этой последовательности определить их количество и расположение. Это означает, что одному и тому же обходу по уровням может соответствовать сразу несколько различных структур двоичного дерева.

        Как показано на рисунке 7-13, для неполной двоичной структуры описанный выше способ представления массивом уже перестает работать.

        Рисунок 7-13   Одной последовательности обхода по уровням соответствуют разные двоичные структуры

        Чтобы решить эту проблему, мы можем явно записывать все None в последовательности обхода по уровням . Как показано на рисунке 7-14, после такой обработки последовательность обхода по уровням уже сможет однозначно задавать двоичное дерево. Пример кода приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
        # Представление двоичного дерева массивом\n# Используем None для обозначения пустых позиций\ntree = [1, 2, 3, 4, None, 6, 7, 8, 9, None, None, 12, None, None, 15]\n
        /* Представление двоичного дерева массивом */\n// Используем максимальное значение int, INT_MAX, для обозначения пустых позиций\nvector<int> tree = {1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15};\n
        /* Представление двоичного дерева массивом */\n// Используя обертку Integer для int, можно применять null для обозначения пустых позиций\nInteger[] tree = { 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 };\n
        /* Представление двоичного дерева массивом */\n// Используя nullable-тип int? , можно применять null для обозначения пустых позиций\nint?[] tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n
        /* Представление двоичного дерева массивом */\n// Используем срез типа any, чтобы можно было применять nil для обозначения пустых позиций\ntree := []any{1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15}\n
        /* Представление двоичного дерева массивом */\n// Используя nullable-тип Int? , можно применять nil для обозначения пустых позиций\nlet tree: [Int?] = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15]\n
        /* Представление двоичного дерева массивом */\n// Используем null для обозначения пустых позиций\nlet tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n
        /* Представление двоичного дерева массивом */\n// Используем null для обозначения пустых позиций\nlet tree: (number | null)[] = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n
        /* Представление двоичного дерева массивом */\n// Используя nullable-тип int? , можно применять null для обозначения пустых позиций\nList<int?> tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n
        /* Представление двоичного дерева массивом */\n// Используем None для обозначения пустых позиций\nlet tree = [Some(1), Some(2), Some(3), Some(4), None, Some(6), Some(7), Some(8), Some(9), None, None, Some(12), None, None, Some(15)];\n
        /* Представление двоичного дерева массивом */\n// Используем максимальное значение int для обозначения пустых позиций, поэтому узлы не должны принимать значение INT_MAX\nint tree[] = {1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15};\n
        /* Представление двоичного дерева массивом */\n// Используем null для обозначения пустых позиций\nval tree = arrayOf( 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 )\n
        ### Представление двоичного дерева массивом ###\n# Используем nil для обозначения пустых позиций\ntree = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15]\n

        Рисунок 7-14   Представление произвольного двоичного дерева массивом

        Стоит отметить, что полное двоичное дерево очень удобно представлять массивом . Если вспомнить определение полного двоичного дерева, то None появляются только на самом нижнем уровне и справа, а значит, все None обязательно находятся в конце последовательности обхода по уровням.

        Это означает, что при представлении полного двоичного дерева массивом можно не хранить все None , что очень удобно. На рисунке 7-15 приведен пример.

        Рисунок 7-15   Представление полного двоичного дерева массивом

        Ниже приведен код реализации двоичного дерева, представленного массивом. Он включает следующие операции.

        • Для заданного узла получить его значение, левого дочернего узла, правого дочернего узла и родительский узел.
        • Получить последовательности прямого, симметричного, обратного обходов и обхода по уровням.
        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_binary_tree.py
        class ArrayBinaryTree:\n    \"\"\"Класс двоичного дерева в массивном представлении\"\"\"\n\n    def __init__(self, arr: list[int | None]):\n        \"\"\"Конструктор\"\"\"\n        self._tree = list(arr)\n\n    def size(self):\n        \"\"\"Вместимость списка\"\"\"\n        return len(self._tree)\n\n    def val(self, i: int) -> int | None:\n        \"\"\"Получить значение узла с индексом i\"\"\"\n        # Если индекс выходит за границы, вернуть None, обозначающий пустую позицию\n        if i < 0 or i >= self.size():\n            return None\n        return self._tree[i]\n\n    def left(self, i: int) -> int | None:\n        \"\"\"Получить индекс левого дочернего узла узла с индексом i\"\"\"\n        return 2 * i + 1\n\n    def right(self, i: int) -> int | None:\n        \"\"\"Получить индекс правого дочернего узла узла с индексом i\"\"\"\n        return 2 * i + 2\n\n    def parent(self, i: int) -> int | None:\n        \"\"\"Получить индекс родительского узла узла с индексом i\"\"\"\n        return (i - 1) // 2\n\n    def level_order(self) -> list[int]:\n        \"\"\"Обход в ширину\"\"\"\n        self.res = []\n        # Непосредственно обходить массив\n        for i in range(self.size()):\n            if self.val(i) is not None:\n                self.res.append(self.val(i))\n        return self.res\n\n    def dfs(self, i: int, order: str):\n        \"\"\"Обход в глубину\"\"\"\n        if self.val(i) is None:\n            return\n        # Предварительный обход\n        if order == \"pre\":\n            self.res.append(self.val(i))\n        self.dfs(self.left(i), order)\n        # Симметричный обход\n        if order == \"in\":\n            self.res.append(self.val(i))\n        self.dfs(self.right(i), order)\n        # Обратный обход\n        if order == \"post\":\n            self.res.append(self.val(i))\n\n    def pre_order(self) -> list[int]:\n        \"\"\"Предварительный обход\"\"\"\n        self.res = []\n        self.dfs(0, order=\"pre\")\n        return self.res\n\n    def in_order(self) -> list[int]:\n        \"\"\"Симметричный обход\"\"\"\n        self.res = []\n        self.dfs(0, order=\"in\")\n        return self.res\n\n    def post_order(self) -> list[int]:\n        \"\"\"Обратный обход\"\"\"\n        self.res = []\n        self.dfs(0, order=\"post\")\n        return self.res\n
        array_binary_tree.cpp
        /* Класс двоичного дерева в массивном представлении */\nclass ArrayBinaryTree {\n  public:\n    /* Конструктор */\n    ArrayBinaryTree(vector<int> arr) {\n        tree = arr;\n    }\n\n    /* Вместимость списка */\n    int size() {\n        return tree.size();\n    }\n\n    /* Получить значение узла с индексом i */\n    int val(int i) {\n        // Если индекс выходит за границы, вернуть INT_MAX, обозначающий пустую позицию\n        if (i < 0 || i >= size())\n            return INT_MAX;\n        return tree[i];\n    }\n\n    /* Получить индекс левого дочернего узла узла с индексом i */\n    int left(int i) {\n        return 2 * i + 1;\n    }\n\n    /* Получить индекс правого дочернего узла узла с индексом i */\n    int right(int i) {\n        return 2 * i + 2;\n    }\n\n    /* Получить индекс родительского узла узла с индексом i */\n    int parent(int i) {\n        return (i - 1) / 2;\n    }\n\n    /* Обход в ширину */\n    vector<int> levelOrder() {\n        vector<int> res;\n        // Непосредственно обходить массив\n        for (int i = 0; i < size(); i++) {\n            if (val(i) != INT_MAX)\n                res.push_back(val(i));\n        }\n        return res;\n    }\n\n    /* Предварительный обход */\n    vector<int> preOrder() {\n        vector<int> res;\n        dfs(0, \"pre\", res);\n        return res;\n    }\n\n    /* Симметричный обход */\n    vector<int> inOrder() {\n        vector<int> res;\n        dfs(0, \"in\", res);\n        return res;\n    }\n\n    /* Обратный обход */\n    vector<int> postOrder() {\n        vector<int> res;\n        dfs(0, \"post\", res);\n        return res;\n    }\n\n  private:\n    vector<int> tree;\n\n    /* Обход в глубину */\n    void dfs(int i, string order, vector<int> &res) {\n        // Если это пустая позиция, вернуть\n        if (val(i) == INT_MAX)\n            return;\n        // Предварительный обход\n        if (order == \"pre\")\n            res.push_back(val(i));\n        dfs(left(i), order, res);\n        // Симметричный обход\n        if (order == \"in\")\n            res.push_back(val(i));\n        dfs(right(i), order, res);\n        // Обратный обход\n        if (order == \"post\")\n            res.push_back(val(i));\n    }\n};\n
        array_binary_tree.java
        /* Класс двоичного дерева в массивном представлении */\nclass ArrayBinaryTree {\n    private List<Integer> tree;\n\n    /* Конструктор */\n    public ArrayBinaryTree(List<Integer> arr) {\n        tree = new ArrayList<>(arr);\n    }\n\n    /* Вместимость списка */\n    public int size() {\n        return tree.size();\n    }\n\n    /* Получить значение узла с индексом i */\n    public Integer val(int i) {\n        // Если индекс выходит за границы, вернуть null, обозначающий пустую позицию\n        if (i < 0 || i >= size())\n            return null;\n        return tree.get(i);\n    }\n\n    /* Получить индекс левого дочернего узла узла с индексом i */\n    public Integer left(int i) {\n        return 2 * i + 1;\n    }\n\n    /* Получить индекс правого дочернего узла узла с индексом i */\n    public Integer right(int i) {\n        return 2 * i + 2;\n    }\n\n    /* Получить индекс родительского узла узла с индексом i */\n    public Integer parent(int i) {\n        return (i - 1) / 2;\n    }\n\n    /* Обход в ширину */\n    public List<Integer> levelOrder() {\n        List<Integer> res = new ArrayList<>();\n        // Непосредственно обходить массив\n        for (int i = 0; i < size(); i++) {\n            if (val(i) != null)\n                res.add(val(i));\n        }\n        return res;\n    }\n\n    /* Обход в глубину */\n    private void dfs(Integer i, String order, List<Integer> res) {\n        // Если это пустая позиция, вернуть\n        if (val(i) == null)\n            return;\n        // Предварительный обход\n        if (\"pre\".equals(order))\n            res.add(val(i));\n        dfs(left(i), order, res);\n        // Симметричный обход\n        if (\"in\".equals(order))\n            res.add(val(i));\n        dfs(right(i), order, res);\n        // Обратный обход\n        if (\"post\".equals(order))\n            res.add(val(i));\n    }\n\n    /* Предварительный обход */\n    public List<Integer> preOrder() {\n        List<Integer> res = new ArrayList<>();\n        dfs(0, \"pre\", res);\n        return res;\n    }\n\n    /* Симметричный обход */\n    public List<Integer> inOrder() {\n        List<Integer> res = new ArrayList<>();\n        dfs(0, \"in\", res);\n        return res;\n    }\n\n    /* Обратный обход */\n    public List<Integer> postOrder() {\n        List<Integer> res = new ArrayList<>();\n        dfs(0, \"post\", res);\n        return res;\n    }\n}\n
        array_binary_tree.cs
        /* Класс двоичного дерева в массивном представлении */\nclass ArrayBinaryTree(List<int?> arr) {\n    List<int?> tree = new(arr);\n\n    /* Вместимость списка */\n    public int Size() {\n        return tree.Count;\n    }\n\n    /* Получить значение узла с индексом i */\n    public int? Val(int i) {\n        // Если индекс выходит за границы, вернуть null, обозначающий пустую позицию\n        if (i < 0 || i >= Size())\n            return null;\n        return tree[i];\n    }\n\n    /* Получить индекс левого дочернего узла узла с индексом i */\n    public int Left(int i) {\n        return 2 * i + 1;\n    }\n\n    /* Получить индекс правого дочернего узла узла с индексом i */\n    public int Right(int i) {\n        return 2 * i + 2;\n    }\n\n    /* Получить индекс родительского узла узла с индексом i */\n    public int Parent(int i) {\n        return (i - 1) / 2;\n    }\n\n    /* Обход в ширину */\n    public List<int> LevelOrder() {\n        List<int> res = [];\n        // Непосредственно обходить массив\n        for (int i = 0; i < Size(); i++) {\n            if (Val(i).HasValue)\n                res.Add(Val(i)!.Value);\n        }\n        return res;\n    }\n\n    /* Обход в глубину */\n    void DFS(int i, string order, List<int> res) {\n        // Если это пустая позиция, вернуть\n        if (!Val(i).HasValue)\n            return;\n        // Предварительный обход\n        if (order == \"pre\")\n            res.Add(Val(i)!.Value);\n        DFS(Left(i), order, res);\n        // Симметричный обход\n        if (order == \"in\")\n            res.Add(Val(i)!.Value);\n        DFS(Right(i), order, res);\n        // Обратный обход\n        if (order == \"post\")\n            res.Add(Val(i)!.Value);\n    }\n\n    /* Предварительный обход */\n    public List<int> PreOrder() {\n        List<int> res = [];\n        DFS(0, \"pre\", res);\n        return res;\n    }\n\n    /* Симметричный обход */\n    public List<int> InOrder() {\n        List<int> res = [];\n        DFS(0, \"in\", res);\n        return res;\n    }\n\n    /* Обратный обход */\n    public List<int> PostOrder() {\n        List<int> res = [];\n        DFS(0, \"post\", res);\n        return res;\n    }\n}\n
        array_binary_tree.go
        /* Класс двоичного дерева в массивном представлении */\ntype arrayBinaryTree struct {\n    tree []any\n}\n\n/* Конструктор */\nfunc newArrayBinaryTree(arr []any) *arrayBinaryTree {\n    return &arrayBinaryTree{\n        tree: arr,\n    }\n}\n\n/* Вместимость списка */\nfunc (abt *arrayBinaryTree) size() int {\n    return len(abt.tree)\n}\n\n/* Получить значение узла с индексом i */\nfunc (abt *arrayBinaryTree) val(i int) any {\n    // Если индекс выходит за границы, вернуть null, обозначающий пустую позицию\n    if i < 0 || i >= abt.size() {\n        return nil\n    }\n    return abt.tree[i]\n}\n\n/* Получить индекс левого дочернего узла узла с индексом i */\nfunc (abt *arrayBinaryTree) left(i int) int {\n    return 2*i + 1\n}\n\n/* Получить индекс правого дочернего узла узла с индексом i */\nfunc (abt *arrayBinaryTree) right(i int) int {\n    return 2*i + 2\n}\n\n/* Получить индекс родительского узла узла с индексом i */\nfunc (abt *arrayBinaryTree) parent(i int) int {\n    return (i - 1) / 2\n}\n\n/* Обход в ширину */\nfunc (abt *arrayBinaryTree) levelOrder() []any {\n    var res []any\n    // Непосредственно обходить массив\n    for i := 0; i < abt.size(); i++ {\n        if abt.val(i) != nil {\n            res = append(res, abt.val(i))\n        }\n    }\n    return res\n}\n\n/* Обход в глубину */\nfunc (abt *arrayBinaryTree) dfs(i int, order string, res *[]any) {\n    // Если это пустая позиция, вернуть\n    if abt.val(i) == nil {\n        return\n    }\n    // Предварительный обход\n    if order == \"pre\" {\n        *res = append(*res, abt.val(i))\n    }\n    abt.dfs(abt.left(i), order, res)\n    // Симметричный обход\n    if order == \"in\" {\n        *res = append(*res, abt.val(i))\n    }\n    abt.dfs(abt.right(i), order, res)\n    // Обратный обход\n    if order == \"post\" {\n        *res = append(*res, abt.val(i))\n    }\n}\n\n/* Предварительный обход */\nfunc (abt *arrayBinaryTree) preOrder() []any {\n    var res []any\n    abt.dfs(0, \"pre\", &res)\n    return res\n}\n\n/* Симметричный обход */\nfunc (abt *arrayBinaryTree) inOrder() []any {\n    var res []any\n    abt.dfs(0, \"in\", &res)\n    return res\n}\n\n/* Обратный обход */\nfunc (abt *arrayBinaryTree) postOrder() []any {\n    var res []any\n    abt.dfs(0, \"post\", &res)\n    return res\n}\n
        array_binary_tree.swift
        /* Класс двоичного дерева в массивном представлении */\nclass ArrayBinaryTree {\n    private var tree: [Int?]\n\n    /* Конструктор */\n    init(arr: [Int?]) {\n        tree = arr\n    }\n\n    /* Вместимость списка */\n    func size() -> Int {\n        tree.count\n    }\n\n    /* Получить значение узла с индексом i */\n    func val(i: Int) -> Int? {\n        // Если индекс выходит за границы, вернуть null, обозначающий пустую позицию\n        if i < 0 || i >= size() {\n            return nil\n        }\n        return tree[i]\n    }\n\n    /* Получить индекс левого дочернего узла узла с индексом i */\n    func left(i: Int) -> Int {\n        2 * i + 1\n    }\n\n    /* Получить индекс правого дочернего узла узла с индексом i */\n    func right(i: Int) -> Int {\n        2 * i + 2\n    }\n\n    /* Получить индекс родительского узла узла с индексом i */\n    func parent(i: Int) -> Int {\n        (i - 1) / 2\n    }\n\n    /* Обход в ширину */\n    func levelOrder() -> [Int] {\n        var res: [Int] = []\n        // Непосредственно обходить массив\n        for i in 0 ..< size() {\n            if let val = val(i: i) {\n                res.append(val)\n            }\n        }\n        return res\n    }\n\n    /* Обход в глубину */\n    private func dfs(i: Int, order: String, res: inout [Int]) {\n        // Если это пустая позиция, вернуть\n        guard let val = val(i: i) else {\n            return\n        }\n        // Предварительный обход\n        if order == \"pre\" {\n            res.append(val)\n        }\n        dfs(i: left(i: i), order: order, res: &res)\n        // Симметричный обход\n        if order == \"in\" {\n            res.append(val)\n        }\n        dfs(i: right(i: i), order: order, res: &res)\n        // Обратный обход\n        if order == \"post\" {\n            res.append(val)\n        }\n    }\n\n    /* Предварительный обход */\n    func preOrder() -> [Int] {\n        var res: [Int] = []\n        dfs(i: 0, order: \"pre\", res: &res)\n        return res\n    }\n\n    /* Симметричный обход */\n    func inOrder() -> [Int] {\n        var res: [Int] = []\n        dfs(i: 0, order: \"in\", res: &res)\n        return res\n    }\n\n    /* Обратный обход */\n    func postOrder() -> [Int] {\n        var res: [Int] = []\n        dfs(i: 0, order: \"post\", res: &res)\n        return res\n    }\n}\n
        array_binary_tree.js
        /* Класс двоичного дерева в массивном представлении */\nclass ArrayBinaryTree {\n    #tree;\n\n    /* Конструктор */\n    constructor(arr) {\n        this.#tree = arr;\n    }\n\n    /* Вместимость списка */\n    size() {\n        return this.#tree.length;\n    }\n\n    /* Получить значение узла с индексом i */\n    val(i) {\n        // Если индекс выходит за границы, вернуть null, обозначающий пустую позицию\n        if (i < 0 || i >= this.size()) return null;\n        return this.#tree[i];\n    }\n\n    /* Получить индекс левого дочернего узла узла с индексом i */\n    left(i) {\n        return 2 * i + 1;\n    }\n\n    /* Получить индекс правого дочернего узла узла с индексом i */\n    right(i) {\n        return 2 * i + 2;\n    }\n\n    /* Получить индекс родительского узла узла с индексом i */\n    parent(i) {\n        return Math.floor((i - 1) / 2); // Округление вниз при делении\n    }\n\n    /* Обход в ширину */\n    levelOrder() {\n        let res = [];\n        // Непосредственно обходить массив\n        for (let i = 0; i < this.size(); i++) {\n            if (this.val(i) !== null) res.push(this.val(i));\n        }\n        return res;\n    }\n\n    /* Обход в глубину */\n    #dfs(i, order, res) {\n        // Если это пустая позиция, вернуть\n        if (this.val(i) === null) return;\n        // Предварительный обход\n        if (order === 'pre') res.push(this.val(i));\n        this.#dfs(this.left(i), order, res);\n        // Симметричный обход\n        if (order === 'in') res.push(this.val(i));\n        this.#dfs(this.right(i), order, res);\n        // Обратный обход\n        if (order === 'post') res.push(this.val(i));\n    }\n\n    /* Предварительный обход */\n    preOrder() {\n        const res = [];\n        this.#dfs(0, 'pre', res);\n        return res;\n    }\n\n    /* Симметричный обход */\n    inOrder() {\n        const res = [];\n        this.#dfs(0, 'in', res);\n        return res;\n    }\n\n    /* Обратный обход */\n    postOrder() {\n        const res = [];\n        this.#dfs(0, 'post', res);\n        return res;\n    }\n}\n
        array_binary_tree.ts
        /* Класс двоичного дерева в массивном представлении */\nclass ArrayBinaryTree {\n    #tree: (number | null)[];\n\n    /* Конструктор */\n    constructor(arr: (number | null)[]) {\n        this.#tree = arr;\n    }\n\n    /* Вместимость списка */\n    size(): number {\n        return this.#tree.length;\n    }\n\n    /* Получить значение узла с индексом i */\n    val(i: number): number | null {\n        // Если индекс выходит за границы, вернуть null, обозначающий пустую позицию\n        if (i < 0 || i >= this.size()) return null;\n        return this.#tree[i];\n    }\n\n    /* Получить индекс левого дочернего узла узла с индексом i */\n    left(i: number): number {\n        return 2 * i + 1;\n    }\n\n    /* Получить индекс правого дочернего узла узла с индексом i */\n    right(i: number): number {\n        return 2 * i + 2;\n    }\n\n    /* Получить индекс родительского узла узла с индексом i */\n    parent(i: number): number {\n        return Math.floor((i - 1) / 2); // Округление вниз при делении\n    }\n\n    /* Обход в ширину */\n    levelOrder(): number[] {\n        let res = [];\n        // Непосредственно обходить массив\n        for (let i = 0; i < this.size(); i++) {\n            if (this.val(i) !== null) res.push(this.val(i));\n        }\n        return res;\n    }\n\n    /* Обход в глубину */\n    #dfs(i: number, order: Order, res: (number | null)[]): void {\n        // Если это пустая позиция, вернуть\n        if (this.val(i) === null) return;\n        // Предварительный обход\n        if (order === 'pre') res.push(this.val(i));\n        this.#dfs(this.left(i), order, res);\n        // Симметричный обход\n        if (order === 'in') res.push(this.val(i));\n        this.#dfs(this.right(i), order, res);\n        // Обратный обход\n        if (order === 'post') res.push(this.val(i));\n    }\n\n    /* Предварительный обход */\n    preOrder(): (number | null)[] {\n        const res = [];\n        this.#dfs(0, 'pre', res);\n        return res;\n    }\n\n    /* Симметричный обход */\n    inOrder(): (number | null)[] {\n        const res = [];\n        this.#dfs(0, 'in', res);\n        return res;\n    }\n\n    /* Обратный обход */\n    postOrder(): (number | null)[] {\n        const res = [];\n        this.#dfs(0, 'post', res);\n        return res;\n    }\n}\n
        array_binary_tree.dart
        /* Класс двоичного дерева в массивном представлении */\nclass ArrayBinaryTree {\n  late List<int?> _tree;\n\n  /* Конструктор */\n  ArrayBinaryTree(this._tree);\n\n  /* Вместимость списка */\n  int size() {\n    return _tree.length;\n  }\n\n  /* Получить значение узла с индексом i */\n  int? val(int i) {\n    // Если индекс выходит за границы, вернуть null, обозначающий пустую позицию\n    if (i < 0 || i >= size()) {\n      return null;\n    }\n    return _tree[i];\n  }\n\n  /* Получить индекс левого дочернего узла узла с индексом i */\n  int? left(int i) {\n    return 2 * i + 1;\n  }\n\n  /* Получить индекс правого дочернего узла узла с индексом i */\n  int? right(int i) {\n    return 2 * i + 2;\n  }\n\n  /* Получить индекс родительского узла узла с индексом i */\n  int? parent(int i) {\n    return (i - 1) ~/ 2;\n  }\n\n  /* Обход в ширину */\n  List<int> levelOrder() {\n    List<int> res = [];\n    for (int i = 0; i < size(); i++) {\n      if (val(i) != null) {\n        res.add(val(i)!);\n      }\n    }\n    return res;\n  }\n\n  /* Обход в глубину */\n  void dfs(int i, String order, List<int?> res) {\n    // Если это пустая позиция, вернуть\n    if (val(i) == null) {\n      return;\n    }\n    // Предварительный обход\n    if (order == 'pre') {\n      res.add(val(i));\n    }\n    dfs(left(i)!, order, res);\n    // Симметричный обход\n    if (order == 'in') {\n      res.add(val(i));\n    }\n    dfs(right(i)!, order, res);\n    // Обратный обход\n    if (order == 'post') {\n      res.add(val(i));\n    }\n  }\n\n  /* Предварительный обход */\n  List<int?> preOrder() {\n    List<int?> res = [];\n    dfs(0, 'pre', res);\n    return res;\n  }\n\n  /* Симметричный обход */\n  List<int?> inOrder() {\n    List<int?> res = [];\n    dfs(0, 'in', res);\n    return res;\n  }\n\n  /* Обратный обход */\n  List<int?> postOrder() {\n    List<int?> res = [];\n    dfs(0, 'post', res);\n    return res;\n  }\n}\n
        array_binary_tree.rs
        /* Класс двоичного дерева в массивном представлении */\nstruct ArrayBinaryTree {\n    tree: Vec<Option<i32>>,\n}\n\nimpl ArrayBinaryTree {\n    /* Конструктор */\n    fn new(arr: Vec<Option<i32>>) -> Self {\n        Self { tree: arr }\n    }\n\n    /* Вместимость списка */\n    fn size(&self) -> i32 {\n        self.tree.len() as i32\n    }\n\n    /* Получить значение узла с индексом i */\n    fn val(&self, i: i32) -> Option<i32> {\n        // Если индекс выходит за границы, вернуть None, обозначающий пустую позицию\n        if i < 0 || i >= self.size() {\n            None\n        } else {\n            self.tree[i as usize]\n        }\n    }\n\n    /* Получить индекс левого дочернего узла узла с индексом i */\n    fn left(&self, i: i32) -> i32 {\n        2 * i + 1\n    }\n\n    /* Получить индекс правого дочернего узла узла с индексом i */\n    fn right(&self, i: i32) -> i32 {\n        2 * i + 2\n    }\n\n    /* Получить индекс родительского узла узла с индексом i */\n    fn parent(&self, i: i32) -> i32 {\n        (i - 1) / 2\n    }\n\n    /* Обход в ширину */\n    fn level_order(&self) -> Vec<i32> {\n        self.tree.iter().filter_map(|&x| x).collect()\n    }\n\n    /* Обход в глубину */\n    fn dfs(&self, i: i32, order: &'static str, res: &mut Vec<i32>) {\n        if self.val(i).is_none() {\n            return;\n        }\n        let val = self.val(i).unwrap();\n        // Предварительный обход\n        if order == \"pre\" {\n            res.push(val);\n        }\n        self.dfs(self.left(i), order, res);\n        // Симметричный обход\n        if order == \"in\" {\n            res.push(val);\n        }\n        self.dfs(self.right(i), order, res);\n        // Обратный обход\n        if order == \"post\" {\n            res.push(val);\n        }\n    }\n\n    /* Предварительный обход */\n    fn pre_order(&self) -> Vec<i32> {\n        let mut res = vec![];\n        self.dfs(0, \"pre\", &mut res);\n        res\n    }\n\n    /* Симметричный обход */\n    fn in_order(&self) -> Vec<i32> {\n        let mut res = vec![];\n        self.dfs(0, \"in\", &mut res);\n        res\n    }\n\n    /* Обратный обход */\n    fn post_order(&self) -> Vec<i32> {\n        let mut res = vec![];\n        self.dfs(0, \"post\", &mut res);\n        res\n    }\n}\n
        array_binary_tree.c
        /* Структура двоичного дерева в представлении массивом */\ntypedef struct {\n    int *tree;\n    int size;\n} ArrayBinaryTree;\n\n/* Конструктор */\nArrayBinaryTree *newArrayBinaryTree(int *arr, int arrSize) {\n    ArrayBinaryTree *abt = (ArrayBinaryTree *)malloc(sizeof(ArrayBinaryTree));\n    abt->tree = malloc(sizeof(int) * arrSize);\n    memcpy(abt->tree, arr, sizeof(int) * arrSize);\n    abt->size = arrSize;\n    return abt;\n}\n\n/* Деструктор */\nvoid delArrayBinaryTree(ArrayBinaryTree *abt) {\n    free(abt->tree);\n    free(abt);\n}\n\n/* Вместимость списка */\nint size(ArrayBinaryTree *abt) {\n    return abt->size;\n}\n\n/* Получить значение узла с индексом i */\nint val(ArrayBinaryTree *abt, int i) {\n    // Если индекс выходит за границы, вернуть INT_MAX, обозначающий пустую позицию\n    if (i < 0 || i >= size(abt))\n        return INT_MAX;\n    return abt->tree[i];\n}\n\n/* Обход в ширину */\nint *levelOrder(ArrayBinaryTree *abt, int *returnSize) {\n    int *res = (int *)malloc(sizeof(int) * size(abt));\n    int index = 0;\n    // Непосредственно обходить массив\n    for (int i = 0; i < size(abt); i++) {\n        if (val(abt, i) != INT_MAX)\n            res[index++] = val(abt, i);\n    }\n    *returnSize = index;\n    return res;\n}\n\n/* Обход в глубину */\nvoid dfs(ArrayBinaryTree *abt, int i, char *order, int *res, int *index) {\n    // Если это пустая позиция, вернуть\n    if (val(abt, i) == INT_MAX)\n        return;\n    // Предварительный обход\n    if (strcmp(order, \"pre\") == 0)\n        res[(*index)++] = val(abt, i);\n    dfs(abt, left(i), order, res, index);\n    // Симметричный обход\n    if (strcmp(order, \"in\") == 0)\n        res[(*index)++] = val(abt, i);\n    dfs(abt, right(i), order, res, index);\n    // Обратный обход\n    if (strcmp(order, \"post\") == 0)\n        res[(*index)++] = val(abt, i);\n}\n\n/* Предварительный обход */\nint *preOrder(ArrayBinaryTree *abt, int *returnSize) {\n    int *res = (int *)malloc(sizeof(int) * size(abt));\n    int index = 0;\n    dfs(abt, 0, \"pre\", res, &index);\n    *returnSize = index;\n    return res;\n}\n\n/* Симметричный обход */\nint *inOrder(ArrayBinaryTree *abt, int *returnSize) {\n    int *res = (int *)malloc(sizeof(int) * size(abt));\n    int index = 0;\n    dfs(abt, 0, \"in\", res, &index);\n    *returnSize = index;\n    return res;\n}\n\n/* Обратный обход */\nint *postOrder(ArrayBinaryTree *abt, int *returnSize) {\n    int *res = (int *)malloc(sizeof(int) * size(abt));\n    int index = 0;\n    dfs(abt, 0, \"post\", res, &index);\n    *returnSize = index;\n    return res;\n}\n
        array_binary_tree.kt
        /* Класс двоичного дерева в массивном представлении */\nclass ArrayBinaryTree(val tree: MutableList<Int?>) {\n    /* Вместимость списка */\n    fun size(): Int {\n        return tree.size\n    }\n\n    /* Получить значение узла с индексом i */\n    fun _val(i: Int): Int? {\n        // Если индекс выходит за границы, вернуть null, обозначающий пустую позицию\n        if (i < 0 || i >= size()) return null\n        return tree[i]\n    }\n\n    /* Получить индекс левого дочернего узла узла с индексом i */\n    fun left(i: Int): Int {\n        return 2 * i + 1\n    }\n\n    /* Получить индекс правого дочернего узла узла с индексом i */\n    fun right(i: Int): Int {\n        return 2 * i + 2\n    }\n\n    /* Получить индекс родительского узла узла с индексом i */\n    fun parent(i: Int): Int {\n        return (i - 1) / 2\n    }\n\n    /* Обход в ширину */\n    fun levelOrder(): MutableList<Int?> {\n        val res = mutableListOf<Int?>()\n        // Непосредственно обходить массив\n        for (i in 0..<size()) {\n            if (_val(i) != null)\n                res.add(_val(i))\n        }\n        return res\n    }\n\n    /* Обход в глубину */\n    fun dfs(i: Int, order: String, res: MutableList<Int?>) {\n        // Если это пустая позиция, вернуть\n        if (_val(i) == null)\n            return\n        // Предварительный обход\n        if (\"pre\" == order)\n            res.add(_val(i))\n        dfs(left(i), order, res)\n        // Симметричный обход\n        if (\"in\" == order)\n            res.add(_val(i))\n        dfs(right(i), order, res)\n        // Обратный обход\n        if (\"post\" == order)\n            res.add(_val(i))\n    }\n\n    /* Предварительный обход */\n    fun preOrder(): MutableList<Int?> {\n        val res = mutableListOf<Int?>()\n        dfs(0, \"pre\", res)\n        return res\n    }\n\n    /* Симметричный обход */\n    fun inOrder(): MutableList<Int?> {\n        val res = mutableListOf<Int?>()\n        dfs(0, \"in\", res)\n        return res\n    }\n\n    /* Обратный обход */\n    fun postOrder(): MutableList<Int?> {\n        val res = mutableListOf<Int?>()\n        dfs(0, \"post\", res)\n        return res\n    }\n}\n
        array_binary_tree.rb
        ### Класс двоичного дерева в массивном представлении ###\nclass ArrayBinaryTree\n  ### Конструктор ###\n  def initialize(arr)\n    @tree = arr.to_a\n  end\n\n  ### Вместимость списка ###\n  def size\n    @tree.length\n  end\n\n  ### Получить значение узла с индексом i ###\n  def val(i)\n    # Если индекс выходит за границы, вернуть nil, обозначающий пустую ячейку\n    return if i < 0 || i >= size\n\n    @tree[i]\n  end\n\n  ### Получить индекс левого дочернего узла узла с индексом i ###\n  def left(i)\n    2 * i + 1\n  end\n\n  ### Получить индекс правого дочернего узла узла с индексом i ###\n  def right(i)\n    2 * i + 2\n  end\n\n  ### Получить индекс родительского узла узла с индексом i ###\n  def parent(i)\n    (i - 1) / 2\n  end\n\n  ### Обход в ширину ###\n  def level_order\n    @res = []\n\n    # Непосредственно обходить массив\n    for i in 0...size\n      @res << val(i) unless val(i).nil?\n    end\n\n    @res\n  end\n\n  ### Обход в глубину ###\n  def dfs(i, order)\n    return if val(i).nil?\n    # Предварительный обход\n    @res << val(i) if order == :pre\n    dfs(left(i), order)\n    # Симметричный обход\n    @res << val(i) if order == :in\n    dfs(right(i), order)\n    # Обратный обход\n    @res << val(i) if order == :post\n  end\n\n  ### Предварительный обход ###\n  def pre_order\n    @res = []\n    dfs(0, :pre)\n    @res\n  end\n\n  ### Симметричный обход ###\n  def in_order\n    @res = []\n    dfs(0, :in)\n    @res\n  end\n\n  ### Обратный обход ###\n  def post_order\n    @res = []\n    dfs(0, :post)\n    @res\n  end\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 7. Деревья","7.3   Представление двоичного дерева массивом"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/#733","level":2,"title":"7.3.3   Преимущества и ограничения","text":"

        Представление двоичного дерева массивом имеет в основном следующие преимущества.

        • Массив хранится в непрерывной области памяти, хорошо работает с кешем и обеспечивает высокую скорость доступа и обхода.
        • Не нужно хранить указатели, поэтому память расходуется экономнее.
        • Разрешается произвольный доступ к узлам.

        Однако у представления массивом есть и некоторые ограничения.

        • Для хранения массива требуется непрерывная область памяти, поэтому такой способ не подходит для деревьев с очень большим объемом данных.
        • Добавление и удаление узлов приходится реализовывать через вставку и удаление элементов массива, а это не слишком эффективно.
        • Когда в двоичном дереве имеется большое число None , доля действительно полезных данных в массиве оказывается низкой, и эффективность использования пространства падает.
        ","path":["Глава 7. Деревья","7.3   Представление двоичного дерева массивом"],"tags":[]},{"location":"chapter_tree/avl_tree/","level":1,"title":"7.5   AVL-дерево *","text":"

        В разделе «Двоичное дерево поиска» мы упоминали, что после многократных операций вставки и удаления узлов двоичное дерево поиска может выродиться в связный список. В таком случае временная сложность всех операций ухудшается с \\(O(\\log n)\\) до \\(O(n)\\) .

        Как показано на рисунке 7-24, после двух операций удаления узлов это двоичное дерево поиска вырождается в связный список.

        Рисунок 7-24   Деградация AVL-дерева после удаления узлов

        Другой пример: если в идеальное двоичное дерево, показанное на рисунке 7-25, вставить два узла, то дерево сильно наклонится влево, а временная сложность поиска тоже ухудшится.

        Рисунок 7-25   Деградация AVL-дерева после вставки узлов

        В 1962 году Г. М. Adelson-Velsky и Е. М. Landis в статье «An algorithm for the organization of information» предложили AVL-дерево. В статье подробно описан набор операций, гарантирующий, что при непрерывном добавлении и удалении узлов AVL-дерево не вырождается, благодаря чему временная сложность различных операций сохраняется на уровне \\(O(\\log n)\\) . Иначе говоря, в сценариях, где часто выполняются вставка, удаление, поиск и изменение, AVL-дерево всегда поддерживает эффективную работу с данными и потому имеет высокую практическую ценность.

        ","path":["Глава 7. Деревья","7.5   AVL-дерево *"],"tags":[]},{"location":"chapter_tree/avl_tree/#751-avl-","level":2,"title":"7.5.1   Распространенные термины AVL-дерева","text":"

        AVL-дерево одновременно является и двоичным деревом поиска, и сбалансированным двоичным деревом, то есть одновременно удовлетворяет всем свойствам обеих этих структур. Поэтому AVL-дерево является разновидностью сбалансированного двоичного дерева поиска (balanced binary search tree).

        ","path":["Глава 7. Деревья","7.5   AVL-дерево *"],"tags":[]},{"location":"chapter_tree/avl_tree/#1","level":3,"title":"1.   Высота узла","text":"

        Поскольку операции AVL-дерева требуют получать высоту узла, нам нужно добавить в класс узла переменную height :

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
        class TreeNode:\n    \"\"\"Класс узла AVL-дерева\"\"\"\n    def __init__(self, val: int):\n        self.val: int = val                 # Значение узла\n        self.height: int = 0                # Высота узла\n        self.left: TreeNode | None = None   # Ссылка на левого дочернего узла\n        self.right: TreeNode | None = None  # Ссылка на правого дочернего узла\n
        /* Класс узла AVL-дерева */\nstruct TreeNode {\n    int val{};          // Значение узла\n    int height = 0;     // Высота узла\n    TreeNode *left{};   // Левый дочерний узел\n    TreeNode *right{};  // Правый дочерний узел\n    TreeNode() = default;\n    explicit TreeNode(int x) : val(x){}\n};\n
        /* Класс узла AVL-дерева */\nclass TreeNode {\n    public int val;        // Значение узла\n    public int height;     // Высота узла\n    public TreeNode left;  // Левый дочерний узел\n    public TreeNode right; // Правый дочерний узел\n    public TreeNode(int x) { val = x; }\n}\n
        /* Класс узла AVL-дерева */\nclass TreeNode(int? x) {\n    public int? val = x;    // Значение узла\n    public int height;      // Высота узла\n    public TreeNode? left;  // Ссылка на левого дочернего узла\n    public TreeNode? right; // Ссылка на правого дочернего узла\n}\n
        /* Структура узла AVL-дерева */\ntype TreeNode struct {\n    Val    int       // Значение узла\n    Height int       // Высота узла\n    Left   *TreeNode // Ссылка на левого дочернего узла\n    Right  *TreeNode // Ссылка на правого дочернего узла\n}\n
        /* Класс узла AVL-дерева */\nclass TreeNode {\n    var val: Int // Значение узла\n    var height: Int // Высота узла\n    var left: TreeNode? // Левый дочерний узел\n    var right: TreeNode? // Правый дочерний узел\n\n    init(x: Int) {\n        val = x\n        height = 0\n    }\n}\n
        /* Класс узла AVL-дерева */\nclass TreeNode {\n    val; // Значение узла\n    height; // Высота узла\n    left; // Указатель на левого дочернего узла\n    right; // Указатель на правого дочернего узла\n    constructor(val, left, right, height) {\n        this.val = val === undefined ? 0 : val;\n        this.height = height === undefined ? 0 : height;\n        this.left = left === undefined ? null : left;\n        this.right = right === undefined ? null : right;\n    }\n}\n
        /* Класс узла AVL-дерева */\nclass TreeNode {\n    val: number;            // Значение узла\n    height: number;         // Высота узла\n    left: TreeNode | null;  // Указатель на левого дочернего узла\n    right: TreeNode | null; // Указатель на правого дочернего узла\n    constructor(val?: number, height?: number, left?: TreeNode | null, right?: TreeNode | null) {\n        this.val = val === undefined ? 0 : val;\n        this.height = height === undefined ? 0 : height;\n        this.left = left === undefined ? null : left;\n        this.right = right === undefined ? null : right;\n    }\n}\n
        /* Класс узла AVL-дерева */\nclass TreeNode {\n  int val;         // Значение узла\n  int height;      // Высота узла\n  TreeNode? left;  // Левый дочерний узел\n  TreeNode? right; // Правый дочерний узел\n  TreeNode(this.val, [this.height = 0, this.left, this.right]);\n}\n
        use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* Структура узла AVL-дерева */\nstruct TreeNode {\n    val: i32,                               // Значение узла\n    height: i32,                            // Высота узла\n    left: Option<Rc<RefCell<TreeNode>>>,    // Левый дочерний узел\n    right: Option<Rc<RefCell<TreeNode>>>,   // Правый дочерний узел\n}\n\nimpl TreeNode {\n    /* Конструктор */\n    fn new(val: i32) -> Rc<RefCell<Self>> {\n        Rc::new(RefCell::new(Self {\n            val,\n            height: 0,\n            left: None,\n            right: None\n        }))\n    }\n}\n
        /* Структура узла AVL-дерева */\ntypedef struct TreeNode {\n    int val;\n    int height;\n    struct TreeNode *left;\n    struct TreeNode *right;\n} TreeNode;\n\n/* Конструктор */\nTreeNode *newTreeNode(int val) {\n    TreeNode *node;\n\n    node = (TreeNode *)malloc(sizeof(TreeNode));\n    node->val = val;\n    node->height = 0;\n    node->left = NULL;\n    node->right = NULL;\n    return node;\n}\n
        /* Класс узла AVL-дерева */\nclass TreeNode(val _val: Int) {  // Значение узла\n    val height: Int = 0          // Высота узла\n    val left: TreeNode? = null   // Левый дочерний узел\n    val right: TreeNode? = null  // Правый дочерний узел\n}\n
        ### Класс узла AVL-дерева ###\nclass TreeNode\n  attr_accessor :val    # Значение узла\n  attr_accessor :height # Высота узла\n  attr_accessor :left   # Ссылка на левого дочернего узла\n  attr_accessor :right  # Ссылка на правого дочернего узла\n\n  def initialize(val)\n    @val = val\n    @height = 0\n  end\nend\n

        «Высота узла» означает расстояние от этого узла до самого удаленного листового узла, то есть число пройденных «ребер». Особенно важно помнить, что высота листового узла равна \\(0\\) , а высота пустого узла равна \\(-1\\) . Мы создадим две вспомогательные функции: одну для получения высоты узла, другую для ее обновления:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
        def height(self, node: TreeNode | None) -> int:\n    \"\"\"Получить высоту узла\"\"\"\n    # Высота пустого узла равна -1, высота листового узла равна 0\n    if node is not None:\n        return node.height\n    return -1\n\ndef update_height(self, node: TreeNode | None):\n    \"\"\"Обновить высоту узла\"\"\"\n    # Высота узла равна высоте более высокого поддерева + 1\n    node.height = max([self.height(node.left), self.height(node.right)]) + 1\n
        avl_tree.cpp
        /* Получить высоту узла */\nint height(TreeNode *node) {\n    // Высота пустого узла равна -1, высота листового узла равна 0\n    return node == nullptr ? -1 : node->height;\n}\n\n/* Обновить высоту узла */\nvoid updateHeight(TreeNode *node) {\n    // Высота узла равна высоте более высокого поддерева + 1\n    node->height = max(height(node->left), height(node->right)) + 1;\n}\n
        avl_tree.java
        /* Получить высоту узла */\nint height(TreeNode node) {\n    // Высота пустого узла равна -1, высота листового узла равна 0\n    return node == null ? -1 : node.height;\n}\n\n/* Обновить высоту узла */\nvoid updateHeight(TreeNode node) {\n    // Высота узла равна высоте более высокого поддерева + 1\n    node.height = Math.max(height(node.left), height(node.right)) + 1;\n}\n
        avl_tree.cs
        /* Получить высоту узла */\nint Height(TreeNode? node) {\n    // Высота пустого узла равна -1, высота листового узла равна 0\n    return node == null ? -1 : node.height;\n}\n\n/* Обновить высоту узла */\nvoid UpdateHeight(TreeNode node) {\n    // Высота узла равна высоте более высокого поддерева + 1\n    node.height = Math.Max(Height(node.left), Height(node.right)) + 1;\n}\n
        avl_tree.go
        /* Получить высоту узла */\nfunc (t *aVLTree) height(node *TreeNode) int {\n    // Высота пустого узла равна -1, высота листового узла равна 0\n    if node != nil {\n        return node.Height\n    }\n    return -1\n}\n\n/* Обновить высоту узла */\nfunc (t *aVLTree) updateHeight(node *TreeNode) {\n    lh := t.height(node.Left)\n    rh := t.height(node.Right)\n    // Высота узла равна высоте более высокого поддерева + 1\n    if lh > rh {\n        node.Height = lh + 1\n    } else {\n        node.Height = rh + 1\n    }\n}\n
        avl_tree.swift
        /* Получить высоту узла */\nfunc height(node: TreeNode?) -> Int {\n    // Высота пустого узла равна -1, высота листового узла равна 0\n    node?.height ?? -1\n}\n\n/* Обновить высоту узла */\nfunc updateHeight(node: TreeNode?) {\n    // Высота узла равна высоте более высокого поддерева + 1\n    node?.height = max(height(node: node?.left), height(node: node?.right)) + 1\n}\n
        avl_tree.js
        /* Получить высоту узла */\nheight(node) {\n    // Высота пустого узла равна -1, высота листового узла равна 0\n    return node === null ? -1 : node.height;\n}\n\n/* Обновить высоту узла */\n#updateHeight(node) {\n    // Высота узла равна высоте более высокого поддерева + 1\n    node.height =\n        Math.max(this.height(node.left), this.height(node.right)) + 1;\n}\n
        avl_tree.ts
        /* Получить высоту узла */\nheight(node: TreeNode): number {\n    // Высота пустого узла равна -1, высота листового узла равна 0\n    return node === null ? -1 : node.height;\n}\n\n/* Обновить высоту узла */\nupdateHeight(node: TreeNode): void {\n    // Высота узла равна высоте более высокого поддерева + 1\n    node.height =\n        Math.max(this.height(node.left), this.height(node.right)) + 1;\n}\n
        avl_tree.dart
        /* Получить высоту узла */\nint height(TreeNode? node) {\n  // Высота пустого узла равна -1, высота листового узла равна 0\n  return node == null ? -1 : node.height;\n}\n\n/* Обновить высоту узла */\nvoid updateHeight(TreeNode? node) {\n  // Высота узла равна высоте более высокого поддерева + 1\n  node!.height = max(height(node.left), height(node.right)) + 1;\n}\n
        avl_tree.rs
        /* Получить высоту узла */\nfn height(node: OptionTreeNodeRc) -> i32 {\n    // Высота пустого узла равна -1, высота листового узла равна 0\n    match node {\n        Some(node) => node.borrow().height,\n        None => -1,\n    }\n}\n\n/* Обновить высоту узла */\nfn update_height(node: OptionTreeNodeRc) {\n    if let Some(node) = node {\n        let left = node.borrow().left.clone();\n        let right = node.borrow().right.clone();\n        // Высота узла равна высоте более высокого поддерева + 1\n        node.borrow_mut().height = std::cmp::max(Self::height(left), Self::height(right)) + 1;\n    }\n}\n
        avl_tree.c
        /* Получить высоту узла */\nint height(TreeNode *node) {\n    // Высота пустого узла равна -1, высота листового узла равна 0\n    if (node != NULL) {\n        return node->height;\n    }\n    return -1;\n}\n\n/* Обновить высоту узла */\nvoid updateHeight(TreeNode *node) {\n    int lh = height(node->left);\n    int rh = height(node->right);\n    // Высота узла равна высоте более высокого поддерева + 1\n    if (lh > rh) {\n        node->height = lh + 1;\n    } else {\n        node->height = rh + 1;\n    }\n}\n
        avl_tree.kt
        /* Получить высоту узла */\nfun height(node: TreeNode?): Int {\n    // Высота пустого узла равна -1, высота листового узла равна 0\n    return node?.height ?: -1\n}\n\n/* Обновить высоту узла */\nfun updateHeight(node: TreeNode?) {\n    // Высота узла равна высоте более высокого поддерева + 1\n    node?.height = max(height(node?.left), height(node?.right)) + 1\n}\n
        avl_tree.rb
        ### Получить высоту узла ###\ndef height(node)\n  # Высота пустого узла равна -1, высота листового узла равна 0\n  return node.height unless node.nil?\n\n  -1\nend\n\n### Обновить высоту узла ###\ndef update_height(node)\n  # Высота узла равна высоте более высокого поддерева + 1\n  node.height = [height(node.left), height(node.right)].max + 1\nend\n
        ","path":["Глава 7. Деревья","7.5   AVL-дерево *"],"tags":[]},{"location":"chapter_tree/avl_tree/#2-","level":3,"title":"2.   Баланс-фактор узла","text":"

        Баланс-фактор (balance factor) узла определяется как высота левого поддерева минус высота правого поддерева. При этом баланс-фактор пустого узла считается равным \\(0\\) . Мы также инкапсулируем получение баланс-фактора в отдельную функцию, чтобы потом было удобнее ее использовать:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
        def balance_factor(self, node: TreeNode | None) -> int:\n    \"\"\"Получить коэффициент баланса\"\"\"\n    # Коэффициент баланса пустого узла равен 0\n    if node is None:\n        return 0\n    # Коэффициент баланса узла = высота левого поддерева - высота правого поддерева\n    return self.height(node.left) - self.height(node.right)\n
        avl_tree.cpp
        /* Получить коэффициент баланса */\nint balanceFactor(TreeNode *node) {\n    // Коэффициент баланса пустого узла равен 0\n    if (node == nullptr)\n        return 0;\n    // Коэффициент баланса узла = высота левого поддерева - высота правого поддерева\n    return height(node->left) - height(node->right);\n}\n
        avl_tree.java
        /* Получить коэффициент баланса */\nint balanceFactor(TreeNode node) {\n    // Коэффициент баланса пустого узла равен 0\n    if (node == null)\n        return 0;\n    // Коэффициент баланса узла = высота левого поддерева - высота правого поддерева\n    return height(node.left) - height(node.right);\n}\n
        avl_tree.cs
        /* Получить коэффициент баланса */\nint BalanceFactor(TreeNode? node) {\n    // Коэффициент баланса пустого узла равен 0\n    if (node == null) return 0;\n    // Коэффициент баланса узла = высота левого поддерева - высота правого поддерева\n    return Height(node.left) - Height(node.right);\n}\n
        avl_tree.go
        /* Получить коэффициент баланса */\nfunc (t *aVLTree) balanceFactor(node *TreeNode) int {\n    // Коэффициент баланса пустого узла равен 0\n    if node == nil {\n        return 0\n    }\n    // Коэффициент баланса узла = высота левого поддерева - высота правого поддерева\n    return t.height(node.Left) - t.height(node.Right)\n}\n
        avl_tree.swift
        /* Получить коэффициент баланса */\nfunc balanceFactor(node: TreeNode?) -> Int {\n    // Коэффициент баланса пустого узла равен 0\n    guard let node = node else { return 0 }\n    // Коэффициент баланса узла = высота левого поддерева - высота правого поддерева\n    return height(node: node.left) - height(node: node.right)\n}\n
        avl_tree.js
        /* Получить коэффициент баланса */\nbalanceFactor(node) {\n    // Коэффициент баланса пустого узла равен 0\n    if (node === null) return 0;\n    // Коэффициент баланса узла = высота левого поддерева - высота правого поддерева\n    return this.height(node.left) - this.height(node.right);\n}\n
        avl_tree.ts
        /* Получить коэффициент баланса */\nbalanceFactor(node: TreeNode): number {\n    // Коэффициент баланса пустого узла равен 0\n    if (node === null) return 0;\n    // Коэффициент баланса узла = высота левого поддерева - высота правого поддерева\n    return this.height(node.left) - this.height(node.right);\n}\n
        avl_tree.dart
        /* Получить коэффициент баланса */\nint balanceFactor(TreeNode? node) {\n  // Коэффициент баланса пустого узла равен 0\n  if (node == null) return 0;\n  // Коэффициент баланса узла = высота левого поддерева - высота правого поддерева\n  return height(node.left) - height(node.right);\n}\n
        avl_tree.rs
        /* Получить коэффициент баланса */\nfn balance_factor(node: OptionTreeNodeRc) -> i32 {\n    match node {\n        // Коэффициент баланса пустого узла равен 0\n        None => 0,\n        // Коэффициент баланса узла = высота левого поддерева - высота правого поддерева\n        Some(node) => {\n            Self::height(node.borrow().left.clone()) - Self::height(node.borrow().right.clone())\n        }\n    }\n}\n
        avl_tree.c
        /* Получить коэффициент баланса */\nint balanceFactor(TreeNode *node) {\n    // Коэффициент баланса пустого узла равен 0\n    if (node == NULL) {\n        return 0;\n    }\n    // Коэффициент баланса узла = высота левого поддерева - высота правого поддерева\n    return height(node->left) - height(node->right);\n}\n
        avl_tree.kt
        /* Получить коэффициент баланса */\nfun balanceFactor(node: TreeNode?): Int {\n    // Коэффициент баланса пустого узла равен 0\n    if (node == null) return 0\n    // Коэффициент баланса узла = высота левого поддерева - высота правого поддерева\n    return height(node.left) - height(node.right)\n}\n
        avl_tree.rb
        ### Получить коэффициент баланса ###\ndef balance_factor(node)\n  # Коэффициент баланса пустого узла равен 0\n  return 0 if node.nil?\n\n  # Коэффициент баланса узла = высота левого поддерева - высота правого поддерева\n  height(node.left) - height(node.right)\nend\n

        Tip

        Пусть баланс-фактор равен \\(f\\). Тогда для любого узла AVL-дерева выполняется \\(-1 \\le f \\le 1\\) .

        ","path":["Глава 7. Деревья","7.5   AVL-дерево *"],"tags":[]},{"location":"chapter_tree/avl_tree/#752-avl-","level":2,"title":"7.5.2   Вращения AVL-дерева","text":"

        Особенность AVL-дерева заключается в операции «вращения», которая позволяет заново сбалансировать разбалансированный узел, не нарушая последовательность симметричного обхода двоичного дерева. Иначе говоря, операция вращения одновременно сохраняет свойство «двоичного дерева поиска» и возвращает дерево в состояние «сбалансированного двоичного дерева».

        Узлы, для которых абсолютное значение баланс-фактора больше \\(1\\) , мы называем «разбалансированными узлами». В зависимости от вида разбаланса вращения делятся на четыре типа: правое вращение, левое вращение, сначала левое затем правое, и сначала правое затем левое. Ниже разберем их подробно.

        ","path":["Глава 7. Деревья","7.5   AVL-дерево *"],"tags":[]},{"location":"chapter_tree/avl_tree/#1_1","level":3,"title":"1.   Правое вращение","text":"

        Как показано на рисунке 7-26, под узлом указан его баланс-фактор. Если двигаться снизу вверх, то первым разбалансированным узлом в двоичном дереве будет «узел 3». Рассмотрим поддерево с этим узлом в качестве корня, обозначим данный узел как node , его левого дочернего узла как child и выполним «правое вращение». После завершения правого вращения поддерево снова станет сбалансированным и при этом сохранит свойство двоичного дерева поиска.

        <1><2><3><4>

        Рисунок 7-26   Шаги правого вращения

        Как показано на рисунке 7-27, когда у узла child есть правый дочерний узел, который мы обозначим как grand_child , в правое вращение нужно добавить еще один шаг: сделать grand_child левым дочерним узлом node .

        Рисунок 7-27   Правое вращение при наличии grand_child

        «Поворот вправо» - это лишь образное описание. В реальности он реализуется через изменение указателей узлов. Код приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
        def right_rotate(self, node: TreeNode | None) -> TreeNode | None:\n    \"\"\"Операция правого вращения\"\"\"\n    child = node.left\n    grand_child = child.right\n    # Выполнить правое вращение узла node вокруг child\n    child.right = node\n    node.left = grand_child\n    # Обновить высоту узла\n    self.update_height(node)\n    self.update_height(child)\n    # Вернуть корневой узел поддерева после вращения\n    return child\n
        avl_tree.cpp
        /* Операция правого вращения */\nTreeNode *rightRotate(TreeNode *node) {\n    TreeNode *child = node->left;\n    TreeNode *grandChild = child->right;\n    // Выполнить правое вращение узла node вокруг child\n    child->right = node;\n    node->left = grandChild;\n    // Обновить высоту узла\n    updateHeight(node);\n    updateHeight(child);\n    // Вернуть корневой узел поддерева после вращения\n    return child;\n}\n
        avl_tree.java
        /* Операция правого вращения */\nTreeNode rightRotate(TreeNode node) {\n    TreeNode child = node.left;\n    TreeNode grandChild = child.right;\n    // Выполнить правое вращение узла node вокруг child\n    child.right = node;\n    node.left = grandChild;\n    // Обновить высоту узла\n    updateHeight(node);\n    updateHeight(child);\n    // Вернуть корневой узел поддерева после вращения\n    return child;\n}\n
        avl_tree.cs
        /* Операция правого вращения */\nTreeNode? RightRotate(TreeNode? node) {\n    TreeNode? child = node?.left;\n    TreeNode? grandChild = child?.right;\n    // Выполнить правое вращение узла node вокруг child\n    child.right = node;\n    node.left = grandChild;\n    // Обновить высоту узла\n    UpdateHeight(node);\n    UpdateHeight(child);\n    // Вернуть корневой узел поддерева после вращения\n    return child;\n}\n
        avl_tree.go
        /* Операция правого вращения */\nfunc (t *aVLTree) rightRotate(node *TreeNode) *TreeNode {\n    child := node.Left\n    grandChild := child.Right\n    // Выполнить правое вращение узла node вокруг child\n    child.Right = node\n    node.Left = grandChild\n    // Обновить высоту узла\n    t.updateHeight(node)\n    t.updateHeight(child)\n    // Вернуть корневой узел поддерева после вращения\n    return child\n}\n
        avl_tree.swift
        /* Операция правого вращения */\nfunc rightRotate(node: TreeNode?) -> TreeNode? {\n    let child = node?.left\n    let grandChild = child?.right\n    // Выполнить правое вращение узла node вокруг child\n    child?.right = node\n    node?.left = grandChild\n    // Обновить высоту узла\n    updateHeight(node: node)\n    updateHeight(node: child)\n    // Вернуть корневой узел поддерева после вращения\n    return child\n}\n
        avl_tree.js
        /* Операция правого вращения */\n#rightRotate(node) {\n    const child = node.left;\n    const grandChild = child.right;\n    // Выполнить правое вращение узла node вокруг child\n    child.right = node;\n    node.left = grandChild;\n    // Обновить высоту узла\n    this.#updateHeight(node);\n    this.#updateHeight(child);\n    // Вернуть корневой узел поддерева после вращения\n    return child;\n}\n
        avl_tree.ts
        /* Операция правого вращения */\nrightRotate(node: TreeNode): TreeNode {\n    const child = node.left;\n    const grandChild = child.right;\n    // Выполнить правое вращение узла node вокруг child\n    child.right = node;\n    node.left = grandChild;\n    // Обновить высоту узла\n    this.updateHeight(node);\n    this.updateHeight(child);\n    // Вернуть корневой узел поддерева после вращения\n    return child;\n}\n
        avl_tree.dart
        /* Операция правого вращения */\nTreeNode? rightRotate(TreeNode? node) {\n  TreeNode? child = node!.left;\n  TreeNode? grandChild = child!.right;\n  // Выполнить правое вращение узла node вокруг child\n  child.right = node;\n  node.left = grandChild;\n  // Обновить высоту узла\n  updateHeight(node);\n  updateHeight(child);\n  // Вернуть корневой узел поддерева после вращения\n  return child;\n}\n
        avl_tree.rs
        /* Операция правого вращения */\nfn right_rotate(node: OptionTreeNodeRc) -> OptionTreeNodeRc {\n    match node {\n        Some(node) => {\n            let child = node.borrow().left.clone().unwrap();\n            let grand_child = child.borrow().right.clone();\n            // Выполнить правое вращение узла node вокруг child\n            child.borrow_mut().right = Some(node.clone());\n            node.borrow_mut().left = grand_child;\n            // Обновить высоту узла\n            Self::update_height(Some(node));\n            Self::update_height(Some(child.clone()));\n            // Вернуть корневой узел поддерева после вращения\n            Some(child)\n        }\n        None => None,\n    }\n}\n
        avl_tree.c
        /* Операция правого вращения */\nTreeNode *rightRotate(TreeNode *node) {\n    TreeNode *child, *grandChild;\n    child = node->left;\n    grandChild = child->right;\n    // Выполнить правое вращение узла node вокруг child\n    child->right = node;\n    node->left = grandChild;\n    // Обновить высоту узла\n    updateHeight(node);\n    updateHeight(child);\n    // Вернуть корневой узел поддерева после вращения\n    return child;\n}\n
        avl_tree.kt
        /* Операция правого вращения */\nfun rightRotate(node: TreeNode?): TreeNode {\n    val child = node!!.left\n    val grandChild = child!!.right\n    // Выполнить правое вращение узла node вокруг child\n    child.right = node\n    node.left = grandChild\n    // Обновить высоту узла\n    updateHeight(node)\n    updateHeight(child)\n    // Вернуть корневой узел поддерева после вращения\n    return child\n}\n
        avl_tree.rb
        ### Операция правого вращения ###\ndef right_rotate(node)\n  child = node.left\n  grand_child = child.right\n  # Выполнить правое вращение узла node вокруг child\n  child.right = node\n  node.left = grand_child\n  # Обновить высоту узла\n  update_height(node)\n  update_height(child)\n  # Вернуть корневой узел поддерева после вращения\n  child\nend\n
        ","path":["Глава 7. Деревья","7.5   AVL-дерево *"],"tags":[]},{"location":"chapter_tree/avl_tree/#2","level":3,"title":"2.   Левое вращение","text":"

        Соответственно, если рассмотреть «зеркальную» версию приведенного выше разбалансированного двоичного дерева, то понадобится выполнить «левое вращение», показанное на рисунке 7-28.

        Рисунок 7-28   Левое вращение

        Аналогичная ситуация показана на рисунке 7-29. Если у узла child есть левый дочерний узел, который обозначим как grand_child , то в левое вращение также требуется добавить шаг: сделать grand_child правым дочерним узлом node .

        Рисунок 7-29   Левое вращение при наличии grand_child

        Можно заметить, что операции правого и левого вращения логически зеркально симметричны, и два вида разбаланса, которые они исправляют, тоже симметричны. Поэтому, опираясь на эту симметрию, достаточно заменить в коде правого вращения все left на right , а все right на left , чтобы получить реализацию левого вращения:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
        def left_rotate(self, node: TreeNode | None) -> TreeNode | None:\n    \"\"\"Операция левого вращения\"\"\"\n    child = node.right\n    grand_child = child.left\n    # Выполнить левое вращение узла node вокруг child\n    child.left = node\n    node.right = grand_child\n    # Обновить высоту узла\n    self.update_height(node)\n    self.update_height(child)\n    # Вернуть корневой узел поддерева после вращения\n    return child\n
        avl_tree.cpp
        /* Операция левого вращения */\nTreeNode *leftRotate(TreeNode *node) {\n    TreeNode *child = node->right;\n    TreeNode *grandChild = child->left;\n    // Выполнить левое вращение узла node вокруг child\n    child->left = node;\n    node->right = grandChild;\n    // Обновить высоту узла\n    updateHeight(node);\n    updateHeight(child);\n    // Вернуть корневой узел поддерева после вращения\n    return child;\n}\n
        avl_tree.java
        /* Операция левого вращения */\nTreeNode leftRotate(TreeNode node) {\n    TreeNode child = node.right;\n    TreeNode grandChild = child.left;\n    // Выполнить левое вращение узла node вокруг child\n    child.left = node;\n    node.right = grandChild;\n    // Обновить высоту узла\n    updateHeight(node);\n    updateHeight(child);\n    // Вернуть корневой узел поддерева после вращения\n    return child;\n}\n
        avl_tree.cs
        /* Операция левого вращения */\nTreeNode? LeftRotate(TreeNode? node) {\n    TreeNode? child = node?.right;\n    TreeNode? grandChild = child?.left;\n    // Выполнить левое вращение узла node вокруг child\n    child.left = node;\n    node.right = grandChild;\n    // Обновить высоту узла\n    UpdateHeight(node);\n    UpdateHeight(child);\n    // Вернуть корневой узел поддерева после вращения\n    return child;\n}\n
        avl_tree.go
        /* Операция левого вращения */\nfunc (t *aVLTree) leftRotate(node *TreeNode) *TreeNode {\n    child := node.Right\n    grandChild := child.Left\n    // Выполнить левое вращение узла node вокруг child\n    child.Left = node\n    node.Right = grandChild\n    // Обновить высоту узла\n    t.updateHeight(node)\n    t.updateHeight(child)\n    // Вернуть корневой узел поддерева после вращения\n    return child\n}\n
        avl_tree.swift
        /* Операция левого вращения */\nfunc leftRotate(node: TreeNode?) -> TreeNode? {\n    let child = node?.right\n    let grandChild = child?.left\n    // Выполнить левое вращение узла node вокруг child\n    child?.left = node\n    node?.right = grandChild\n    // Обновить высоту узла\n    updateHeight(node: node)\n    updateHeight(node: child)\n    // Вернуть корневой узел поддерева после вращения\n    return child\n}\n
        avl_tree.js
        /* Операция левого вращения */\n#leftRotate(node) {\n    const child = node.right;\n    const grandChild = child.left;\n    // Выполнить левое вращение узла node вокруг child\n    child.left = node;\n    node.right = grandChild;\n    // Обновить высоту узла\n    this.#updateHeight(node);\n    this.#updateHeight(child);\n    // Вернуть корневой узел поддерева после вращения\n    return child;\n}\n
        avl_tree.ts
        /* Операция левого вращения */\nleftRotate(node: TreeNode): TreeNode {\n    const child = node.right;\n    const grandChild = child.left;\n    // Выполнить левое вращение узла node вокруг child\n    child.left = node;\n    node.right = grandChild;\n    // Обновить высоту узла\n    this.updateHeight(node);\n    this.updateHeight(child);\n    // Вернуть корневой узел поддерева после вращения\n    return child;\n}\n
        avl_tree.dart
        /* Операция левого вращения */\nTreeNode? leftRotate(TreeNode? node) {\n  TreeNode? child = node!.right;\n  TreeNode? grandChild = child!.left;\n  // Выполнить левое вращение узла node вокруг child\n  child.left = node;\n  node.right = grandChild;\n  // Обновить высоту узла\n  updateHeight(node);\n  updateHeight(child);\n  // Вернуть корневой узел поддерева после вращения\n  return child;\n}\n
        avl_tree.rs
        /* Операция левого вращения */\nfn left_rotate(node: OptionTreeNodeRc) -> OptionTreeNodeRc {\n    match node {\n        Some(node) => {\n            let child = node.borrow().right.clone().unwrap();\n            let grand_child = child.borrow().left.clone();\n            // Выполнить левое вращение узла node вокруг child\n            child.borrow_mut().left = Some(node.clone());\n            node.borrow_mut().right = grand_child;\n            // Обновить высоту узла\n            Self::update_height(Some(node));\n            Self::update_height(Some(child.clone()));\n            // Вернуть корневой узел поддерева после вращения\n            Some(child)\n        }\n        None => None,\n    }\n}\n
        avl_tree.c
        /* Операция левого вращения */\nTreeNode *leftRotate(TreeNode *node) {\n    TreeNode *child, *grandChild;\n    child = node->right;\n    grandChild = child->left;\n    // Выполнить левое вращение узла node вокруг child\n    child->left = node;\n    node->right = grandChild;\n    // Обновить высоту узла\n    updateHeight(node);\n    updateHeight(child);\n    // Вернуть корневой узел поддерева после вращения\n    return child;\n}\n
        avl_tree.kt
        /* Операция левого вращения */\nfun leftRotate(node: TreeNode?): TreeNode {\n    val child = node!!.right\n    val grandChild = child!!.left\n    // Выполнить левое вращение узла node вокруг child\n    child.left = node\n    node.right = grandChild\n    // Обновить высоту узла\n    updateHeight(node)\n    updateHeight(child)\n    // Вернуть корневой узел поддерева после вращения\n    return child\n}\n
        avl_tree.rb
        ### Операция левого вращения ###\ndef left_rotate(node)\n  child = node.right\n  grand_child = child.left\n  # Выполнить левое вращение узла node вокруг child\n  child.left = node\n  node.right = grand_child\n  # Обновить высоту узла\n  update_height(node)\n  update_height(child)\n  # Вернуть корневой узел поддерева после вращения\n  child\nend\n
        ","path":["Глава 7. Деревья","7.5   AVL-дерево *"],"tags":[]},{"location":"chapter_tree/avl_tree/#3","level":3,"title":"3.   Сначала левое, затем правое вращение","text":"

        Для разбалансированного узла 3 на рисунке 7-30 ни одно лишь левое вращение, ни одно лишь правое вращение не способны вернуть поддерево в баланс. В этом случае нужно сначала выполнить «левое вращение» для child , а затем выполнить «правое вращение» для node .

        Рисунок 7-30   Сначала левое, затем правое вращение

        ","path":["Глава 7. Деревья","7.5   AVL-дерево *"],"tags":[]},{"location":"chapter_tree/avl_tree/#4","level":3,"title":"4.   Сначала правое, затем левое вращение","text":"

        Как показано на рисунке 7-31, для зеркальной ситуации предыдущего разбалансированного двоичного дерева нужно сначала выполнить «правое вращение» для child , а затем «левое вращение» для node .

        Рисунок 7-31   Сначала правое, затем левое вращение

        ","path":["Глава 7. Деревья","7.5   AVL-дерево *"],"tags":[]},{"location":"chapter_tree/avl_tree/#5","level":3,"title":"5.   Выбор вращения","text":"

        Четыре вида разбаланса, показанные на рисунке 7-32, по одному соответствуют рассмотренным выше случаям. Для них соответственно требуются правое вращение, сначала левое затем правое, сначала правое затем левое и левое вращение.

        Рисунок 7-32   Четыре случая вращений AVL-дерева

        Как показано в таблице 7-3, мы определяем, какому из случаев на рисунке 7-32 соответствует разбалансированный узел, по знаку баланс-фактора самого разбалансированного узла и по знаку баланс-фактора дочернего узла на более высокой стороне.

        Таблица 7-3   Условия выбора для четырех случаев вращений

        Баланс-фактор разбалансированного узла Баланс-фактор дочернего узла Какое вращение использовать \\(> 1\\) (левостороннее дерево) \\(\\geq 0\\) Правое вращение \\(> 1\\) (левостороннее дерево) \\(<0\\) Сначала левое, затем правое \\(< -1\\) (правостороннее дерево) \\(\\leq 0\\) Левое вращение \\(< -1\\) (правостороннее дерево) \\(>0\\) Сначала правое, затем левое

        Для удобства мы инкапсулируем операцию вращения в отдельную функцию. С помощью этой функции можно выполнить корректное вращение для любой ситуации разбаланса и снова привести узел в сбалансированное состояние. Код приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
        def rotate(self, node: TreeNode | None) -> TreeNode | None:\n    \"\"\"Выполнить вращение, чтобы снова сбалансировать поддерево\"\"\"\n    # Получить коэффициент баланса узла node\n    balance_factor = self.balance_factor(node)\n    # Левосторонне перекошенное дерево\n    if balance_factor > 1:\n        if self.balance_factor(node.left) >= 0:\n            # Правое вращение\n            return self.right_rotate(node)\n        else:\n            # Сначала левое вращение, затем правое\n            node.left = self.left_rotate(node.left)\n            return self.right_rotate(node)\n    # Правосторонне перекошенное дерево\n    elif balance_factor < -1:\n        if self.balance_factor(node.right) <= 0:\n            # Левое вращение\n            return self.left_rotate(node)\n        else:\n            # Сначала правое вращение, затем левое\n            node.right = self.right_rotate(node.right)\n            return self.left_rotate(node)\n    # Дерево сбалансировано, вращение не требуется, вернуть сразу\n    return node\n
        avl_tree.cpp
        /* Выполнить вращение, чтобы снова сбалансировать поддерево */\nTreeNode *rotate(TreeNode *node) {\n    // Получить коэффициент баланса узла node\n    int _balanceFactor = balanceFactor(node);\n    // Левосторонне перекошенное дерево\n    if (_balanceFactor > 1) {\n        if (balanceFactor(node->left) >= 0) {\n            // Правое вращение\n            return rightRotate(node);\n        } else {\n            // Сначала левое вращение, затем правое\n            node->left = leftRotate(node->left);\n            return rightRotate(node);\n        }\n    }\n    // Правосторонне перекошенное дерево\n    if (_balanceFactor < -1) {\n        if (balanceFactor(node->right) <= 0) {\n            // Левое вращение\n            return leftRotate(node);\n        } else {\n            // Сначала правое вращение, затем левое\n            node->right = rightRotate(node->right);\n            return leftRotate(node);\n        }\n    }\n    // Дерево сбалансировано, вращение не требуется, вернуть сразу\n    return node;\n}\n
        avl_tree.java
        /* Выполнить вращение, чтобы снова сбалансировать поддерево */\nTreeNode rotate(TreeNode node) {\n    // Получить коэффициент баланса узла node\n    int balanceFactor = balanceFactor(node);\n    // Левосторонне перекошенное дерево\n    if (balanceFactor > 1) {\n        if (balanceFactor(node.left) >= 0) {\n            // Правое вращение\n            return rightRotate(node);\n        } else {\n            // Сначала левое вращение, затем правое\n            node.left = leftRotate(node.left);\n            return rightRotate(node);\n        }\n    }\n    // Правосторонне перекошенное дерево\n    if (balanceFactor < -1) {\n        if (balanceFactor(node.right) <= 0) {\n            // Левое вращение\n            return leftRotate(node);\n        } else {\n            // Сначала правое вращение, затем левое\n            node.right = rightRotate(node.right);\n            return leftRotate(node);\n        }\n    }\n    // Дерево сбалансировано, вращение не требуется, вернуть сразу\n    return node;\n}\n
        avl_tree.cs
        /* Выполнить вращение, чтобы снова сбалансировать поддерево */\nTreeNode? Rotate(TreeNode? node) {\n    // Получить коэффициент баланса узла node\n    int balanceFactorInt = BalanceFactor(node);\n    // Левосторонне перекошенное дерево\n    if (balanceFactorInt > 1) {\n        if (BalanceFactor(node?.left) >= 0) {\n            // Правое вращение\n            return RightRotate(node);\n        } else {\n            // Сначала левое вращение, затем правое\n            node!.left = LeftRotate(node!.left);\n            return RightRotate(node);\n        }\n    }\n    // Правосторонне перекошенное дерево\n    if (balanceFactorInt < -1) {\n        if (BalanceFactor(node?.right) <= 0) {\n            // Левое вращение\n            return LeftRotate(node);\n        } else {\n            // Сначала правое вращение, затем левое\n            node!.right = RightRotate(node!.right);\n            return LeftRotate(node);\n        }\n    }\n    // Дерево сбалансировано, вращение не требуется, вернуть сразу\n    return node;\n}\n
        avl_tree.go
        /* Выполнить вращение, чтобы снова сбалансировать поддерево */\nfunc (t *aVLTree) rotate(node *TreeNode) *TreeNode {\n    // Получить коэффициент баланса узла node\n    // В Go рекомендуется использовать короткие имена переменных, здесь bf обозначает t.balanceFactor\n    bf := t.balanceFactor(node)\n    // Левосторонне перекошенное дерево\n    if bf > 1 {\n        if t.balanceFactor(node.Left) >= 0 {\n            // Правое вращение\n            return t.rightRotate(node)\n        } else {\n            // Сначала левое вращение, затем правое\n            node.Left = t.leftRotate(node.Left)\n            return t.rightRotate(node)\n        }\n    }\n    // Правосторонне перекошенное дерево\n    if bf < -1 {\n        if t.balanceFactor(node.Right) <= 0 {\n            // Левое вращение\n            return t.leftRotate(node)\n        } else {\n            // Сначала правое вращение, затем левое\n            node.Right = t.rightRotate(node.Right)\n            return t.leftRotate(node)\n        }\n    }\n    // Дерево сбалансировано, вращение не требуется, вернуть сразу\n    return node\n}\n
        avl_tree.swift
        /* Выполнить вращение, чтобы снова сбалансировать поддерево */\nfunc rotate(node: TreeNode?) -> TreeNode? {\n    // Получить коэффициент баланса узла node\n    let balanceFactor = balanceFactor(node: node)\n    // Левосторонне перекошенное дерево\n    if balanceFactor > 1 {\n        if self.balanceFactor(node: node?.left) >= 0 {\n            // Правое вращение\n            return rightRotate(node: node)\n        } else {\n            // Сначала левое вращение, затем правое\n            node?.left = leftRotate(node: node?.left)\n            return rightRotate(node: node)\n        }\n    }\n    // Правосторонне перекошенное дерево\n    if balanceFactor < -1 {\n        if self.balanceFactor(node: node?.right) <= 0 {\n            // Левое вращение\n            return leftRotate(node: node)\n        } else {\n            // Сначала правое вращение, затем левое\n            node?.right = rightRotate(node: node?.right)\n            return leftRotate(node: node)\n        }\n    }\n    // Дерево сбалансировано, вращение не требуется, вернуть сразу\n    return node\n}\n
        avl_tree.js
        /* Выполнить вращение, чтобы снова сбалансировать поддерево */\n#rotate(node) {\n    // Получить коэффициент баланса узла node\n    const balanceFactor = this.balanceFactor(node);\n    // Левосторонне перекошенное дерево\n    if (balanceFactor > 1) {\n        if (this.balanceFactor(node.left) >= 0) {\n            // Правое вращение\n            return this.#rightRotate(node);\n        } else {\n            // Сначала левое вращение, затем правое\n            node.left = this.#leftRotate(node.left);\n            return this.#rightRotate(node);\n        }\n    }\n    // Правосторонне перекошенное дерево\n    if (balanceFactor < -1) {\n        if (this.balanceFactor(node.right) <= 0) {\n            // Левое вращение\n            return this.#leftRotate(node);\n        } else {\n            // Сначала правое вращение, затем левое\n            node.right = this.#rightRotate(node.right);\n            return this.#leftRotate(node);\n        }\n    }\n    // Дерево сбалансировано, вращение не требуется, вернуть сразу\n    return node;\n}\n
        avl_tree.ts
        /* Выполнить вращение, чтобы снова сбалансировать поддерево */\nrotate(node: TreeNode): TreeNode {\n    // Получить коэффициент баланса узла node\n    const balanceFactor = this.balanceFactor(node);\n    // Левосторонне перекошенное дерево\n    if (balanceFactor > 1) {\n        if (this.balanceFactor(node.left) >= 0) {\n            // Правое вращение\n            return this.rightRotate(node);\n        } else {\n            // Сначала левое вращение, затем правое\n            node.left = this.leftRotate(node.left);\n            return this.rightRotate(node);\n        }\n    }\n    // Правосторонне перекошенное дерево\n    if (balanceFactor < -1) {\n        if (this.balanceFactor(node.right) <= 0) {\n            // Левое вращение\n            return this.leftRotate(node);\n        } else {\n            // Сначала правое вращение, затем левое\n            node.right = this.rightRotate(node.right);\n            return this.leftRotate(node);\n        }\n    }\n    // Дерево сбалансировано, вращение не требуется, вернуть сразу\n    return node;\n}\n
        avl_tree.dart
        /* Выполнить вращение, чтобы снова сбалансировать поддерево */\nTreeNode? rotate(TreeNode? node) {\n  // Получить коэффициент баланса узла node\n  int factor = balanceFactor(node);\n  // Левосторонне перекошенное дерево\n  if (factor > 1) {\n    if (balanceFactor(node!.left) >= 0) {\n      // Правое вращение\n      return rightRotate(node);\n    } else {\n      // Сначала левое вращение, затем правое\n      node.left = leftRotate(node.left);\n      return rightRotate(node);\n    }\n  }\n  // Правосторонне перекошенное дерево\n  if (factor < -1) {\n    if (balanceFactor(node!.right) <= 0) {\n      // Левое вращение\n      return leftRotate(node);\n    } else {\n      // Сначала правое вращение, затем левое\n      node.right = rightRotate(node.right);\n      return leftRotate(node);\n    }\n  }\n  // Дерево сбалансировано, вращение не требуется, вернуть сразу\n  return node;\n}\n
        avl_tree.rs
        /* Выполнить вращение, чтобы снова сбалансировать поддерево */\nfn rotate(node: OptionTreeNodeRc) -> OptionTreeNodeRc {\n    // Получить коэффициент баланса узла node\n    let balance_factor = Self::balance_factor(node.clone());\n    // Левосторонне перекошенное дерево\n    if balance_factor > 1 {\n        let node = node.unwrap();\n        if Self::balance_factor(node.borrow().left.clone()) >= 0 {\n            // Правое вращение\n            Self::right_rotate(Some(node))\n        } else {\n            // Сначала левое вращение, затем правое\n            let left = node.borrow().left.clone();\n            node.borrow_mut().left = Self::left_rotate(left);\n            Self::right_rotate(Some(node))\n        }\n    }\n    // Правосторонне перекошенное дерево\n    else if balance_factor < -1 {\n        let node = node.unwrap();\n        if Self::balance_factor(node.borrow().right.clone()) <= 0 {\n            // Левое вращение\n            Self::left_rotate(Some(node))\n        } else {\n            // Сначала правое вращение, затем левое\n            let right = node.borrow().right.clone();\n            node.borrow_mut().right = Self::right_rotate(right);\n            Self::left_rotate(Some(node))\n        }\n    } else {\n        // Дерево сбалансировано, вращение не требуется, вернуть сразу\n        node\n    }\n}\n
        avl_tree.c
        /* Выполнить вращение, чтобы снова сбалансировать поддерево */\nTreeNode *rotate(TreeNode *node) {\n    // Получить коэффициент баланса узла node\n    int bf = balanceFactor(node);\n    // Левосторонне перекошенное дерево\n    if (bf > 1) {\n        if (balanceFactor(node->left) >= 0) {\n            // Правое вращение\n            return rightRotate(node);\n        } else {\n            // Сначала левое вращение, затем правое\n            node->left = leftRotate(node->left);\n            return rightRotate(node);\n        }\n    }\n    // Правосторонне перекошенное дерево\n    if (bf < -1) {\n        if (balanceFactor(node->right) <= 0) {\n            // Левое вращение\n            return leftRotate(node);\n        } else {\n            // Сначала правое вращение, затем левое\n            node->right = rightRotate(node->right);\n            return leftRotate(node);\n        }\n    }\n    // Дерево сбалансировано, вращение не требуется, вернуть сразу\n    return node;\n}\n
        avl_tree.kt
        /* Выполнить вращение, чтобы снова сбалансировать поддерево */\nfun rotate(node: TreeNode): TreeNode {\n    // Получить коэффициент баланса узла node\n    val balanceFactor = balanceFactor(node)\n    // Левосторонне перекошенное дерево\n    if (balanceFactor > 1) {\n        if (balanceFactor(node.left) >= 0) {\n            // Правое вращение\n            return rightRotate(node)\n        } else {\n            // Сначала левое вращение, затем правое\n            node.left = leftRotate(node.left)\n            return rightRotate(node)\n        }\n    }\n    // Правосторонне перекошенное дерево\n    if (balanceFactor < -1) {\n        if (balanceFactor(node.right) <= 0) {\n            // Левое вращение\n            return leftRotate(node)\n        } else {\n            // Сначала правое вращение, затем левое\n            node.right = rightRotate(node.right)\n            return leftRotate(node)\n        }\n    }\n    // Дерево сбалансировано, вращение не требуется, вернуть сразу\n    return node\n}\n
        avl_tree.rb
        ### Выполнить вращение, чтобы снова сбалансировать поддерево ###\ndef rotate(node)\n  # Получить коэффициент баланса узла node\n  balance_factor = balance_factor(node)\n  # Обойти левое поддерево\n  if balance_factor > 1\n    if balance_factor(node.left) >= 0\n      # Правое вращение\n      return right_rotate(node)\n    else\n      # Сначала левое вращение, затем правое\n      node.left = left_rotate(node.left)\n      return right_rotate(node)\n    end\n  # Правостороннее дерево обхода\n  elsif balance_factor < -1\n    if balance_factor(node.right) <= 0\n      # Левое вращение\n      return left_rotate(node)\n    else\n      # Сначала правое вращение, затем левое\n      node.right = right_rotate(node.right)\n      return left_rotate(node)\n    end\n  end\n  # Дерево сбалансировано, вращение не требуется, вернуть сразу\n  node\nend\n
        ","path":["Глава 7. Деревья","7.5   AVL-дерево *"],"tags":[]},{"location":"chapter_tree/avl_tree/#753-avl-","level":2,"title":"7.5.3   Распространенные операции AVL-дерева","text":"","path":["Глава 7. Деревья","7.5   AVL-дерево *"],"tags":[]},{"location":"chapter_tree/avl_tree/#1_2","level":3,"title":"1.   Вставка узла","text":"

        Операция вставки узла в AVL-дерево по основному процессу похожа на вставку в двоичное дерево поиска. Единственная разница состоит в том, что после вставки в AVL-дерево на пути от вставленного узла к корню может появиться цепочка разбалансированных узлов. Поэтому начиная от этого узла, мы должны выполнять вращения снизу вверх, чтобы вернуть в баланс все разбалансированные узлы. Код приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
        def insert(self, val):\n    \"\"\"Вставка узла\"\"\"\n    self._root = self.insert_helper(self._root, val)\n\ndef insert_helper(self, node: TreeNode | None, val: int) -> TreeNode:\n    \"\"\"Рекурсивная вставка узла (вспомогательный метод)\"\"\"\n    if node is None:\n        return TreeNode(val)\n    # 1. Найти позицию вставки и вставить узел\n    if val < node.val:\n        node.left = self.insert_helper(node.left, val)\n    elif val > node.val:\n        node.right = self.insert_helper(node.right, val)\n    else:\n        # Повторяющийся узел не вставлять, сразу вернуть\n        return node\n    # Обновить высоту узла\n    self.update_height(node)\n    # 2. Выполнить вращение, чтобы снова сбалансировать поддерево\n    return self.rotate(node)\n
        avl_tree.cpp
        /* Вставка узла */\nvoid insert(int val) {\n    root = insertHelper(root, val);\n}\n\n/* Рекурсивная вставка узла (вспомогательный метод) */\nTreeNode *insertHelper(TreeNode *node, int val) {\n    if (node == nullptr)\n        return new TreeNode(val);\n    /* 1. Найти позицию вставки и вставить узел */\n    if (val < node->val)\n        node->left = insertHelper(node->left, val);\n    else if (val > node->val)\n        node->right = insertHelper(node->right, val);\n    else\n        return node;    // Повторяющийся узел не вставлять, сразу вернуть\n    updateHeight(node); // Обновить высоту узла\n    /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n    node = rotate(node);\n    // Вернуть корневой узел поддерева\n    return node;\n}\n
        avl_tree.java
        /* Вставка узла */\nvoid insert(int val) {\n    root = insertHelper(root, val);\n}\n\n/* Рекурсивная вставка узла (вспомогательный метод) */\nTreeNode insertHelper(TreeNode node, int val) {\n    if (node == null)\n        return new TreeNode(val);\n    /* 1. Найти позицию вставки и вставить узел */\n    if (val < node.val)\n        node.left = insertHelper(node.left, val);\n    else if (val > node.val)\n        node.right = insertHelper(node.right, val);\n    else\n        return node; // Повторяющийся узел не вставлять, сразу вернуть\n    updateHeight(node); // Обновить высоту узла\n    /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n    node = rotate(node);\n    // Вернуть корневой узел поддерева\n    return node;\n}\n
        avl_tree.cs
        /* Вставка узла */\nvoid Insert(int val) {\n    root = InsertHelper(root, val);\n}\n\n/* Рекурсивная вставка узла (вспомогательный метод) */\nTreeNode? InsertHelper(TreeNode? node, int val) {\n    if (node == null) return new TreeNode(val);\n    /* 1. Найти позицию вставки и вставить узел */\n    if (val < node.val)\n        node.left = InsertHelper(node.left, val);\n    else if (val > node.val)\n        node.right = InsertHelper(node.right, val);\n    else\n        return node;     // Повторяющийся узел не вставлять, сразу вернуть\n    UpdateHeight(node);  // Обновить высоту узла\n    /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n    node = Rotate(node);\n    // Вернуть корневой узел поддерева\n    return node;\n}\n
        avl_tree.go
        /* Вставка узла */\nfunc (t *aVLTree) insert(val int) {\n    t.root = t.insertHelper(t.root, val)\n}\n\n/* Рекурсивная вставка узла (вспомогательная функция) */\nfunc (t *aVLTree) insertHelper(node *TreeNode, val int) *TreeNode {\n    if node == nil {\n        return NewTreeNode(val)\n    }\n    /* 1. Найти позицию вставки и вставить узел */\n    if val < node.Val.(int) {\n        node.Left = t.insertHelper(node.Left, val)\n    } else if val > node.Val.(int) {\n        node.Right = t.insertHelper(node.Right, val)\n    } else {\n        // Повторяющийся узел не вставлять, сразу вернуть\n        return node\n    }\n    // Обновить высоту узла\n    t.updateHeight(node)\n    /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n    node = t.rotate(node)\n    // Вернуть корневой узел поддерева\n    return node\n}\n
        avl_tree.swift
        /* Вставка узла */\nfunc insert(val: Int) {\n    root = insertHelper(node: root, val: val)\n}\n\n/* Рекурсивная вставка узла (вспомогательный метод) */\nfunc insertHelper(node: TreeNode?, val: Int) -> TreeNode? {\n    var node = node\n    if node == nil {\n        return TreeNode(x: val)\n    }\n    /* 1. Найти позицию вставки и вставить узел */\n    if val < node!.val {\n        node?.left = insertHelper(node: node?.left, val: val)\n    } else if val > node!.val {\n        node?.right = insertHelper(node: node?.right, val: val)\n    } else {\n        return node // Повторяющийся узел не вставлять, сразу вернуть\n    }\n    updateHeight(node: node) // Обновить высоту узла\n    /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n    node = rotate(node: node)\n    // Вернуть корневой узел поддерева\n    return node\n}\n
        avl_tree.js
        /* Вставка узла */\ninsert(val) {\n    this.root = this.#insertHelper(this.root, val);\n}\n\n/* Рекурсивная вставка узла (вспомогательный метод) */\n#insertHelper(node, val) {\n    if (node === null) return new TreeNode(val);\n    /* 1. Найти позицию вставки и вставить узел */\n    if (val < node.val) node.left = this.#insertHelper(node.left, val);\n    else if (val > node.val)\n        node.right = this.#insertHelper(node.right, val);\n    else return node; // Повторяющийся узел не вставлять, сразу вернуть\n    this.#updateHeight(node); // Обновить высоту узла\n    /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n    node = this.#rotate(node);\n    // Вернуть корневой узел поддерева\n    return node;\n}\n
        avl_tree.ts
        /* Вставка узла */\ninsert(val: number): void {\n    this.root = this.insertHelper(this.root, val);\n}\n\n/* Рекурсивная вставка узла (вспомогательный метод) */\ninsertHelper(node: TreeNode, val: number): TreeNode {\n    if (node === null) return new TreeNode(val);\n    /* 1. Найти позицию вставки и вставить узел */\n    if (val < node.val) {\n        node.left = this.insertHelper(node.left, val);\n    } else if (val > node.val) {\n        node.right = this.insertHelper(node.right, val);\n    } else {\n        return node; // Повторяющийся узел не вставлять, сразу вернуть\n    }\n    this.updateHeight(node); // Обновить высоту узла\n    /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n    node = this.rotate(node);\n    // Вернуть корневой узел поддерева\n    return node;\n}\n
        avl_tree.dart
        /* Вставка узла */\nvoid insert(int val) {\n  root = insertHelper(root, val);\n}\n\n/* Рекурсивная вставка узла (вспомогательный метод) */\nTreeNode? insertHelper(TreeNode? node, int val) {\n  if (node == null) return TreeNode(val);\n  /* 1. Найти позицию вставки и вставить узел */\n  if (val < node.val)\n    node.left = insertHelper(node.left, val);\n  else if (val > node.val)\n    node.right = insertHelper(node.right, val);\n  else\n    return node; // Повторяющийся узел не вставлять, сразу вернуть\n  updateHeight(node); // Обновить высоту узла\n  /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n  node = rotate(node);\n  // Вернуть корневой узел поддерева\n  return node;\n}\n
        avl_tree.rs
        /* Вставка узла */\nfn insert(&mut self, val: i32) {\n    self.root = Self::insert_helper(self.root.clone(), val);\n}\n\n/* Рекурсивная вставка узла (вспомогательный метод) */\nfn insert_helper(node: OptionTreeNodeRc, val: i32) -> OptionTreeNodeRc {\n    match node {\n        Some(mut node) => {\n            /* 1. Найти позицию вставки и вставить узел */\n            match {\n                let node_val = node.borrow().val;\n                node_val\n            }\n            .cmp(&val)\n            {\n                Ordering::Greater => {\n                    let left = node.borrow().left.clone();\n                    node.borrow_mut().left = Self::insert_helper(left, val);\n                }\n                Ordering::Less => {\n                    let right = node.borrow().right.clone();\n                    node.borrow_mut().right = Self::insert_helper(right, val);\n                }\n                Ordering::Equal => {\n                    return Some(node); // Повторяющийся узел не вставлять, сразу вернуть\n                }\n            }\n            Self::update_height(Some(node.clone())); // Обновить высоту узла\n\n            /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n            node = Self::rotate(Some(node)).unwrap();\n            // Вернуть корневой узел поддерева\n            Some(node)\n        }\n        None => Some(TreeNode::new(val)),\n    }\n}\n
        avl_tree.c
        /* Вставка узла */\nvoid insert(AVLTree *tree, int val) {\n    tree->root = insertHelper(tree->root, val);\n}\n\n/* Рекурсивная вставка узла (вспомогательная функция) */\nTreeNode *insertHelper(TreeNode *node, int val) {\n    if (node == NULL) {\n        return newTreeNode(val);\n    }\n    /* 1. Найти позицию вставки и вставить узел */\n    if (val < node->val) {\n        node->left = insertHelper(node->left, val);\n    } else if (val > node->val) {\n        node->right = insertHelper(node->right, val);\n    } else {\n        // Повторяющийся узел не вставлять, сразу вернуть\n        return node;\n    }\n    // Обновить высоту узла\n    updateHeight(node);\n    /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n    node = rotate(node);\n    // Вернуть корневой узел поддерева\n    return node;\n}\n
        avl_tree.kt
        /* Вставка узла */\nfun insert(_val: Int) {\n    root = insertHelper(root, _val)\n}\n\n/* Рекурсивная вставка узла (вспомогательный метод) */\nfun insertHelper(n: TreeNode?, _val: Int): TreeNode {\n    if (n == null)\n        return TreeNode(_val)\n    var node = n\n    /* 1. Найти позицию вставки и вставить узел */\n    if (_val < node._val)\n        node.left = insertHelper(node.left, _val)\n    else if (_val > node._val)\n        node.right = insertHelper(node.right, _val)\n    else\n        return node // Повторяющийся узел не вставлять, сразу вернуть\n    updateHeight(node) // Обновить высоту узла\n    /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n    node = rotate(node)\n    // Вернуть корневой узел поддерева\n    return node\n}\n
        avl_tree.rb
        ### Вставка узла ###\ndef insert(val)\n  @root = insert_helper(@root, val)\nend\n\n### Вставка узла ###\ndef insert(val)\n  @root = insert_helper(@root, val)\nend\n\n# ## Рекурсивная вставка узла (вспомогательный метод) ###\ndef insert_helper(node, val)\n  return TreeNode.new(val) if node.nil?\n  # 1. Найти позицию вставки и вставить узел\n  if val < node.val\n    node.left = insert_helper(node.left, val)\n  elsif val > node.val\n    node.right = insert_helper(node.right, val)\n  else\n    # Повторяющийся узел не вставлять, сразу вернуть\n    return node\n  end\n  # Обновить высоту узла\n  update_height(node)\n  # 2. Выполнить вращение, чтобы снова сбалансировать поддерево\n  rotate(node)\nend\n
        ","path":["Глава 7. Деревья","7.5   AVL-дерево *"],"tags":[]},{"location":"chapter_tree/avl_tree/#2_1","level":3,"title":"2.   Удаление узла","text":"

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

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
        def remove(self, val: int):\n    \"\"\"Удаление узла\"\"\"\n    self._root = self.remove_helper(self._root, val)\n\ndef remove_helper(self, node: TreeNode | None, val: int) -> TreeNode | None:\n    \"\"\"Рекурсивное удаление узла (вспомогательный метод)\"\"\"\n    if node is None:\n        return None\n    # 1. Найти узел и удалить его\n    if val < node.val:\n        node.left = self.remove_helper(node.left, val)\n    elif val > node.val:\n        node.right = self.remove_helper(node.right, val)\n    else:\n        if node.left is None or node.right is None:\n            child = node.left or node.right\n            # Число дочерних узлов = 0, удалить node и сразу вернуть\n            if child is None:\n                return None\n            # Число дочерних узлов = 1, удалить node напрямую\n            else:\n                node = child\n        else:\n            # Число дочерних узлов = 2, удалить следующий по симметричному обходу узел и заменить им текущий узел\n            temp = node.right\n            while temp.left is not None:\n                temp = temp.left\n            node.right = self.remove_helper(node.right, temp.val)\n            node.val = temp.val\n    # Обновить высоту узла\n    self.update_height(node)\n    # 2. Выполнить вращение, чтобы снова сбалансировать поддерево\n    return self.rotate(node)\n
        avl_tree.cpp
        /* Удаление узла */\nvoid remove(int val) {\n    root = removeHelper(root, val);\n}\n\n/* Рекурсивное удаление узла (вспомогательный метод) */\nTreeNode *removeHelper(TreeNode *node, int val) {\n    if (node == nullptr)\n        return nullptr;\n    /* 1. Найти узел и удалить его */\n    if (val < node->val)\n        node->left = removeHelper(node->left, val);\n    else if (val > node->val)\n        node->right = removeHelper(node->right, val);\n    else {\n        if (node->left == nullptr || node->right == nullptr) {\n            TreeNode *child = node->left != nullptr ? node->left : node->right;\n            // Число дочерних узлов = 0, удалить node и сразу вернуть\n            if (child == nullptr) {\n                delete node;\n                return nullptr;\n            }\n            // Число дочерних узлов = 1, удалить node напрямую\n            else {\n                delete node;\n                node = child;\n            }\n        } else {\n            // Число дочерних узлов = 2, удалить следующий по симметричному обходу узел и заменить им текущий узел\n            TreeNode *temp = node->right;\n            while (temp->left != nullptr) {\n                temp = temp->left;\n            }\n            int tempVal = temp->val;\n            node->right = removeHelper(node->right, temp->val);\n            node->val = tempVal;\n        }\n    }\n    updateHeight(node); // Обновить высоту узла\n    /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n    node = rotate(node);\n    // Вернуть корневой узел поддерева\n    return node;\n}\n
        avl_tree.java
        /* Удаление узла */\nvoid remove(int val) {\n    root = removeHelper(root, val);\n}\n\n/* Рекурсивное удаление узла (вспомогательный метод) */\nTreeNode removeHelper(TreeNode node, int val) {\n    if (node == null)\n        return null;\n    /* 1. Найти узел и удалить его */\n    if (val < node.val)\n        node.left = removeHelper(node.left, val);\n    else if (val > node.val)\n        node.right = removeHelper(node.right, val);\n    else {\n        if (node.left == null || node.right == null) {\n            TreeNode child = node.left != null ? node.left : node.right;\n            // Число дочерних узлов = 0, удалить node и сразу вернуть\n            if (child == null)\n                return null;\n            // Число дочерних узлов = 1, удалить node напрямую\n            else\n                node = child;\n        } else {\n            // Число дочерних узлов = 2, удалить следующий по симметричному обходу узел и заменить им текущий узел\n            TreeNode temp = node.right;\n            while (temp.left != null) {\n                temp = temp.left;\n            }\n            node.right = removeHelper(node.right, temp.val);\n            node.val = temp.val;\n        }\n    }\n    updateHeight(node); // Обновить высоту узла\n    /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n    node = rotate(node);\n    // Вернуть корневой узел поддерева\n    return node;\n}\n
        avl_tree.cs
        /* Удаление узла */\nvoid Remove(int val) {\n    root = RemoveHelper(root, val);\n}\n\n/* Рекурсивное удаление узла (вспомогательный метод) */\nTreeNode? RemoveHelper(TreeNode? node, int val) {\n    if (node == null) return null;\n    /* 1. Найти узел и удалить его */\n    if (val < node.val)\n        node.left = RemoveHelper(node.left, val);\n    else if (val > node.val)\n        node.right = RemoveHelper(node.right, val);\n    else {\n        if (node.left == null || node.right == null) {\n            TreeNode? child = node.left ?? node.right;\n            // Число дочерних узлов = 0, удалить node и сразу вернуть\n            if (child == null)\n                return null;\n            // Число дочерних узлов = 1, удалить node напрямую\n            else\n                node = child;\n        } else {\n            // Число дочерних узлов = 2, удалить следующий по симметричному обходу узел и заменить им текущий узел\n            TreeNode? temp = node.right;\n            while (temp.left != null) {\n                temp = temp.left;\n            }\n            node.right = RemoveHelper(node.right, temp.val!.Value);\n            node.val = temp.val;\n        }\n    }\n    UpdateHeight(node);  // Обновить высоту узла\n    /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n    node = Rotate(node);\n    // Вернуть корневой узел поддерева\n    return node;\n}\n
        avl_tree.go
        /* Удаление узла */\nfunc (t *aVLTree) remove(val int) {\n    t.root = t.removeHelper(t.root, val)\n}\n\n/* Рекурсивное удаление узла (вспомогательная функция) */\nfunc (t *aVLTree) removeHelper(node *TreeNode, val int) *TreeNode {\n    if node == nil {\n        return nil\n    }\n    /* 1. Найти узел и удалить его */\n    if val < node.Val.(int) {\n        node.Left = t.removeHelper(node.Left, val)\n    } else if val > node.Val.(int) {\n        node.Right = t.removeHelper(node.Right, val)\n    } else {\n        if node.Left == nil || node.Right == nil {\n            child := node.Left\n            if node.Right != nil {\n                child = node.Right\n            }\n            if child == nil {\n                // Число дочерних узлов = 0, удалить node и сразу вернуть\n                return nil\n            } else {\n                // Число дочерних узлов = 1, удалить node напрямую\n                node = child\n            }\n        } else {\n            // Число дочерних узлов = 2, удалить следующий по симметричному обходу узел и заменить им текущий узел\n            temp := node.Right\n            for temp.Left != nil {\n                temp = temp.Left\n            }\n            node.Right = t.removeHelper(node.Right, temp.Val.(int))\n            node.Val = temp.Val\n        }\n    }\n    // Обновить высоту узла\n    t.updateHeight(node)\n    /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n    node = t.rotate(node)\n    // Вернуть корневой узел поддерева\n    return node\n}\n
        avl_tree.swift
        /* Удаление узла */\nfunc remove(val: Int) {\n    root = removeHelper(node: root, val: val)\n}\n\n/* Рекурсивное удаление узла (вспомогательный метод) */\nfunc removeHelper(node: TreeNode?, val: Int) -> TreeNode? {\n    var node = node\n    if node == nil {\n        return nil\n    }\n    /* 1. Найти узел и удалить его */\n    if val < node!.val {\n        node?.left = removeHelper(node: node?.left, val: val)\n    } else if val > node!.val {\n        node?.right = removeHelper(node: node?.right, val: val)\n    } else {\n        if node?.left == nil || node?.right == nil {\n            let child = node?.left ?? node?.right\n            // Число дочерних узлов = 0, удалить node и сразу вернуть\n            if child == nil {\n                return nil\n            }\n            // Число дочерних узлов = 1, удалить node напрямую\n            else {\n                node = child\n            }\n        } else {\n            // Число дочерних узлов = 2, удалить следующий по симметричному обходу узел и заменить им текущий узел\n            var temp = node?.right\n            while temp?.left != nil {\n                temp = temp?.left\n            }\n            node?.right = removeHelper(node: node?.right, val: temp!.val)\n            node?.val = temp!.val\n        }\n    }\n    updateHeight(node: node) // Обновить высоту узла\n    /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n    node = rotate(node: node)\n    // Вернуть корневой узел поддерева\n    return node\n}\n
        avl_tree.js
        /* Удаление узла */\nremove(val) {\n    this.root = this.#removeHelper(this.root, val);\n}\n\n/* Рекурсивное удаление узла (вспомогательный метод) */\n#removeHelper(node, val) {\n    if (node === null) return null;\n    /* 1. Найти узел и удалить его */\n    if (val < node.val) node.left = this.#removeHelper(node.left, val);\n    else if (val > node.val)\n        node.right = this.#removeHelper(node.right, val);\n    else {\n        if (node.left === null || node.right === null) {\n            const child = node.left !== null ? node.left : node.right;\n            // Число дочерних узлов = 0, удалить node и сразу вернуть\n            if (child === null) return null;\n            // Число дочерних узлов = 1, удалить node напрямую\n            else node = child;\n        } else {\n            // Число дочерних узлов = 2, удалить следующий по симметричному обходу узел и заменить им текущий узел\n            let temp = node.right;\n            while (temp.left !== null) {\n                temp = temp.left;\n            }\n            node.right = this.#removeHelper(node.right, temp.val);\n            node.val = temp.val;\n        }\n    }\n    this.#updateHeight(node); // Обновить высоту узла\n    /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n    node = this.#rotate(node);\n    // Вернуть корневой узел поддерева\n    return node;\n}\n
        avl_tree.ts
        /* Удаление узла */\nremove(val: number): void {\n    this.root = this.removeHelper(this.root, val);\n}\n\n/* Рекурсивное удаление узла (вспомогательный метод) */\nremoveHelper(node: TreeNode, val: number): TreeNode {\n    if (node === null) return null;\n    /* 1. Найти узел и удалить его */\n    if (val < node.val) {\n        node.left = this.removeHelper(node.left, val);\n    } else if (val > node.val) {\n        node.right = this.removeHelper(node.right, val);\n    } else {\n        if (node.left === null || node.right === null) {\n            const child = node.left !== null ? node.left : node.right;\n            // Число дочерних узлов = 0, удалить node и сразу вернуть\n            if (child === null) {\n                return null;\n            } else {\n                // Число дочерних узлов = 1, удалить node напрямую\n                node = child;\n            }\n        } else {\n            // Число дочерних узлов = 2, удалить следующий по симметричному обходу узел и заменить им текущий узел\n            let temp = node.right;\n            while (temp.left !== null) {\n                temp = temp.left;\n            }\n            node.right = this.removeHelper(node.right, temp.val);\n            node.val = temp.val;\n        }\n    }\n    this.updateHeight(node); // Обновить высоту узла\n    /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n    node = this.rotate(node);\n    // Вернуть корневой узел поддерева\n    return node;\n}\n
        avl_tree.dart
        /* Удаление узла */\nvoid remove(int val) {\n  root = removeHelper(root, val);\n}\n\n/* Рекурсивное удаление узла (вспомогательный метод) */\nTreeNode? removeHelper(TreeNode? node, int val) {\n  if (node == null) return null;\n  /* 1. Найти узел и удалить его */\n  if (val < node.val)\n    node.left = removeHelper(node.left, val);\n  else if (val > node.val)\n    node.right = removeHelper(node.right, val);\n  else {\n    if (node.left == null || node.right == null) {\n      TreeNode? child = node.left ?? node.right;\n      // Число дочерних узлов = 0, удалить node и сразу вернуть\n      if (child == null)\n        return null;\n      // Число дочерних узлов = 1, удалить node напрямую\n      else\n        node = child;\n    } else {\n      // Число дочерних узлов = 2, удалить следующий по симметричному обходу узел и заменить им текущий узел\n      TreeNode? temp = node.right;\n      while (temp!.left != null) {\n        temp = temp.left;\n      }\n      node.right = removeHelper(node.right, temp.val);\n      node.val = temp.val;\n    }\n  }\n  updateHeight(node); // Обновить высоту узла\n  /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n  node = rotate(node);\n  // Вернуть корневой узел поддерева\n  return node;\n}\n
        avl_tree.rs
        /* Удаление узла */\nfn remove(&self, val: i32) {\n    Self::remove_helper(self.root.clone(), val);\n}\n\n/* Рекурсивное удаление узла (вспомогательный метод) */\nfn remove_helper(node: OptionTreeNodeRc, val: i32) -> OptionTreeNodeRc {\n    match node {\n        Some(mut node) => {\n            /* 1. Найти узел и удалить его */\n            if val < node.borrow().val {\n                let left = node.borrow().left.clone();\n                node.borrow_mut().left = Self::remove_helper(left, val);\n            } else if val > node.borrow().val {\n                let right = node.borrow().right.clone();\n                node.borrow_mut().right = Self::remove_helper(right, val);\n            } else if node.borrow().left.is_none() || node.borrow().right.is_none() {\n                let child = if node.borrow().left.is_some() {\n                    node.borrow().left.clone()\n                } else {\n                    node.borrow().right.clone()\n                };\n                match child {\n                    // Число дочерних узлов = 0, удалить node и сразу вернуть\n                    None => {\n                        return None;\n                    }\n                    // Число дочерних узлов = 1, удалить node напрямую\n                    Some(child) => node = child,\n                }\n            } else {\n                // Число дочерних узлов = 2, удалить следующий по симметричному обходу узел и заменить им текущий узел\n                let mut temp = node.borrow().right.clone().unwrap();\n                loop {\n                    let temp_left = temp.borrow().left.clone();\n                    if temp_left.is_none() {\n                        break;\n                    }\n                    temp = temp_left.unwrap();\n                }\n                let right = node.borrow().right.clone();\n                node.borrow_mut().right = Self::remove_helper(right, temp.borrow().val);\n                node.borrow_mut().val = temp.borrow().val;\n            }\n            Self::update_height(Some(node.clone())); // Обновить высоту узла\n\n            /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n            node = Self::rotate(Some(node)).unwrap();\n            // Вернуть корневой узел поддерева\n            Some(node)\n        }\n        None => None,\n    }\n}\n
        avl_tree.c
        /* Удаление узла */\n// Из-за подключения stdio.h здесь нельзя использовать ключевое слово remove\nvoid removeItem(AVLTree *tree, int val) {\n    TreeNode *root = removeHelper(tree->root, val);\n}\n\n/* Рекурсивное удаление узла (вспомогательная функция) */\nTreeNode *removeHelper(TreeNode *node, int val) {\n    TreeNode *child, *grandChild;\n    if (node == NULL) {\n        return NULL;\n    }\n    /* 1. Найти узел и удалить его */\n    if (val < node->val) {\n        node->left = removeHelper(node->left, val);\n    } else if (val > node->val) {\n        node->right = removeHelper(node->right, val);\n    } else {\n        if (node->left == NULL || node->right == NULL) {\n            child = node->left;\n            if (node->right != NULL) {\n                child = node->right;\n            }\n            // Число дочерних узлов = 0, удалить node и сразу вернуть\n            if (child == NULL) {\n                return NULL;\n            } else {\n                // Число дочерних узлов = 1, удалить node напрямую\n                node = child;\n            }\n        } else {\n            // Число дочерних узлов = 2, удалить следующий по симметричному обходу узел и заменить им текущий узел\n            TreeNode *temp = node->right;\n            while (temp->left != NULL) {\n                temp = temp->left;\n            }\n            int tempVal = temp->val;\n            node->right = removeHelper(node->right, temp->val);\n            node->val = tempVal;\n        }\n    }\n    // Обновить высоту узла\n    updateHeight(node);\n    /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n    node = rotate(node);\n    // Вернуть корневой узел поддерева\n    return node;\n}\n
        avl_tree.kt
        /* Удаление узла */\nfun remove(_val: Int) {\n    root = removeHelper(root, _val)\n}\n\n/* Рекурсивное удаление узла (вспомогательный метод) */\nfun removeHelper(n: TreeNode?, _val: Int): TreeNode? {\n    var node = n ?: return null\n    /* 1. Найти узел и удалить его */\n    if (_val < node._val)\n        node.left = removeHelper(node.left, _val)\n    else if (_val > node._val)\n        node.right = removeHelper(node.right, _val)\n    else {\n        if (node.left == null || node.right == null) {\n            val child = if (node.left != null)\n                node.left\n            else\n                node.right\n            // Число дочерних узлов = 0, удалить node и сразу вернуть\n            if (child == null)\n                return null\n            // Число дочерних узлов = 1, удалить node напрямую\n            else\n                node = child\n        } else {\n            // Число дочерних узлов = 2, удалить следующий по симметричному обходу узел и заменить им текущий узел\n            var temp = node.right\n            while (temp!!.left != null) {\n                temp = temp.left\n            }\n            node.right = removeHelper(node.right, temp._val)\n            node._val = temp._val\n        }\n    }\n    updateHeight(node) // Обновить высоту узла\n    /* 2. Выполнить вращение, чтобы снова сбалансировать поддерево */\n    node = rotate(node)\n    // Вернуть корневой узел поддерева\n    return node\n}\n
        avl_tree.rb
        ### Удаление узла ###\ndef remove(val)\n  @root = remove_helper(@root, val)\nend\n\n### Удаление узла ###\ndef remove(val)\n  @root = remove_helper(@root, val)\nend\n\n# ## Рекурсивное удаление узла (вспомогательный метод) ###\ndef remove_helper(node, val)\n  return if node.nil?\n  # 1. Найти узел и удалить его\n  if val < node.val\n    node.left = remove_helper(node.left, val)\n  elsif val > node.val\n    node.right = remove_helper(node.right, val)\n  else\n    if node.left.nil? || node.right.nil?\n      child = node.left || node.right\n      # Число дочерних узлов = 0, удалить node и сразу вернуть\n      return if child.nil?\n      # Число дочерних узлов = 1, удалить node напрямую\n      node = child\n    else\n      # Число дочерних узлов = 2, удалить следующий по симметричному обходу узел и заменить им текущий узел\n      temp = node.right\n      while !temp.left.nil?\n        temp = temp.left\n      end\n      node.right = remove_helper(node.right, temp.val)\n      node.val = temp.val\n    end\n  end\n  # Обновить высоту узла\n  update_height(node)\n  # 2. Выполнить вращение, чтобы снова сбалансировать поддерево\n  rotate(node)\nend\n
        ","path":["Глава 7. Деревья","7.5   AVL-дерево *"],"tags":[]},{"location":"chapter_tree/avl_tree/#3_1","level":3,"title":"3.   Поиск узла","text":"

        Операция поиска узла в AVL-дереве совпадает с поиском в двоичном дереве поиска, поэтому здесь она повторно не рассматривается.

        ","path":["Глава 7. Деревья","7.5   AVL-дерево *"],"tags":[]},{"location":"chapter_tree/avl_tree/#754-avl-","level":2,"title":"7.5.4   Типичные применения AVL-дерева","text":"
        • Организация и хранение больших массивов данных, особенно в сценариях с частым поиском и относительно редкими вставками и удалениями.
        • Использование при построении индексных систем в базах данных.
        • Красно-черное дерево тоже является распространенным видом сбалансированного двоичного дерева поиска. По сравнению с AVL-деревом условия баланса у красно-черного дерева мягче, поэтому при вставке и удалении требуется меньше вращений, а средняя эффективность операций добавления и удаления выше.
        ","path":["Глава 7. Деревья","7.5   AVL-дерево *"],"tags":[]},{"location":"chapter_tree/binary_search_tree/","level":1,"title":"7.4   Двоичное дерево поиска","text":"

        Как показано на рисунке 7-16, двоичное дерево поиска (binary search tree) удовлетворяет следующим условиям.

        1. Для корневого узла все значения в левом поддереве меньше значения корневого узла, а все значения в правом поддереве больше значения корневого узла.
        2. Левое и правое поддеревья любого узла также являются двоичными деревьями поиска, то есть тоже удовлетворяют условию 1. .

        Рисунок 7-16   Двоичное дерево поиска

        ","path":["Глава 7. Деревья","7.4   Двоичное дерево поиска"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#741","level":2,"title":"7.4.1   Операции с двоичным деревом поиска","text":"

        Мы инкапсулируем двоичное дерево поиска в класс BinarySearchTree и объявляем переменную-член root , которая указывает на корневой узел дерева.

        ","path":["Глава 7. Деревья","7.4   Двоичное дерево поиска"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#1","level":3,"title":"1.   Поиск узла","text":"

        Для заданного целевого значения узла num можно выполнить поиск, опираясь на свойства двоичного дерева поиска. Как показано на рисунке 7-17, мы объявляем узел cur , стартуем от корня дерева root и циклически сравниваем значения cur.val и num .

        • Если cur.val < num , это означает, что целевой узел находится в правом поддереве cur , поэтому выполняем cur = cur.right .
        • Если cur.val > num , это означает, что целевой узел находится в левом поддереве cur , поэтому выполняем cur = cur.left .
        • Если cur.val = num , это означает, что целевой узел найден, и мы выходим из цикла, возвращая этот узел.
        <1><2><3><4>

        Рисунок 7-17   Пример поиска узла в двоичном дереве поиска

        Операция поиска в двоичном дереве поиска работает по тому же принципу, что и двоичный поиск: на каждом шаге она отбрасывает половину вариантов. Число итераций не превосходит высоты двоичного дерева, а когда дерево сбалансировано, требуется \\(O(\\log n)\\) времени. Пример кода приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_tree.py
        def search(self, num: int) -> TreeNode | None:\n    \"\"\"Поиск узла\"\"\"\n    cur = self._root\n    # Искать в цикле и выйти после прохода за листовой узел\n    while cur is not None:\n        # Целевой узел находится в правом поддереве cur\n        if cur.val < num:\n            cur = cur.right\n        # Целевой узел находится в левом поддереве cur\n        elif cur.val > num:\n            cur = cur.left\n        # Найти целевой узел и выйти из цикла\n        else:\n            break\n    return cur\n
        binary_search_tree.cpp
        /* Поиск узла */\nTreeNode *search(int num) {\n    TreeNode *cur = root;\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur != nullptr) {\n        // Целевой узел находится в правом поддереве cur\n        if (cur->val < num)\n            cur = cur->right;\n        // Целевой узел находится в левом поддереве cur\n        else if (cur->val > num)\n            cur = cur->left;\n        // Найти целевой узел и выйти из цикла\n        else\n            break;\n    }\n    // Вернуть целевой узел\n    return cur;\n}\n
        binary_search_tree.java
        /* Поиск узла */\nTreeNode search(int num) {\n    TreeNode cur = root;\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur != null) {\n        // Целевой узел находится в правом поддереве cur\n        if (cur.val < num)\n            cur = cur.right;\n        // Целевой узел находится в левом поддереве cur\n        else if (cur.val > num)\n            cur = cur.left;\n        // Найти целевой узел и выйти из цикла\n        else\n            break;\n    }\n    // Вернуть целевой узел\n    return cur;\n}\n
        binary_search_tree.cs
        /* Поиск узла */\nTreeNode? Search(int num) {\n    TreeNode? cur = root;\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur != null) {\n        // Целевой узел находится в правом поддереве cur\n        if (cur.val < num) cur =\n            cur.right;\n        // Целевой узел находится в левом поддереве cur\n        else if (cur.val > num)\n            cur = cur.left;\n        // Найти целевой узел и выйти из цикла\n        else\n            break;\n    }\n    // Вернуть целевой узел\n    return cur;\n}\n
        binary_search_tree.go
        /* Поиск узла */\nfunc (bst *binarySearchTree) search(num int) *TreeNode {\n    node := bst.root\n    // Искать в цикле и выйти после прохода за листовой узел\n    for node != nil {\n        if node.Val.(int) < num {\n            // Целевой узел находится в правом поддереве cur\n            node = node.Right\n        } else if node.Val.(int) > num {\n            // Целевой узел находится в левом поддереве cur\n            node = node.Left\n        } else {\n            // Найти целевой узел и выйти из цикла\n            break\n        }\n    }\n    // Вернуть целевой узел\n    return node\n}\n
        binary_search_tree.swift
        /* Поиск узла */\nfunc search(num: Int) -> TreeNode? {\n    var cur = root\n    // Искать в цикле и выйти после прохода за листовой узел\n    while cur != nil {\n        // Целевой узел находится в правом поддереве cur\n        if cur!.val < num {\n            cur = cur?.right\n        }\n        // Целевой узел находится в левом поддереве cur\n        else if cur!.val > num {\n            cur = cur?.left\n        }\n        // Найти целевой узел и выйти из цикла\n        else {\n            break\n        }\n    }\n    // Вернуть целевой узел\n    return cur\n}\n
        binary_search_tree.js
        /* Поиск узла */\nsearch(num) {\n    let cur = this.root;\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur !== null) {\n        // Целевой узел находится в правом поддереве cur\n        if (cur.val < num) cur = cur.right;\n        // Целевой узел находится в левом поддереве cur\n        else if (cur.val > num) cur = cur.left;\n        // Найти целевой узел и выйти из цикла\n        else break;\n    }\n    // Вернуть целевой узел\n    return cur;\n}\n
        binary_search_tree.ts
        /* Поиск узла */\nsearch(num: number): TreeNode | null {\n    let cur = this.root;\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur !== null) {\n        // Целевой узел находится в правом поддереве cur\n        if (cur.val < num) cur = cur.right;\n        // Целевой узел находится в левом поддереве cur\n        else if (cur.val > num) cur = cur.left;\n        // Найти целевой узел и выйти из цикла\n        else break;\n    }\n    // Вернуть целевой узел\n    return cur;\n}\n
        binary_search_tree.dart
        /* Поиск узла */\nTreeNode? search(int _num) {\n  TreeNode? cur = _root;\n  // Искать в цикле и выйти после прохода за листовой узел\n  while (cur != null) {\n    // Целевой узел находится в правом поддереве cur\n    if (cur.val < _num)\n      cur = cur.right;\n    // Целевой узел находится в левом поддереве cur\n    else if (cur.val > _num)\n      cur = cur.left;\n    // Найти целевой узел и выйти из цикла\n    else\n      break;\n  }\n  // Вернуть целевой узел\n  return cur;\n}\n
        binary_search_tree.rs
        /* Поиск узла */\npub fn search(&self, num: i32) -> OptionTreeNodeRc {\n    let mut cur = self.root.clone();\n    // Искать в цикле и выйти после прохода за листовой узел\n    while let Some(node) = cur.clone() {\n        match num.cmp(&node.borrow().val) {\n            // Целевой узел находится в правом поддереве cur\n            Ordering::Greater => cur = node.borrow().right.clone(),\n            // Целевой узел находится в левом поддереве cur\n            Ordering::Less => cur = node.borrow().left.clone(),\n            // Найти целевой узел и выйти из цикла\n            Ordering::Equal => break,\n        }\n    }\n\n    // Вернуть целевой узел\n    cur\n}\n
        binary_search_tree.c
        /* Поиск узла */\nTreeNode *search(BinarySearchTree *bst, int num) {\n    TreeNode *cur = bst->root;\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur != NULL) {\n        if (cur->val < num) {\n            // Целевой узел находится в правом поддереве cur\n            cur = cur->right;\n        } else if (cur->val > num) {\n            // Целевой узел находится в левом поддереве cur\n            cur = cur->left;\n        } else {\n            // Найти целевой узел и выйти из цикла\n            break;\n        }\n    }\n    // Вернуть целевой узел\n    return cur;\n}\n
        binary_search_tree.kt
        /* Поиск узла */\nfun search(num: Int): TreeNode? {\n    var cur = root\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur != null) {\n        // Целевой узел находится в правом поддереве cur\n        cur = if (cur._val < num)\n            cur.right\n        // Целевой узел находится в левом поддереве cur\n        else if (cur._val > num)\n            cur.left\n        // Найти целевой узел и выйти из цикла\n        else\n            break\n    }\n    // Вернуть целевой узел\n    return cur\n}\n
        binary_search_tree.rb
        ### Поиск узла ###\ndef search(num)\n  cur = @root\n\n  # Искать в цикле и выйти после прохода за листовой узел\n  while !cur.nil?\n    # Целевой узел находится в правом поддереве cur\n    if cur.val < num\n      cur = cur.right\n    # Целевой узел находится в левом поддереве cur\n    elsif cur.val > num\n      cur = cur.left\n    # Найти целевой узел и выйти из цикла\n    else\n      break\n    end\n  end\n\n  cur\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 7. Деревья","7.4   Двоичное дерево поиска"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#2","level":3,"title":"2.   Вставка узла","text":"

        Пусть дан элемент num , который нужно вставить. Чтобы сохранить свойство двоичного дерева поиска «левое поддерево < корень < правое поддерево», процесс вставки показан на рисунке 7-18.

        1. Найти позицию для вставки: как и в операции поиска, начиная от корня, мы циклически спускаемся вниз в зависимости от соотношения между текущим значением узла и num , пока не выйдем за листовой узел (то есть не дойдем до None ).
        2. Вставить узел в найденную позицию: инициализировать узел num и поставить его на место этого None .

        Рисунок 7-18   Вставка узла в двоичное дерево поиска

        В реализации кода нужно обратить внимание на следующие два момента.

        • Двоичное дерево поиска не допускает дублирующихся узлов, иначе его определение будет нарушено. Поэтому если вставляемый узел уже существует в дереве, вставка не выполняется и функция сразу возвращается.
        • Чтобы реализовать вставку, нам нужно использовать узел pre для сохранения узла предыдущей итерации цикла. Тогда, когда обход дойдет до None , мы сможем получить его родителя и завершить вставку.
        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_tree.py
        def insert(self, num: int):\n    \"\"\"Вставка узла\"\"\"\n    # Если дерево пусто, инициализировать корневой узел\n    if self._root is None:\n        self._root = TreeNode(num)\n        return\n    # Искать в цикле и выйти после прохода за листовой узел\n    cur, pre = self._root, None\n    while cur is not None:\n        # Найти повторяющийся узел и сразу вернуть\n        if cur.val == num:\n            return\n        pre = cur\n        # Позиция вставки находится в правом поддереве cur\n        if cur.val < num:\n            cur = cur.right\n        # Позиция вставки находится в левом поддереве cur\n        else:\n            cur = cur.left\n    # Вставка узла\n    node = TreeNode(num)\n    if pre.val < num:\n        pre.right = node\n    else:\n        pre.left = node\n
        binary_search_tree.cpp
        /* Вставка узла */\nvoid insert(int num) {\n    // Если дерево пусто, инициализировать корневой узел\n    if (root == nullptr) {\n        root = new TreeNode(num);\n        return;\n    }\n    TreeNode *cur = root, *pre = nullptr;\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur != nullptr) {\n        // Найти повторяющийся узел и сразу вернуть\n        if (cur->val == num)\n            return;\n        pre = cur;\n        // Позиция вставки находится в правом поддереве cur\n        if (cur->val < num)\n            cur = cur->right;\n        // Позиция вставки находится в левом поддереве cur\n        else\n            cur = cur->left;\n    }\n    // Вставка узла\n    TreeNode *node = new TreeNode(num);\n    if (pre->val < num)\n        pre->right = node;\n    else\n        pre->left = node;\n}\n
        binary_search_tree.java
        /* Вставка узла */\nvoid insert(int num) {\n    // Если дерево пусто, инициализировать корневой узел\n    if (root == null) {\n        root = new TreeNode(num);\n        return;\n    }\n    TreeNode cur = root, pre = null;\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur != null) {\n        // Найти повторяющийся узел и сразу вернуть\n        if (cur.val == num)\n            return;\n        pre = cur;\n        // Позиция вставки находится в правом поддереве cur\n        if (cur.val < num)\n            cur = cur.right;\n        // Позиция вставки находится в левом поддереве cur\n        else\n            cur = cur.left;\n    }\n    // Вставка узла\n    TreeNode node = new TreeNode(num);\n    if (pre.val < num)\n        pre.right = node;\n    else\n        pre.left = node;\n}\n
        binary_search_tree.cs
        /* Вставка узла */\nvoid Insert(int num) {\n    // Если дерево пусто, инициализировать корневой узел\n    if (root == null) {\n        root = new TreeNode(num);\n        return;\n    }\n    TreeNode? cur = root, pre = null;\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur != null) {\n        // Найти повторяющийся узел и сразу вернуть\n        if (cur.val == num)\n            return;\n        pre = cur;\n        // Позиция вставки находится в правом поддереве cur\n        if (cur.val < num)\n            cur = cur.right;\n        // Позиция вставки находится в левом поддереве cur\n        else\n            cur = cur.left;\n    }\n\n    // Вставка узла\n    TreeNode node = new(num);\n    if (pre != null) {\n        if (pre.val < num)\n            pre.right = node;\n        else\n            pre.left = node;\n    }\n}\n
        binary_search_tree.go
        /* Вставка узла */\nfunc (bst *binarySearchTree) insert(num int) {\n    cur := bst.root\n    // Если дерево пусто, инициализировать корневой узел\n    if cur == nil {\n        bst.root = NewTreeNode(num)\n        return\n    }\n    // Позиция узла, предшествующего вставляемому\n    var pre *TreeNode = nil\n    // Искать в цикле и выйти после прохода за листовой узел\n    for cur != nil {\n        if cur.Val == num {\n            return\n        }\n        pre = cur\n        if cur.Val.(int) < num {\n            cur = cur.Right\n        } else {\n            cur = cur.Left\n        }\n    }\n    // Вставка узла\n    node := NewTreeNode(num)\n    if pre.Val.(int) < num {\n        pre.Right = node\n    } else {\n        pre.Left = node\n    }\n}\n
        binary_search_tree.swift
        /* Вставка узла */\nfunc insert(num: Int) {\n    // Если дерево пусто, инициализировать корневой узел\n    if root == nil {\n        root = TreeNode(x: num)\n        return\n    }\n    var cur = root\n    var pre: TreeNode?\n    // Искать в цикле и выйти после прохода за листовой узел\n    while cur != nil {\n        // Найти повторяющийся узел и сразу вернуть\n        if cur!.val == num {\n            return\n        }\n        pre = cur\n        // Позиция вставки находится в правом поддереве cur\n        if cur!.val < num {\n            cur = cur?.right\n        }\n        // Позиция вставки находится в левом поддереве cur\n        else {\n            cur = cur?.left\n        }\n    }\n    // Вставка узла\n    let node = TreeNode(x: num)\n    if pre!.val < num {\n        pre?.right = node\n    } else {\n        pre?.left = node\n    }\n}\n
        binary_search_tree.js
        /* Вставка узла */\ninsert(num) {\n    // Если дерево пусто, инициализировать корневой узел\n    if (this.root === null) {\n        this.root = new TreeNode(num);\n        return;\n    }\n    let cur = this.root,\n        pre = null;\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur !== null) {\n        // Найти повторяющийся узел и сразу вернуть\n        if (cur.val === num) return;\n        pre = cur;\n        // Позиция вставки находится в правом поддереве cur\n        if (cur.val < num) cur = cur.right;\n        // Позиция вставки находится в левом поддереве cur\n        else cur = cur.left;\n    }\n    // Вставка узла\n    const node = new TreeNode(num);\n    if (pre.val < num) pre.right = node;\n    else pre.left = node;\n}\n
        binary_search_tree.ts
        /* Вставка узла */\ninsert(num: number): void {\n    // Если дерево пусто, инициализировать корневой узел\n    if (this.root === null) {\n        this.root = new TreeNode(num);\n        return;\n    }\n    let cur: TreeNode | null = this.root,\n        pre: TreeNode | null = null;\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur !== null) {\n        // Найти повторяющийся узел и сразу вернуть\n        if (cur.val === num) return;\n        pre = cur;\n        // Позиция вставки находится в правом поддереве cur\n        if (cur.val < num) cur = cur.right;\n        // Позиция вставки находится в левом поддереве cur\n        else cur = cur.left;\n    }\n    // Вставка узла\n    const node = new TreeNode(num);\n    if (pre!.val < num) pre!.right = node;\n    else pre!.left = node;\n}\n
        binary_search_tree.dart
        /* Вставка узла */\nvoid insert(int _num) {\n  // Если дерево пусто, инициализировать корневой узел\n  if (_root == null) {\n    _root = TreeNode(_num);\n    return;\n  }\n  TreeNode? cur = _root;\n  TreeNode? pre = null;\n  // Искать в цикле и выйти после прохода за листовой узел\n  while (cur != null) {\n    // Найти повторяющийся узел и сразу вернуть\n    if (cur.val == _num) return;\n    pre = cur;\n    // Позиция вставки находится в правом поддереве cur\n    if (cur.val < _num)\n      cur = cur.right;\n    // Позиция вставки находится в левом поддереве cur\n    else\n      cur = cur.left;\n  }\n  // Вставка узла\n  TreeNode? node = TreeNode(_num);\n  if (pre!.val < _num)\n    pre.right = node;\n  else\n    pre.left = node;\n}\n
        binary_search_tree.rs
        /* Вставка узла */\npub fn insert(&mut self, num: i32) {\n    // Если дерево пусто, инициализировать корневой узел\n    if self.root.is_none() {\n        self.root = Some(TreeNode::new(num));\n        return;\n    }\n    let mut cur = self.root.clone();\n    let mut pre = None;\n    // Искать в цикле и выйти после прохода за листовой узел\n    while let Some(node) = cur.clone() {\n        match num.cmp(&node.borrow().val) {\n            // Найти повторяющийся узел и сразу вернуть\n            Ordering::Equal => return,\n            // Позиция вставки находится в правом поддереве cur\n            Ordering::Greater => {\n                pre = cur.clone();\n                cur = node.borrow().right.clone();\n            }\n            // Позиция вставки находится в левом поддереве cur\n            Ordering::Less => {\n                pre = cur.clone();\n                cur = node.borrow().left.clone();\n            }\n        }\n    }\n    // Вставка узла\n    let pre = pre.unwrap();\n    let node = Some(TreeNode::new(num));\n    if num > pre.borrow().val {\n        pre.borrow_mut().right = node;\n    } else {\n        pre.borrow_mut().left = node;\n    }\n}\n
        binary_search_tree.c
        /* Вставка узла */\nvoid insert(BinarySearchTree *bst, int num) {\n    // Если дерево пусто, инициализировать корневой узел\n    if (bst->root == NULL) {\n        bst->root = newTreeNode(num);\n        return;\n    }\n    TreeNode *cur = bst->root, *pre = NULL;\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur != NULL) {\n        // Найти повторяющийся узел и сразу вернуть\n        if (cur->val == num) {\n            return;\n        }\n        pre = cur;\n        if (cur->val < num) {\n            // Позиция вставки находится в правом поддереве cur\n            cur = cur->right;\n        } else {\n            // Позиция вставки находится в левом поддереве cur\n            cur = cur->left;\n        }\n    }\n    // Вставка узла\n    TreeNode *node = newTreeNode(num);\n    if (pre->val < num) {\n        pre->right = node;\n    } else {\n        pre->left = node;\n    }\n}\n
        binary_search_tree.kt
        /* Вставка узла */\nfun insert(num: Int) {\n    // Если дерево пусто, инициализировать корневой узел\n    if (root == null) {\n        root = TreeNode(num)\n        return\n    }\n    var cur = root\n    var pre: TreeNode? = null\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur != null) {\n        // Найти повторяющийся узел и сразу вернуть\n        if (cur._val == num)\n            return\n        pre = cur\n        // Позиция вставки находится в правом поддереве cur\n        cur = if (cur._val < num)\n            cur.right\n        // Позиция вставки находится в левом поддереве cur\n        else\n            cur.left\n    }\n    // Вставка узла\n    val node = TreeNode(num)\n    if (pre?._val!! < num)\n        pre.right = node\n    else\n        pre.left = node\n}\n
        binary_search_tree.rb
        ### Вставка узла ###\ndef insert(num)\n  # Если дерево пусто, инициализировать корневой узел\n  if @root.nil?\n    @root = TreeNode.new(num)\n    return\n  end\n\n  # Искать в цикле и выйти после прохода за листовой узел\n  cur, pre = @root, nil\n  while !cur.nil?\n    # Найти повторяющийся узел и сразу вернуть\n    return if cur.val == num\n\n    pre = cur\n    # Позиция вставки находится в правом поддереве cur\n    if cur.val < num\n      cur = cur.right\n    # Позиция вставки находится в левом поддереве cur\n    else\n      cur = cur.left\n    end\n  end\n\n  # Вставка узла\n  node = TreeNode.new(num)\n  if pre.val < num\n    pre.right = node\n  else\n    pre.left = node\n  end\nend\n
        Визуализация кода

        Во весь экран >

        Как и поиск узла, вставка узла требует \\(O(\\log n)\\) времени.

        ","path":["Глава 7. Деревья","7.4   Двоичное дерево поиска"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#3","level":3,"title":"3.   Удаление узла","text":"

        Сначала нужно найти в двоичном дереве целевой узел, а затем удалить его. Как и при вставке, после удаления необходимо сохранить свойство двоичного дерева поиска: «левое поддерево < корень < правое поддерево». Поэтому в зависимости от числа дочерних узлов у удаляемого узла, то есть для случаев со степенью 0, 1 и 2, выполняются разные операции удаления.

        Как показано на рисунке 7-19, когда степень удаляемого узла равна \\(0\\) , это значит, что узел является листом и может быть удален напрямую.

        Рисунок 7-19   Удаление узла в двоичном дереве поиска (степень 0)

        Как показано на рисунке 7-20, когда степень удаляемого узла равна \\(1\\) , достаточно заменить удаляемый узел его дочерним узлом.

        Рисунок 7-20   Удаление узла в двоичном дереве поиска (степень 1)

        Когда степень удаляемого узла равна \\(2\\) , мы уже не можем удалить его напрямую и должны использовать для замены другой узел. Чтобы сохранить свойство двоичного дерева поиска «левое поддерево \\(<\\) корень \\(<\\) правое поддерево», этим узлом может быть минимальный узел правого поддерева или максимальный узел левого поддерева.

        Предположим, мы выбираем минимальный узел правого поддерева, то есть следующий узел в симметричном обходе. Тогда процесс удаления показан на рисунке 7-21.

        1. Найти следующий узел в «последовательности симметричного обхода» для удаляемого узла и обозначить его как tmp .
        2. Значением tmp перезаписать значение удаляемого узла, а затем рекурсивно удалить узел tmp из дерева.
        <1><2><3><4>

        Рисунок 7-21   Удаление узла в двоичном дереве поиска (степень 2)

        Операция удаления узла также требует \\(O(\\log n)\\) времени, где поиск удаляемого узла стоит \\(O(\\log n)\\) , а получение следующего узла симметричного обхода также требует \\(O(\\log n)\\) . Пример кода приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_tree.py
        def remove(self, num: int):\n    \"\"\"Удаление узла\"\"\"\n    # Если дерево пусто, сразу вернуть\n    if self._root is None:\n        return\n    # Искать в цикле и выйти после прохода за листовой узел\n    cur, pre = self._root, None\n    while cur is not None:\n        # Найти узел для удаления и выйти из цикла\n        if cur.val == num:\n            break\n        pre = cur\n        # Узел для удаления находится в правом поддереве cur\n        if cur.val < num:\n            cur = cur.right\n        # Узел для удаления находится в левом поддереве cur\n        else:\n            cur = cur.left\n    # Если узел для удаления отсутствует, сразу вернуть\n    if cur is None:\n        return\n\n    # Число дочерних узлов = 0 или 1\n    if cur.left is None or cur.right is None:\n        # Когда число дочерних узлов = 0 / 1, child = null / этот дочерний узел\n        child = cur.left or cur.right\n        # Удалить узел cur\n        if cur != self._root:\n            if pre.left == cur:\n                pre.left = child\n            else:\n                pre.right = child\n        else:\n            # Если удаляемый узел является корнем, заново назначить корневой узел\n            self._root = child\n    # Число дочерних узлов = 2\n    else:\n        # Получить следующий узел после cur в симметричном обходе\n        tmp: TreeNode = cur.right\n        while tmp.left is not None:\n            tmp = tmp.left\n        # Рекурсивно удалить узел tmp\n        self.remove(tmp.val)\n        # Перезаписать cur значением tmp\n        cur.val = tmp.val\n
        binary_search_tree.cpp
        /* Удаление узла */\nvoid remove(int num) {\n    // Если дерево пусто, сразу вернуть\n    if (root == nullptr)\n        return;\n    TreeNode *cur = root, *pre = nullptr;\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur != nullptr) {\n        // Найти узел для удаления и выйти из цикла\n        if (cur->val == num)\n            break;\n        pre = cur;\n        // Узел для удаления находится в правом поддереве cur\n        if (cur->val < num)\n            cur = cur->right;\n        // Узел для удаления находится в левом поддереве cur\n        else\n            cur = cur->left;\n    }\n    // Если узел для удаления отсутствует, сразу вернуть\n    if (cur == nullptr)\n        return;\n    // Число дочерних узлов = 0 или 1\n    if (cur->left == nullptr || cur->right == nullptr) {\n        // Когда число дочерних узлов = 0 / 1, child = nullptr / этот дочерний узел\n        TreeNode *child = cur->left != nullptr ? cur->left : cur->right;\n        // Удалить узел cur\n        if (cur != root) {\n            if (pre->left == cur)\n                pre->left = child;\n            else\n                pre->right = child;\n        } else {\n            // Если удаляемый узел является корнем, заново назначить корневой узел\n            root = child;\n        }\n        // Освободить память\n        delete cur;\n    }\n    // Число дочерних узлов = 2\n    else {\n        // Получить следующий узел после cur в симметричном обходе\n        TreeNode *tmp = cur->right;\n        while (tmp->left != nullptr) {\n            tmp = tmp->left;\n        }\n        int tmpVal = tmp->val;\n        // Рекурсивно удалить узел tmp\n        remove(tmp->val);\n        // Перезаписать cur значением tmp\n        cur->val = tmpVal;\n    }\n}\n
        binary_search_tree.java
        /* Удаление узла */\nvoid remove(int num) {\n    // Если дерево пусто, сразу вернуть\n    if (root == null)\n        return;\n    TreeNode cur = root, pre = null;\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur != null) {\n        // Найти узел для удаления и выйти из цикла\n        if (cur.val == num)\n            break;\n        pre = cur;\n        // Узел для удаления находится в правом поддереве cur\n        if (cur.val < num)\n            cur = cur.right;\n        // Узел для удаления находится в левом поддереве cur\n        else\n            cur = cur.left;\n    }\n    // Если узел для удаления отсутствует, сразу вернуть\n    if (cur == null)\n        return;\n    // Число дочерних узлов = 0 или 1\n    if (cur.left == null || cur.right == null) {\n        // Когда число дочерних узлов = 0 / 1, child = null / этот дочерний узел\n        TreeNode child = cur.left != null ? cur.left : cur.right;\n        // Удалить узел cur\n        if (cur != root) {\n            if (pre.left == cur)\n                pre.left = child;\n            else\n                pre.right = child;\n        } else {\n            // Если удаляемый узел является корнем, заново назначить корневой узел\n            root = child;\n        }\n    }\n    // Число дочерних узлов = 2\n    else {\n        // Получить следующий узел после cur в симметричном обходе\n        TreeNode tmp = cur.right;\n        while (tmp.left != null) {\n            tmp = tmp.left;\n        }\n        // Рекурсивно удалить узел tmp\n        remove(tmp.val);\n        // Перезаписать cur значением tmp\n        cur.val = tmp.val;\n    }\n}\n
        binary_search_tree.cs
        /* Удаление узла */\nvoid Remove(int num) {\n    // Если дерево пусто, сразу вернуть\n    if (root == null)\n        return;\n    TreeNode? cur = root, pre = null;\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur != null) {\n        // Найти узел для удаления и выйти из цикла\n        if (cur.val == num)\n            break;\n        pre = cur;\n        // Узел для удаления находится в правом поддереве cur\n        if (cur.val < num)\n            cur = cur.right;\n        // Узел для удаления находится в левом поддереве cur\n        else\n            cur = cur.left;\n    }\n    // Если узел для удаления отсутствует, сразу вернуть\n    if (cur == null)\n        return;\n    // Число дочерних узлов = 0 или 1\n    if (cur.left == null || cur.right == null) {\n        // Когда число дочерних узлов = 0 / 1, child = null / этот дочерний узел\n        TreeNode? child = cur.left ?? cur.right;\n        // Удалить узел cur\n        if (cur != root) {\n            if (pre!.left == cur)\n                pre.left = child;\n            else\n                pre.right = child;\n        } else {\n            // Если удаляемый узел является корнем, заново назначить корневой узел\n            root = child;\n        }\n    }\n    // Число дочерних узлов = 2\n    else {\n        // Получить следующий узел после cur в симметричном обходе\n        TreeNode? tmp = cur.right;\n        while (tmp.left != null) {\n            tmp = tmp.left;\n        }\n        // Рекурсивно удалить узел tmp\n        Remove(tmp.val!.Value);\n        // Перезаписать cur значением tmp\n        cur.val = tmp.val;\n    }\n}\n
        binary_search_tree.go
        /* Удаление узла */\nfunc (bst *binarySearchTree) remove(num int) {\n    cur := bst.root\n    // Если дерево пусто, сразу вернуть\n    if cur == nil {\n        return\n    }\n    // Позиция узла, предшествующего удаляемому\n    var pre *TreeNode = nil\n    // Искать в цикле и выйти после прохода за листовой узел\n    for cur != nil {\n        if cur.Val == num {\n            break\n        }\n        pre = cur\n        if cur.Val.(int) < num {\n            // Удаляемый узел находится в правом поддереве\n            cur = cur.Right\n        } else {\n            // Удаляемый узел находится в левом поддереве\n            cur = cur.Left\n        }\n    }\n    // Если узел для удаления отсутствует, сразу вернуть\n    if cur == nil {\n        return\n    }\n    // Число дочерних узлов равно 0 или 1\n    if cur.Left == nil || cur.Right == nil {\n        var child *TreeNode = nil\n        // Извлечь дочерний узел удаляемого узла\n        if cur.Left != nil {\n            child = cur.Left\n        } else {\n            child = cur.Right\n        }\n        // Удалить узел cur\n        if cur != bst.root {\n            if pre.Left == cur {\n                pre.Left = child\n            } else {\n                pre.Right = child\n            }\n        } else {\n            // Если удаляемый узел является корнем, заново назначить корневой узел\n            bst.root = child\n        }\n        // Число дочерних узлов равно 2\n    } else {\n        // Получить следующий после cur узел в симметричном обходе для удаляемого узла\n        tmp := cur.Right\n        for tmp.Left != nil {\n            tmp = tmp.Left\n        }\n        // Рекурсивно удалить узел tmp\n        bst.remove(tmp.Val.(int))\n        // Перезаписать cur значением tmp\n        cur.Val = tmp.Val\n    }\n}\n
        binary_search_tree.swift
        /* Удаление узла */\nfunc remove(num: Int) {\n    // Если дерево пусто, сразу вернуть\n    if root == nil {\n        return\n    }\n    var cur = root\n    var pre: TreeNode?\n    // Искать в цикле и выйти после прохода за листовой узел\n    while cur != nil {\n        // Найти узел для удаления и выйти из цикла\n        if cur!.val == num {\n            break\n        }\n        pre = cur\n        // Узел для удаления находится в правом поддереве cur\n        if cur!.val < num {\n            cur = cur?.right\n        }\n        // Узел для удаления находится в левом поддереве cur\n        else {\n            cur = cur?.left\n        }\n    }\n    // Если узел для удаления отсутствует, сразу вернуть\n    if cur == nil {\n        return\n    }\n    // Число дочерних узлов = 0 или 1\n    if cur?.left == nil || cur?.right == nil {\n        // Когда число дочерних узлов = 0 / 1, child = null / этот дочерний узел\n        let child = cur?.left ?? cur?.right\n        // Удалить узел cur\n        if cur !== root {\n            if pre?.left === cur {\n                pre?.left = child\n            } else {\n                pre?.right = child\n            }\n        } else {\n            // Если удаляемый узел является корнем, заново назначить корневой узел\n            root = child\n        }\n    }\n    // Число дочерних узлов = 2\n    else {\n        // Получить следующий узел после cur в симметричном обходе\n        var tmp = cur?.right\n        while tmp?.left != nil {\n            tmp = tmp?.left\n        }\n        // Рекурсивно удалить узел tmp\n        remove(num: tmp!.val)\n        // Перезаписать cur значением tmp\n        cur?.val = tmp!.val\n    }\n}\n
        binary_search_tree.js
        /* Удаление узла */\nremove(num) {\n    // Если дерево пусто, сразу вернуть\n    if (this.root === null) return;\n    let cur = this.root,\n        pre = null;\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur !== null) {\n        // Найти узел для удаления и выйти из цикла\n        if (cur.val === num) break;\n        pre = cur;\n        // Узел для удаления находится в правом поддереве cur\n        if (cur.val < num) cur = cur.right;\n        // Узел для удаления находится в левом поддереве cur\n        else cur = cur.left;\n    }\n    // Если узел для удаления отсутствует, сразу вернуть\n    if (cur === null) return;\n    // Число дочерних узлов = 0 или 1\n    if (cur.left === null || cur.right === null) {\n        // Когда число дочерних узлов = 0 / 1, child = null / этот дочерний узел\n        const child = cur.left !== null ? cur.left : cur.right;\n        // Удалить узел cur\n        if (cur !== this.root) {\n            if (pre.left === cur) pre.left = child;\n            else pre.right = child;\n        } else {\n            // Если удаляемый узел является корнем, заново назначить корневой узел\n            this.root = child;\n        }\n    }\n    // Число дочерних узлов = 2\n    else {\n        // Получить следующий узел после cur в симметричном обходе\n        let tmp = cur.right;\n        while (tmp.left !== null) {\n            tmp = tmp.left;\n        }\n        // Рекурсивно удалить узел tmp\n        this.remove(tmp.val);\n        // Перезаписать cur значением tmp\n        cur.val = tmp.val;\n    }\n}\n
        binary_search_tree.ts
        /* Удаление узла */\nremove(num: number): void {\n    // Если дерево пусто, сразу вернуть\n    if (this.root === null) return;\n    let cur: TreeNode | null = this.root,\n        pre: TreeNode | null = null;\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur !== null) {\n        // Найти узел для удаления и выйти из цикла\n        if (cur.val === num) break;\n        pre = cur;\n        // Узел для удаления находится в правом поддереве cur\n        if (cur.val < num) cur = cur.right;\n        // Узел для удаления находится в левом поддереве cur\n        else cur = cur.left;\n    }\n    // Если узел для удаления отсутствует, сразу вернуть\n    if (cur === null) return;\n    // Число дочерних узлов = 0 или 1\n    if (cur.left === null || cur.right === null) {\n        // Когда число дочерних узлов = 0 / 1, child = null / этот дочерний узел\n        const child: TreeNode | null =\n            cur.left !== null ? cur.left : cur.right;\n        // Удалить узел cur\n        if (cur !== this.root) {\n            if (pre!.left === cur) pre!.left = child;\n            else pre!.right = child;\n        } else {\n            // Если удаляемый узел является корнем, заново назначить корневой узел\n            this.root = child;\n        }\n    }\n    // Число дочерних узлов = 2\n    else {\n        // Получить следующий узел после cur в симметричном обходе\n        let tmp: TreeNode | null = cur.right;\n        while (tmp!.left !== null) {\n            tmp = tmp!.left;\n        }\n        // Рекурсивно удалить узел tmp\n        this.remove(tmp!.val);\n        // Перезаписать cur значением tmp\n        cur.val = tmp!.val;\n    }\n}\n
        binary_search_tree.dart
        /* Удаление узла */\nvoid remove(int _num) {\n  // Если дерево пусто, сразу вернуть\n  if (_root == null) return;\n  TreeNode? cur = _root;\n  TreeNode? pre = null;\n  // Искать в цикле и выйти после прохода за листовой узел\n  while (cur != null) {\n    // Найти узел для удаления и выйти из цикла\n    if (cur.val == _num) break;\n    pre = cur;\n    // Узел для удаления находится в правом поддереве cur\n    if (cur.val < _num)\n      cur = cur.right;\n    // Узел для удаления находится в левом поддереве cur\n    else\n      cur = cur.left;\n  }\n  // Если удаляемого узла нет, сразу вернуть\n  if (cur == null) return;\n  // Число дочерних узлов = 0 или 1\n  if (cur.left == null || cur.right == null) {\n    // Когда число дочерних узлов = 0 / 1, child = null / этот дочерний узел\n    TreeNode? child = cur.left ?? cur.right;\n    // Удалить узел cur\n    if (cur != _root) {\n      if (pre!.left == cur)\n        pre.left = child;\n      else\n        pre.right = child;\n    } else {\n      // Если удаляемый узел является корнем, заново назначить корневой узел\n      _root = child;\n    }\n  } else {\n    // Число дочерних узлов = 2\n    // Получить следующий узел после cur в симметричном обходе\n    TreeNode? tmp = cur.right;\n    while (tmp!.left != null) {\n      tmp = tmp.left;\n    }\n    // Рекурсивно удалить узел tmp\n    remove(tmp.val);\n    // Перезаписать cur значением tmp\n    cur.val = tmp.val;\n  }\n}\n
        binary_search_tree.rs
        /* Удаление узла */\npub fn remove(&mut self, num: i32) {\n    // Если дерево пусто, сразу вернуть\n    if self.root.is_none() {\n        return;\n    }\n    let mut cur = self.root.clone();\n    let mut pre = None;\n    // Искать в цикле и выйти после прохода за листовой узел\n    while let Some(node) = cur.clone() {\n        match num.cmp(&node.borrow().val) {\n            // Найти узел для удаления и выйти из цикла\n            Ordering::Equal => break,\n            // Узел для удаления находится в правом поддереве cur\n            Ordering::Greater => {\n                pre = cur.clone();\n                cur = node.borrow().right.clone();\n            }\n            // Узел для удаления находится в левом поддереве cur\n            Ordering::Less => {\n                pre = cur.clone();\n                cur = node.borrow().left.clone();\n            }\n        }\n    }\n    // Если узел для удаления отсутствует, сразу вернуть\n    if cur.is_none() {\n        return;\n    }\n    let cur = cur.unwrap();\n    let (left_child, right_child) = (cur.borrow().left.clone(), cur.borrow().right.clone());\n    match (left_child.clone(), right_child.clone()) {\n        // Число дочерних узлов = 0 или 1\n        (None, None) | (Some(_), None) | (None, Some(_)) => {\n            // Когда число дочерних узлов = 0 / 1, child = nullptr / этот дочерний узел\n            let child = left_child.or(right_child);\n            let pre = pre.unwrap();\n            // Удалить узел cur\n            if !Rc::ptr_eq(&cur, self.root.as_ref().unwrap()) {\n                let left = pre.borrow().left.clone();\n                if left.is_some() && Rc::ptr_eq(left.as_ref().unwrap(), &cur) {\n                    pre.borrow_mut().left = child;\n                } else {\n                    pre.borrow_mut().right = child;\n                }\n            } else {\n                // Если удаляемый узел является корнем, заново назначить корневой узел\n                self.root = child;\n            }\n        }\n        // Число дочерних узлов = 2\n        (Some(_), Some(_)) => {\n            // Получить следующий узел после cur в симметричном обходе\n            let mut tmp = cur.borrow().right.clone();\n            while let Some(node) = tmp.clone() {\n                if node.borrow().left.is_some() {\n                    tmp = node.borrow().left.clone();\n                } else {\n                    break;\n                }\n            }\n            let tmp_val = tmp.unwrap().borrow().val;\n            // Рекурсивно удалить узел tmp\n            self.remove(tmp_val);\n            // Перезаписать cur значением tmp\n            cur.borrow_mut().val = tmp_val;\n        }\n    }\n}\n
        binary_search_tree.c
        /* Удаление узла */\n// Из-за подключения stdio.h здесь нельзя использовать ключевое слово remove\nvoid removeItem(BinarySearchTree *bst, int num) {\n    // Если дерево пусто, сразу вернуть\n    if (bst->root == NULL)\n        return;\n    TreeNode *cur = bst->root, *pre = NULL;\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur != NULL) {\n        // Найти узел для удаления и выйти из цикла\n        if (cur->val == num)\n            break;\n        pre = cur;\n        if (cur->val < num) {\n            // Удаляемый узел находится в правом поддереве root\n            cur = cur->right;\n        } else {\n            // Удаляемый узел находится в левом поддереве root\n            cur = cur->left;\n        }\n    }\n    // Если узел для удаления отсутствует, сразу вернуть\n    if (cur == NULL)\n        return;\n    // Проверить, есть ли дочерние узлы у удаляемого узла\n    if (cur->left == NULL || cur->right == NULL) {\n        /* Число дочерних узлов = 0 или 1 */\n        // Когда число дочерних узлов = 0 / 1, child = nullptr / этот дочерний узел\n        TreeNode *child = cur->left != NULL ? cur->left : cur->right;\n        // Удалить узел cur\n        if (pre->left == cur) {\n            pre->left = child;\n        } else {\n            pre->right = child;\n        }\n        // Освободить память\n        free(cur);\n    } else {\n        /* Число дочерних узлов = 2 */\n        // Получить следующий узел после cur в симметричном обходе\n        TreeNode *tmp = cur->right;\n        while (tmp->left != NULL) {\n            tmp = tmp->left;\n        }\n        int tmpVal = tmp->val;\n        // Рекурсивно удалить узел tmp\n        removeItem(bst, tmp->val);\n        // Перезаписать cur значением tmp\n        cur->val = tmpVal;\n    }\n}\n
        binary_search_tree.kt
        /* Удаление узла */\nfun remove(num: Int) {\n    // Если дерево пусто, сразу вернуть\n    if (root == null)\n        return\n    var cur = root\n    var pre: TreeNode? = null\n    // Искать в цикле и выйти после прохода за листовой узел\n    while (cur != null) {\n        // Найти узел для удаления и выйти из цикла\n        if (cur._val == num)\n            break\n        pre = cur\n        // Узел для удаления находится в правом поддереве cur\n        cur = if (cur._val < num)\n            cur.right\n        // Узел для удаления находится в левом поддереве cur\n        else\n            cur.left\n    }\n    // Если узел для удаления отсутствует, сразу вернуть\n    if (cur == null)\n        return\n    // Число дочерних узлов = 0 или 1\n    if (cur.left == null || cur.right == null) {\n        // Когда число дочерних узлов = 0 / 1, child = null / этот дочерний узел\n        val child = if (cur.left != null)\n            cur.left\n        else\n            cur.right\n        // Удалить узел cur\n        if (cur != root) {\n            if (pre!!.left == cur)\n                pre.left = child\n            else\n                pre.right = child\n        } else {\n            // Если удаляемый узел является корнем, заново назначить корневой узел\n            root = child\n        }\n        // Число дочерних узлов = 2\n    } else {\n        // Получить следующий узел после cur в симметричном обходе\n        var tmp = cur.right\n        while (tmp!!.left != null) {\n            tmp = tmp.left\n        }\n        // Рекурсивно удалить узел tmp\n        remove(tmp._val)\n        // Перезаписать cur значением tmp\n        cur._val = tmp._val\n    }\n}\n
        binary_search_tree.rb
        ### Удаление узла ###\ndef remove(num)\n  # Если дерево пусто, сразу вернуть\n  return if @root.nil?\n\n  # Искать в цикле и выйти после прохода за листовой узел\n  cur, pre = @root, nil\n  while !cur.nil?\n    # Найти узел для удаления и выйти из цикла\n    break if cur.val == num\n\n    pre = cur\n    # Узел для удаления находится в правом поддереве cur\n    if cur.val < num\n      cur = cur.right\n    # Узел для удаления находится в левом поддереве cur\n    else\n      cur = cur.left\n    end\n  end\n  # Если узел для удаления отсутствует, сразу вернуть\n  return if cur.nil?\n\n  # Число дочерних узлов = 0 или 1\n  if cur.left.nil? || cur.right.nil?\n    # Когда число дочерних узлов = 0 / 1, child = null / этот дочерний узел\n    child = cur.left || cur.right\n    # Удалить узел cur\n    if cur != @root\n      if pre.left == cur\n        pre.left = child\n      else\n        pre.right = child\n      end\n    else\n      # Если удаляемый узел является корнем, заново назначить корневой узел\n      @root = child\n    end\n  # Число дочерних узлов = 2\n  else\n    # Получить следующий узел после cur в симметричном обходе\n    tmp = cur.right\n    while !tmp.left.nil?\n      tmp = tmp.left\n    end\n    # Рекурсивно удалить узел tmp\n    remove(tmp.val)\n    # Перезаписать cur значением tmp\n    cur.val = tmp.val\n  end\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 7. Деревья","7.4   Двоичное дерево поиска"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#4","level":3,"title":"4.   Упорядоченность симметричного обхода","text":"

        Как показано на рисунке 7-22, симметричный обход двоичного дерева следует порядку «лево \\(\\rightarrow\\) корень \\(\\rightarrow\\) право», а двоичное дерево поиска удовлетворяет соотношению «левый дочерний узел \\(<\\) корень \\(<\\) правый дочерний узел».

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

        Используя это свойство возрастающей последовательности симметричного обхода, мы можем получить отсортированные данные из двоичного дерева поиска всего за \\(O(n)\\) времени, без дополнительной сортировки, что очень эффективно.

        Рисунок 7-22   Последовательность симметричного обхода двоичного дерева поиска

        ","path":["Глава 7. Деревья","7.4   Двоичное дерево поиска"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#742","level":2,"title":"7.4.2   Эффективность двоичного дерева поиска","text":"

        Для заданного набора данных можно рассмотреть хранение либо в массиве, либо в двоичном дереве поиска. Как видно по данным в таблице 7-2, временная сложность операций двоичного дерева поиска имеет логарифмический порядок и обеспечивает стабильную высокую производительность. Только в сценариях с очень частыми вставками и редкими поисками и удалениями массив может быть эффективнее, чем двоичное дерево поиска.

        Таблица 7-2   Сравнение эффективности массива и дерева поиска

        Неупорядоченный массив Двоичное дерево поиска Поиск элемента \\(O(n)\\) \\(O(\\log n)\\) Вставка элемента \\(O(1)\\) \\(O(\\log n)\\) Удаление элемента \\(O(n)\\) \\(O(\\log n)\\)

        В идеальном случае двоичное дерево поиска является «сбалансированным», и тогда любой узел можно найти за \\(\\log n\\) итераций.

        Однако если в двоичное дерево поиска непрерывно вставлять и удалять узлы, оно может выродиться в связный список, как показано на рисунке 7-23. Тогда временная сложность различных операций тоже вырождается до \\(O(n)\\) .

        Рисунок 7-23   Деградация двоичного дерева поиска

        ","path":["Глава 7. Деревья","7.4   Двоичное дерево поиска"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#743","level":2,"title":"7.4.3   Типичные применения двоичного дерева поиска","text":"
        • Используется как многоуровневый индекс в системах, обеспечивая эффективный поиск, вставку и удаление.
        • Служит базовой структурой данных для некоторых поисковых алгоритмов.
        • Применяется для хранения потока данных в отсортированном состоянии.
        ","path":["Глава 7. Деревья","7.4   Двоичное дерево поиска"],"tags":[]},{"location":"chapter_tree/binary_tree/","level":1,"title":"7.1   Двоичное дерево","text":"

        Двоичное дерево (binary tree) - это нелинейная структура данных, представляющая отношения между «предками» и «потомками» и отражающая логику «разделяй и властвуй». Подобно связному списку, базовой единицей двоичного дерева является узел. Каждый узел содержит значение, ссылку на левого дочернего узла и ссылку на правого дочернего узла.

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
        class TreeNode:\n    \"\"\"Класс узла двоичного дерева\"\"\"\n    def __init__(self, val: int):\n        self.val: int = val                # Значение узла\n        self.left: TreeNode | None = None  # Ссылка на левого дочернего узла\n        self.right: TreeNode | None = None # Ссылка на правого дочернего узла\n
        /* Структура узла двоичного дерева */\nstruct TreeNode {\n    int val;          // Значение узла\n    TreeNode *left;   // Указатель на левого дочернего узла\n    TreeNode *right;  // Указатель на правого дочернего узла\n    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}\n};\n
        /* Класс узла двоичного дерева */\nclass TreeNode {\n    int val;         // Значение узла\n    TreeNode left;   // Ссылка на левого дочернего узла\n    TreeNode right;  // Ссылка на правого дочернего узла\n    TreeNode(int x) { val = x; }\n}\n
        /* Класс узла двоичного дерева */\nclass TreeNode(int? x) {\n    public int? val = x;    // Значение узла\n    public TreeNode? left;  // Ссылка на левого дочернего узла\n    public TreeNode? right; // Ссылка на правого дочернего узла\n}\n
        /* Структура узла двоичного дерева */\ntype TreeNode struct {\n    Val   int\n    Left  *TreeNode\n    Right *TreeNode\n}\n/* Конструктор */\nfunc NewTreeNode(v int) *TreeNode {\n    return &TreeNode{\n        Left:  nil, // Указатель на левого дочернего узла\n        Right: nil, // Указатель на правого дочернего узла\n        Val:   v,   // Значение узла\n    }\n}\n
        /* Класс узла двоичного дерева */\nclass TreeNode {\n    var val: Int // Значение узла\n    var left: TreeNode? // Ссылка на левого дочернего узла\n    var right: TreeNode? // Ссылка на правого дочернего узла\n\n    init(x: Int) {\n        val = x\n    }\n}\n
        /* Класс узла двоичного дерева */\nclass TreeNode {\n    val; // Значение узла\n    left; // Указатель на левого дочернего узла\n    right; // Указатель на правого дочернего узла\n    constructor(val, left, right) {\n        this.val = val === undefined ? 0 : val;\n        this.left = left === undefined ? null : left;\n        this.right = right === undefined ? null : right;\n    }\n}\n
        /* Класс узла двоичного дерева */\nclass TreeNode {\n    val: number;\n    left: TreeNode | null;\n    right: TreeNode | null;\n\n    constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {\n        this.val = val === undefined ? 0 : val; // Значение узла\n        this.left = left === undefined ? null : left; // Ссылка на левого дочернего узла\n        this.right = right === undefined ? null : right; // Ссылка на правого дочернего узла\n    }\n}\n
        /* Класс узла двоичного дерева */\nclass TreeNode {\n  int val;         // Значение узла\n  TreeNode? left;  // Ссылка на левого дочернего узла\n  TreeNode? right; // Ссылка на правого дочернего узла\n  TreeNode(this.val, [this.left, this.right]);\n}\n
        use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* Структура узла двоичного дерева */\nstruct TreeNode {\n    val: i32,                               // Значение узла\n    left: Option<Rc<RefCell<TreeNode>>>,    // Ссылка на левого дочернего узла\n    right: Option<Rc<RefCell<TreeNode>>>,   // Ссылка на правого дочернего узла\n}\n\nimpl TreeNode {\n    /* Конструктор */\n    fn new(val: i32) -> Rc<RefCell<Self>> {\n        Rc::new(RefCell::new(Self {\n            val,\n            left: None,\n            right: None\n        }))\n    }\n}\n
        /* Структура узла двоичного дерева */\ntypedef struct TreeNode {\n    int val;                // Значение узла\n    int height;             // Высота узла\n    struct TreeNode *left;  // Указатель на левого дочернего узла\n    struct TreeNode *right; // Указатель на правого дочернего узла\n} TreeNode;\n\n/* Конструктор */\nTreeNode *newTreeNode(int val) {\n    TreeNode *node;\n\n    node = (TreeNode *)malloc(sizeof(TreeNode));\n    node->val = val;\n    node->height = 0;\n    node->left = NULL;\n    node->right = NULL;\n    return node;\n}\n
        /* Класс узла двоичного дерева */\nclass TreeNode(val _val: Int) {  // Значение узла\n    val left: TreeNode? = null   // Ссылка на левого дочернего узла\n    val right: TreeNode? = null  // Ссылка на правого дочернего узла\n}\n
        ### Класс узла двоичного дерева ###\nclass TreeNode\n  attr_accessor :val    # Значение узла\n  attr_accessor :left   # Ссылка на левого дочернего узла\n  attr_accessor :right  # Ссылка на правого дочернего узла\n\n  def initialize(val)\n    @val = val\n  end\nend\n

        Каждый узел имеет две ссылки (указателя), которые соответственно указывают на левого дочернего узла (left-child node) и правого дочернего узла (right-child node). Данный узел называется родительским узлом (parent node) для этих двух дочерних узлов. Если задан некоторый узел двоичного дерева, то дерево, образованное его левым дочерним узлом и всеми узлами ниже него, называется левым поддеревом (left subtree) этого узла. Аналогично определяется правое поддерево (right subtree).

        Узлы, не имеющие дочерних узлов, называют листьями, а все остальные узлы содержат дочерние узлы и непустые поддеревья. Как показано на рисунке 7-1, если рассматривать «узел 2» как родительский, то его левым и правым дочерними узлами будут «узел 4» и «узел 5». Левое поддерево - это «узел 4 и дерево ниже него», а правое поддерево - это «узел 5 и дерево ниже него».

        Рисунок 7-1   Родительский узел, дочерние узлы и поддеревья

        ","path":["Глава 7. Деревья","7.1   Двоичное дерево"],"tags":[]},{"location":"chapter_tree/binary_tree/#711","level":2,"title":"7.1.1   Распространенные термины двоичного дерева","text":"

        Распространенные термины двоичного дерева показаны на рисунке 7-2.

        • Корневой узел (root node): узел, расположенный на верхнем уровне двоичного дерева и не имеющий родительского узла.
        • Листовой узел (leaf node): узел без дочерних узлов. Оба его указателя направлены на None .
        • Ребро (edge): отрезок, соединяющий два узла, то есть ссылка (указатель) между узлами.
        • Уровень (level) узла: увеличивается сверху вниз. Уровень корневого узла равен 1 .
        • Степень (degree) узла: число дочерних узлов данного узла. В двоичном дереве возможны степени 0, 1, 2 .
        • Высота (height) двоичного дерева: число ребер от корневого узла до самого удаленного листового узла.
        • Глубина (depth) узла: число ребер от корневого узла до данного узла.
        • Высота (height) узла: число ребер от самого удаленного листового узла до данного узла.

        Рисунок 7-2   Распространенные термины двоичного дерева

        Tip

        Обычно под «высотой» и «глубиной» понимают «число пройденных ребер», но в некоторых задачах или учебниках их могут определять как «число пройденных узлов». В таком случае и высоту, и глубину нужно увеличить на 1 .

        ","path":["Глава 7. Деревья","7.1   Двоичное дерево"],"tags":[]},{"location":"chapter_tree/binary_tree/#712","level":2,"title":"7.1.2   Базовые операции двоичного дерева","text":"","path":["Глава 7. Деревья","7.1   Двоичное дерево"],"tags":[]},{"location":"chapter_tree/binary_tree/#1","level":3,"title":"1.   Инициализация двоичного дерева","text":"

        Как и в связном списке, сначала инициализируются узлы, а затем между ними строятся ссылки (указатели).

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree.py
        # Инициализация двоичного дерева\n# Инициализация узлов\nn1 = TreeNode(val=1)\nn2 = TreeNode(val=2)\nn3 = TreeNode(val=3)\nn4 = TreeNode(val=4)\nn5 = TreeNode(val=5)\n# Построение ссылок (указателей) между узлами\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n
        binary_tree.cpp
        /* Инициализация двоичного дерева */\n// Инициализация узлов\nTreeNode* n1 = new TreeNode(1);\nTreeNode* n2 = new TreeNode(2);\nTreeNode* n3 = new TreeNode(3);\nTreeNode* n4 = new TreeNode(4);\nTreeNode* n5 = new TreeNode(5);\n// Построение ссылок (указателей) между узлами\nn1->left = n2;\nn1->right = n3;\nn2->left = n4;\nn2->right = n5;\n
        binary_tree.java
        // Инициализация узлов\nTreeNode n1 = new TreeNode(1);\nTreeNode n2 = new TreeNode(2);\nTreeNode n3 = new TreeNode(3);\nTreeNode n4 = new TreeNode(4);\nTreeNode n5 = new TreeNode(5);\n// Построение ссылок (указателей) между узлами\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
        binary_tree.cs
        /* Инициализация двоичного дерева */\n// Инициализация узлов\nTreeNode n1 = new(1);\nTreeNode n2 = new(2);\nTreeNode n3 = new(3);\nTreeNode n4 = new(4);\nTreeNode n5 = new(5);\n// Построение ссылок (указателей) между узлами\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
        binary_tree.go
        /* Инициализация двоичного дерева */\n// Инициализация узлов\nn1 := NewTreeNode(1)\nn2 := NewTreeNode(2)\nn3 := NewTreeNode(3)\nn4 := NewTreeNode(4)\nn5 := NewTreeNode(5)\n// Построение ссылок (указателей) между узлами\nn1.Left = n2\nn1.Right = n3\nn2.Left = n4\nn2.Right = n5\n
        binary_tree.swift
        // Инициализация узлов\nlet n1 = TreeNode(x: 1)\nlet n2 = TreeNode(x: 2)\nlet n3 = TreeNode(x: 3)\nlet n4 = TreeNode(x: 4)\nlet n5 = TreeNode(x: 5)\n// Построение ссылок (указателей) между узлами\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n
        binary_tree.js
        /* Инициализация двоичного дерева */\n// Инициализация узлов\nlet n1 = new TreeNode(1),\n    n2 = new TreeNode(2),\n    n3 = new TreeNode(3),\n    n4 = new TreeNode(4),\n    n5 = new TreeNode(5);\n// Построение ссылок (указателей) между узлами\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
        binary_tree.ts
        /* Инициализация двоичного дерева */\n// Инициализация узлов\nlet n1 = new TreeNode(1),\n    n2 = new TreeNode(2),\n    n3 = new TreeNode(3),\n    n4 = new TreeNode(4),\n    n5 = new TreeNode(5);\n// Построение ссылок (указателей) между узлами\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
        binary_tree.dart
        /* Инициализация двоичного дерева */\n// Инициализация узлов\nTreeNode n1 = new TreeNode(1);\nTreeNode n2 = new TreeNode(2);\nTreeNode n3 = new TreeNode(3);\nTreeNode n4 = new TreeNode(4);\nTreeNode n5 = new TreeNode(5);\n// Построение ссылок (указателей) между узлами\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
        binary_tree.rs
        // Инициализация узлов\nlet n1 = TreeNode::new(1);\nlet n2 = TreeNode::new(2);\nlet n3 = TreeNode::new(3);\nlet n4 = TreeNode::new(4);\nlet n5 = TreeNode::new(5);\n// Построение ссылок (указателей) между узлами\nn1.borrow_mut().left = Some(n2.clone());\nn1.borrow_mut().right = Some(n3);\nn2.borrow_mut().left = Some(n4);\nn2.borrow_mut().right = Some(n5);\n
        binary_tree.c
        /* Инициализация двоичного дерева */\n// Инициализация узлов\nTreeNode *n1 = newTreeNode(1);\nTreeNode *n2 = newTreeNode(2);\nTreeNode *n3 = newTreeNode(3);\nTreeNode *n4 = newTreeNode(4);\nTreeNode *n5 = newTreeNode(5);\n// Построение ссылок (указателей) между узлами\nn1->left = n2;\nn1->right = n3;\nn2->left = n4;\nn2->right = n5;\n
        binary_tree.kt
        // Инициализация узлов\nval n1 = TreeNode(1)\nval n2 = TreeNode(2)\nval n3 = TreeNode(3)\nval n4 = TreeNode(4)\nval n5 = TreeNode(5)\n// Построение ссылок (указателей) между узлами\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n
        binary_tree.rb
        # Инициализация двоичного дерева\n# Инициализация узлов\nn1 = TreeNode.new(1)\nn2 = TreeNode.new(2)\nn3 = TreeNode.new(3)\nn4 = TreeNode.new(4)\nn5 = TreeNode.new(5)\n# Построение ссылок (указателей) между узлами\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n
        Визуализация выполнения

        https://pythontutor.com/render.html#code=class%20TreeNode%3A%0A%20%20%20%20%22%22%22%D0%9A%D0%BB%D0%B0%D1%81%D1%81%20%D1%83%D0%B7%D0%BB%D0%B0%20%D0%B4%D0%B2%D0%BE%D0%B8%D1%87%D0%BD%D0%BE%D0%B3%D0%BE%20%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%B0%22%22%22%0A%20%20%20%20def%20__init__%28self%2C%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%D0%97%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%D1%83%D0%B7%D0%BB%D0%B0%0A%20%20%20%20%20%20%20%20self.left%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%D0%A1%D1%81%D1%8B%D0%BB%D0%BA%D0%B0%20%D0%BD%D0%B0%20%D0%BB%D0%B5%D0%B2%D1%8B%D0%B9%20%D0%B4%D0%BE%D1%87%D0%B5%D1%80%D0%BD%D0%B8%D0%B9%20%D1%83%D0%B7%D0%B5%D0%BB%0A%20%20%20%20%20%20%20%20self.right%3A%20TreeNode%20%7C%20None%20%3D%20None%20%23%20%D0%A1%D1%81%D1%8B%D0%BB%D0%BA%D0%B0%20%D0%BD%D0%B0%20%D0%BF%D1%80%D0%B0%D0%B2%D1%8B%D0%B9%20%D0%B4%D0%BE%D1%87%D0%B5%D1%80%D0%BD%D0%B8%D0%B9%20%D1%83%D0%B7%D0%B5%D0%BB%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D0%B4%D0%B2%D0%BE%D0%B8%D1%87%D0%BD%D0%BE%D0%B5%20%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%BE%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%83%D0%B7%D0%B5%D0%BB%0A%20%20%20%20n1%20%3D%20TreeNode%28val%3D1%29%0A%20%20%20%20n2%20%3D%20TreeNode%28val%3D2%29%0A%20%20%20%20n3%20%3D%20TreeNode%28val%3D3%29%0A%20%20%20%20n4%20%3D%20TreeNode%28val%3D4%29%0A%20%20%20%20n5%20%3D%20TreeNode%28val%3D5%29%0A%20%20%20%20%23%20%D0%9F%D0%BE%D1%81%D1%82%D1%80%D0%BE%D0%B8%D1%82%D1%8C%20%D1%81%D1%81%D1%8B%D0%BB%D0%BA%D0%B8%20%D0%BC%D0%B5%D0%B6%D0%B4%D1%83%20%D1%83%D0%B7%D0%BB%D0%B0%D0%BC%D0%B8%20%28%D1%83%D0%BA%D0%B0%D0%B7%D0%B0%D1%82%D0%B5%D0%BB%D0%B8%29%0A%20%20%20%20n1.left%20%3D%20n2%0A%20%20%20%20n1.right%20%3D%20n3%0A%20%20%20%20n2.left%20%3D%20n4%0A%20%20%20%20n2.right%20%3D%20n5&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

        ","path":["Глава 7. Деревья","7.1   Двоичное дерево"],"tags":[]},{"location":"chapter_tree/binary_tree/#2","level":3,"title":"2.   Вставка и удаление узлов","text":"

        Как и в связном списке, вставка и удаление узлов в двоичном дереве могут выполняться через изменение указателей. На рисунке 7-3 приведен пример.

        Рисунок 7-3   Вставка и удаление узлов в двоичном дереве

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree.py
        # Вставка и удаление узлов\np = TreeNode(0)\n# Вставить узел P между n1 -> n2\nn1.left = p\np.left = n2\n# Удалить узел P\nn1.left = n2\n
        binary_tree.cpp
        /* Вставка и удаление узлов */\nTreeNode* P = new TreeNode(0);\n// Вставить узел P между n1 -> n2\nn1->left = P;\nP->left = n2;\n// Удалить узел P\nn1->left = n2;\n// Освободить память\ndelete P;\n
        binary_tree.java
        TreeNode P = new TreeNode(0);\n// Вставить узел P между n1 -> n2\nn1.left = P;\nP.left = n2;\n// Удалить узел P\nn1.left = n2;\n
        binary_tree.cs
        /* Вставка и удаление узлов */\nTreeNode P = new(0);\n// Вставить узел P между n1 -> n2\nn1.left = P;\nP.left = n2;\n// Удалить узел P\nn1.left = n2;\n
        binary_tree.go
        /* Вставка и удаление узлов */\n// Вставить узел P между n1 -> n2\np := NewTreeNode(0)\nn1.Left = p\np.Left = n2\n// Удалить узел P\nn1.Left = n2\n
        binary_tree.swift
        let P = TreeNode(x: 0)\n// Вставить узел P между n1 -> n2\nn1.left = P\nP.left = n2\n// Удалить узел P\nn1.left = n2\n
        binary_tree.js
        /* Вставка и удаление узлов */\nlet P = new TreeNode(0);\n// Вставить узел P между n1 -> n2\nn1.left = P;\nP.left = n2;\n// Удалить узел P\nn1.left = n2;\n
        binary_tree.ts
        /* Вставка и удаление узлов */\nconst P = new TreeNode(0);\n// Вставить узел P между n1 -> n2\nn1.left = P;\nP.left = n2;\n// Удалить узел P\nn1.left = n2;\n
        binary_tree.dart
        /* Вставка и удаление узлов */\nTreeNode P = new TreeNode(0);\n// Вставить узел P между n1 -> n2\nn1.left = P;\nP.left = n2;\n// Удалить узел P\nn1.left = n2;\n
        binary_tree.rs
        let p = TreeNode::new(0);\n// Вставить узел P между n1 -> n2\nn1.borrow_mut().left = Some(p.clone());\np.borrow_mut().left = Some(n2.clone());\n// Удалить узел p\nn1.borrow_mut().left = Some(n2);\n
        binary_tree.c
        /* Вставка и удаление узлов */\nTreeNode *P = newTreeNode(0);\n// Вставить узел P между n1 -> n2\nn1->left = P;\nP->left = n2;\n// Удалить узел P\nn1->left = n2;\n// Освободить память\nfree(P);\n
        binary_tree.kt
        val P = TreeNode(0)\n// Вставить узел P между n1 -> n2\nn1.left = P\nP.left = n2\n// Удалить узел P\nn1.left = n2\n
        binary_tree.rb
        # Вставка и удаление узлов\n_p = TreeNode.new(0)\n# Вставить узел _p между n1 -> n2\nn1.left = _p\n_p.left = n2\n# Удалить узел\nn1.left = n2\n
        Визуализация выполнения

        https://pythontutor.com/render.html#code=class%20TreeNode%3A%0A%20%20%20%20%22%22%22%D0%9A%D0%BB%D0%B0%D1%81%D1%81%20%D1%83%D0%B7%D0%BB%D0%B0%20%D0%B4%D0%B2%D0%BE%D0%B8%D1%87%D0%BD%D0%BE%D0%B3%D0%BE%20%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%B0%22%22%22%0A%20%20%20%20def%20__init__%28self%2C%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%D0%97%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%D1%83%D0%B7%D0%BB%D0%B0%0A%20%20%20%20%20%20%20%20self.left%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%D0%A1%D1%81%D1%8B%D0%BB%D0%BA%D0%B0%20%D0%BD%D0%B0%20%D0%BB%D0%B5%D0%B2%D1%8B%D0%B9%20%D0%B4%D0%BE%D1%87%D0%B5%D1%80%D0%BD%D0%B8%D0%B9%20%D1%83%D0%B7%D0%B5%D0%BB%0A%20%20%20%20%20%20%20%20self.right%3A%20TreeNode%20%7C%20None%20%3D%20None%20%23%20%D0%A1%D1%81%D1%8B%D0%BB%D0%BA%D0%B0%20%D0%BD%D0%B0%20%D0%BF%D1%80%D0%B0%D0%B2%D1%8B%D0%B9%20%D0%B4%D0%BE%D1%87%D0%B5%D1%80%D0%BD%D0%B8%D0%B9%20%D1%83%D0%B7%D0%B5%D0%BB%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D0%B4%D0%B2%D0%BE%D0%B8%D1%87%D0%BD%D0%BE%D0%B5%20%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%BE%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%83%D0%B7%D0%B5%D0%BB%0A%20%20%20%20n1%20%3D%20TreeNode%28val%3D1%29%0A%20%20%20%20n2%20%3D%20TreeNode%28val%3D2%29%0A%20%20%20%20n3%20%3D%20TreeNode%28val%3D3%29%0A%20%20%20%20n4%20%3D%20TreeNode%28val%3D4%29%0A%20%20%20%20n5%20%3D%20TreeNode%28val%3D5%29%0A%20%20%20%20%23%20%D0%9F%D0%BE%D1%81%D1%82%D1%80%D0%BE%D0%B8%D1%82%D1%8C%20%D1%81%D1%81%D1%8B%D0%BB%D0%BA%D0%B8%20%D0%BC%D0%B5%D0%B6%D0%B4%D1%83%20%D1%83%D0%B7%D0%BB%D0%B0%D0%BC%D0%B8%20%28%D1%83%D0%BA%D0%B0%D0%B7%D0%B0%D1%82%D0%B5%D0%BB%D0%B8%29%0A%20%20%20%20n1.left%20%3D%20n2%0A%20%20%20%20n1.right%20%3D%20n3%0A%20%20%20%20n2.left%20%3D%20n4%0A%20%20%20%20n2.right%20%3D%20n5%0A%0A%20%20%20%20%23%20%D0%92%D1%81%D1%82%D0%B0%D0%B2%D0%BA%D0%B0%20%D0%B8%20%D1%83%D0%B4%D0%B0%D0%BB%D0%B5%D0%BD%D0%B8%D0%B5%20%D1%83%D0%B7%D0%BB%D0%BE%D0%B2%0A%20%20%20%20p%20%3D%20TreeNode%280%29%0A%20%20%20%20%23%20%D0%92%D1%81%D1%82%D0%B0%D0%B2%D0%B8%D1%82%D1%8C%20%D1%83%D0%B7%D0%B5%D0%BB%20P%20%D0%BC%D0%B5%D0%B6%D0%B4%D1%83%20n1%20-%3E%20n2%0A%20%20%20%20n1.left%20%3D%20p%0A%20%20%20%20p.left%20%3D%20n2%0A%20%20%20%20%23%20%D0%A3%D0%B4%D0%B0%D0%BB%D0%B8%D1%82%D1%8C%20%D1%83%D0%B7%D0%B5%D0%BB%20P%0A%20%20%20%20n1.left%20%3D%20n2&cumulative=false&curInstr=37&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

        Tip

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

        ","path":["Глава 7. Деревья","7.1   Двоичное дерево"],"tags":[]},{"location":"chapter_tree/binary_tree/#713","level":2,"title":"7.1.3   Распространенные типы двоичных деревьев","text":"","path":["Глава 7. Деревья","7.1   Двоичное дерево"],"tags":[]},{"location":"chapter_tree/binary_tree/#1_1","level":3,"title":"1.   Идеальное двоичное дерево","text":"

        Как показано на рисунке 7-4, идеальное двоичное дерево (perfect binary tree) полностью заполнено на всех уровнях. В идеальном двоичном дереве степень листовых узлов равна \\(0\\) , а у всех остальных узлов степень равна \\(2\\). Если высота дерева равна \\(h\\) , то общее число узлов равно \\(2^{h+1} - 1\\) , что образует стандартную экспоненциальную зависимость и отражает часто встречающееся в природе явление клеточного деления.

        Tip

        В китайскоязычном сообществе идеальное двоичное дерево часто называют полностью заполненным двоичным деревом.

        Рисунок 7-4   Идеальное двоичное дерево

        ","path":["Глава 7. Деревья","7.1   Двоичное дерево"],"tags":[]},{"location":"chapter_tree/binary_tree/#2_1","level":3,"title":"2.   Полное двоичное дерево","text":"

        Как показано на рисунке 7-5, полное двоичное дерево (complete binary tree) допускает неполное заполнение только на самом нижнем уровне, причем узлы этого уровня должны непрерывно заполняться слева направо. Стоит отметить, что идеальное двоичное дерево тоже является полным двоичным деревом.

        Рисунок 7-5   Полное двоичное дерево

        ","path":["Глава 7. Деревья","7.1   Двоичное дерево"],"tags":[]},{"location":"chapter_tree/binary_tree/#3","level":3,"title":"3.   Строгое двоичное дерево","text":"

        Как показано на рисунке 7-6, строгое двоичное дерево (full binary tree) имеет у всех нелистовых узлов ровно двух дочерних узлов.

        Рисунок 7-6   Строгое двоичное дерево

        ","path":["Глава 7. Деревья","7.1   Двоичное дерево"],"tags":[]},{"location":"chapter_tree/binary_tree/#4","level":3,"title":"4.   Сбалансированное двоичное дерево","text":"

        Как показано на рисунке 7-7, в сбалансированном двоичном дереве (balanced binary tree) для любого узла абсолютное значение разности высот левого и правого поддеревьев не превышает 1 .

        Рисунок 7-7   Сбалансированное двоичное дерево

        ","path":["Глава 7. Деревья","7.1   Двоичное дерево"],"tags":[]},{"location":"chapter_tree/binary_tree/#714","level":2,"title":"7.1.4   Вырождение двоичного дерева","text":"

        На рисунке 7-8 показаны идеальная структура двоичного дерева и вырожденная структура. Когда каждый уровень двоичного дерева полностью заполнен узлами, мы получаем «идеальное двоичное дерево». Когда же все узлы смещаются к одной стороне, двоичное дерево вырождается в «связный список».

        • Идеальное двоичное дерево соответствует лучшему случаю и позволяет в полной мере раскрыть преимущества подхода «разделяй и властвуй».
        • Связный список представляет противоположную крайность: все операции становятся линейными, а временная сложность деградирует до \\(O(n)\\) .

        Рисунок 7-8   Лучший и худший случаи структуры двоичного дерева

        Как показано в таблице 7-1, в лучшем и худшем случаях число листовых узлов, общее число узлов, высота и другие характеристики двоичного дерева достигают максимума или минимума.

        Таблица 7-1   Лучший и худший случаи структуры двоичного дерева

        Идеальное двоичное дерево Связный список Число узлов на уровне \\(i\\) \\(2^{i-1}\\) \\(1\\) Число листьев у дерева высоты \\(h\\) \\(2^h\\) \\(1\\) Общее число узлов у дерева высоты \\(h\\) \\(2^{h+1} - 1\\) \\(h + 1\\) Высота дерева с \\(n\\) узлами \\(\\log_2 (n+1) - 1\\) \\(n - 1\\)","path":["Глава 7. Деревья","7.1   Двоичное дерево"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/","level":1,"title":"7.2   Обход двоичного дерева","text":"

        С точки зрения физической структуры дерево представляет собой разновидность структуры данных на основе связей, поэтому его обход выполняется через последовательный доступ к узлам по указателям. Однако дерево является нелинейной структурой данных, а значит, его обход сложнее, чем обход связного списка, и для него требуется использовать поисковые алгоритмы.

        К распространенным способам обхода двоичного дерева относятся обход по уровням, прямой обход, симметричный обход и обратный обход.

        ","path":["Глава 7. Деревья","7.2   Обход двоичного дерева"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#721","level":2,"title":"7.2.1   Обход по уровням","text":"

        Как показано на рисунке 7-9, обход по уровням (level-order traversal) проходит двоичное дерево сверху вниз по уровням и на каждом уровне посещает узлы слева направо.

        По своей сути обход по уровням относится к обходу в ширину (breadth-first traversal), также называемому поиском в ширину (breadth-first search, BFS). Он отражает идею «расширяться от центра к периферии слой за слоем».

        Рисунок 7-9   Обход двоичного дерева по уровням

        ","path":["Глава 7. Деревья","7.2   Обход двоичного дерева"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#1","level":3,"title":"1.   Код реализации","text":"

        Обход в ширину обычно реализуется с помощью «очереди». Очередь подчиняется правилу «первым пришел - первым вышел», а обход в ширину подчиняется правилу «продвигаться по уровням», поэтому стоящая за ними идея согласована. Код реализации приведен ниже:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree_bfs.py
        def level_order(root: TreeNode | None) -> list[int]:\n    \"\"\"Обход в ширину\"\"\"\n    # Инициализировать очередь и добавить корневой узел\n    queue: deque[TreeNode] = deque()\n    queue.append(root)\n    # Инициализировать список для хранения последовательности обхода\n    res = []\n    while queue:\n        node: TreeNode = queue.popleft()  # Извлечение из очереди\n        res.append(node.val)  # Сохранить значение узла\n        if node.left is not None:\n            queue.append(node.left)  # Поместить левый дочерний узел в очередь\n        if node.right is not None:\n            queue.append(node.right)  # Поместить правый дочерний узел в очередь\n    return res\n
        binary_tree_bfs.cpp
        /* Обход в ширину */\nvector<int> levelOrder(TreeNode *root) {\n    // Инициализировать очередь и добавить корневой узел\n    queue<TreeNode *> queue;\n    queue.push(root);\n    // Инициализировать список для хранения последовательности обхода\n    vector<int> vec;\n    while (!queue.empty()) {\n        TreeNode *node = queue.front();\n        queue.pop();              // Извлечение из очереди\n        vec.push_back(node->val); // Сохранить значение узла\n        if (node->left != nullptr)\n            queue.push(node->left); // Поместить левый дочерний узел в очередь\n        if (node->right != nullptr)\n            queue.push(node->right); // Поместить правый дочерний узел в очередь\n    }\n    return vec;\n}\n
        binary_tree_bfs.java
        /* Обход в ширину */\nList<Integer> levelOrder(TreeNode root) {\n    // Инициализировать очередь и добавить корневой узел\n    Queue<TreeNode> queue = new LinkedList<>();\n    queue.add(root);\n    // Инициализировать список для хранения последовательности обхода\n    List<Integer> list = new ArrayList<>();\n    while (!queue.isEmpty()) {\n        TreeNode node = queue.poll(); // Извлечение из очереди\n        list.add(node.val);           // Сохранить значение узла\n        if (node.left != null)\n            queue.offer(node.left);   // Поместить левый дочерний узел в очередь\n        if (node.right != null)\n            queue.offer(node.right);  // Поместить правый дочерний узел в очередь\n    }\n    return list;\n}\n
        binary_tree_bfs.cs
        /* Обход в ширину */\nList<int> LevelOrder(TreeNode root) {\n    // Инициализировать очередь и добавить корневой узел\n    Queue<TreeNode> queue = new();\n    queue.Enqueue(root);\n    // Инициализировать список для хранения последовательности обхода\n    List<int> list = [];\n    while (queue.Count != 0) {\n        TreeNode node = queue.Dequeue(); // Извлечение из очереди\n        list.Add(node.val!.Value);       // Сохранить значение узла\n        if (node.left != null)\n            queue.Enqueue(node.left);    // Поместить левый дочерний узел в очередь\n        if (node.right != null)\n            queue.Enqueue(node.right);   // Поместить правый дочерний узел в очередь\n    }\n    return list;\n}\n
        binary_tree_bfs.go
        /* Обход в ширину */\nfunc levelOrder(root *TreeNode) []any {\n    // Инициализировать очередь и добавить корневой узел\n    queue := list.New()\n    queue.PushBack(root)\n    // Инициализировать срез для хранения последовательности обхода\n    nums := make([]any, 0)\n    for queue.Len() > 0 {\n        // Извлечение из очереди\n        node := queue.Remove(queue.Front()).(*TreeNode)\n        // Сохранить значение узла\n        nums = append(nums, node.Val)\n        if node.Left != nil {\n            // Поместить левый дочерний узел в очередь\n            queue.PushBack(node.Left)\n        }\n        if node.Right != nil {\n            // Поместить правый дочерний узел в очередь\n            queue.PushBack(node.Right)\n        }\n    }\n    return nums\n}\n
        binary_tree_bfs.swift
        /* Обход в ширину */\nfunc levelOrder(root: TreeNode) -> [Int] {\n    // Инициализировать очередь и добавить корневой узел\n    var queue: [TreeNode] = [root]\n    // Инициализировать список для хранения последовательности обхода\n    var list: [Int] = []\n    while !queue.isEmpty {\n        let node = queue.removeFirst() // Извлечение из очереди\n        list.append(node.val) // Сохранить значение узла\n        if let left = node.left {\n            queue.append(left) // Поместить левый дочерний узел в очередь\n        }\n        if let right = node.right {\n            queue.append(right) // Поместить правый дочерний узел в очередь\n        }\n    }\n    return list\n}\n
        binary_tree_bfs.js
        /* Обход в ширину */\nfunction levelOrder(root) {\n    // Инициализировать очередь и добавить корневой узел\n    const queue = [root];\n    // Инициализировать список для хранения последовательности обхода\n    const list = [];\n    while (queue.length) {\n        let node = queue.shift(); // Извлечение из очереди\n        list.push(node.val); // Сохранить значение узла\n        if (node.left) queue.push(node.left); // Поместить левый дочерний узел в очередь\n        if (node.right) queue.push(node.right); // Поместить правый дочерний узел в очередь\n    }\n    return list;\n}\n
        binary_tree_bfs.ts
        /* Обход в ширину */\nfunction levelOrder(root: TreeNode | null): number[] {\n    // Инициализировать очередь и добавить корневой узел\n    const queue = [root];\n    // Инициализировать список для хранения последовательности обхода\n    const list: number[] = [];\n    while (queue.length) {\n        let node = queue.shift() as TreeNode; // Извлечение из очереди\n        list.push(node.val); // Сохранить значение узла\n        if (node.left) {\n            queue.push(node.left); // Поместить левый дочерний узел в очередь\n        }\n        if (node.right) {\n            queue.push(node.right); // Поместить правый дочерний узел в очередь\n        }\n    }\n    return list;\n}\n
        binary_tree_bfs.dart
        /* Обход в ширину */\nList<int> levelOrder(TreeNode? root) {\n  // Инициализировать очередь и добавить корневой узел\n  Queue<TreeNode?> queue = Queue();\n  queue.add(root);\n  // Инициализировать список для хранения последовательности обхода\n  List<int> res = [];\n  while (queue.isNotEmpty) {\n    TreeNode? node = queue.removeFirst(); // Извлечение из очереди\n    res.add(node!.val); // Сохранить значение узла\n    if (node.left != null) queue.add(node.left); // Поместить левый дочерний узел в очередь\n    if (node.right != null) queue.add(node.right); // Поместить правый дочерний узел в очередь\n  }\n  return res;\n}\n
        binary_tree_bfs.rs
        /* Обход в ширину */\nfn level_order(root: &Rc<RefCell<TreeNode>>) -> Vec<i32> {\n    // Инициализировать очередь и добавить корневой узел\n    let mut que = VecDeque::new();\n    que.push_back(root.clone());\n    // Инициализировать список для хранения последовательности обхода\n    let mut vec = Vec::new();\n\n    while let Some(node) = que.pop_front() {\n        // Извлечение из очереди\n        vec.push(node.borrow().val); // Сохранить значение узла\n        if let Some(left) = node.borrow().left.as_ref() {\n            que.push_back(left.clone()); // Поместить левый дочерний узел в очередь\n        }\n        if let Some(right) = node.borrow().right.as_ref() {\n            que.push_back(right.clone()); // Поместить правый дочерний узел в очередь\n        };\n    }\n    vec\n}\n
        binary_tree_bfs.c
        /* Обход в ширину */\nint *levelOrder(TreeNode *root, int *size) {\n    /* Вспомогательная очередь */\n    int front, rear;\n    int index, *arr;\n    TreeNode *node;\n    TreeNode **queue;\n\n    /* Вспомогательная очередь */\n    queue = (TreeNode **)malloc(sizeof(TreeNode *) * MAX_SIZE);\n    // Указатель очереди\n    front = 0, rear = 0;\n    // Добавить корневой узел\n    queue[rear++] = root;\n    // Инициализировать список для хранения последовательности обхода\n    /* Вспомогательный массив */\n    arr = (int *)malloc(sizeof(int) * MAX_SIZE);\n    // Указатель на массив\n    index = 0;\n    while (front < rear) {\n        // Извлечение из очереди\n        node = queue[front++];\n        // Сохранить значение узла\n        arr[index++] = node->val;\n        if (node->left != NULL) {\n            // Поместить левый дочерний узел в очередь\n            queue[rear++] = node->left;\n        }\n        if (node->right != NULL) {\n            // Поместить правый дочерний узел в очередь\n            queue[rear++] = node->right;\n        }\n    }\n    // Обновить значение длины массива\n    *size = index;\n    arr = realloc(arr, sizeof(int) * (*size));\n\n    // Освободить память вспомогательного массива\n    free(queue);\n    return arr;\n}\n
        binary_tree_bfs.kt
        /* Обход в ширину */\nfun levelOrder(root: TreeNode?): MutableList<Int> {\n    // Инициализировать очередь и добавить корневой узел\n    val queue = LinkedList<TreeNode?>()\n    queue.add(root)\n    // Инициализировать список для хранения последовательности обхода\n    val list = mutableListOf<Int>()\n    while (queue.isNotEmpty()) {\n        val node = queue.poll()      // Извлечение из очереди\n        list.add(node?._val!!)       // Сохранить значение узла\n        if (node.left != null)\n            queue.offer(node.left)   // Поместить левый дочерний узел в очередь\n        if (node.right != null)\n            queue.offer(node.right)  // Поместить правый дочерний узел в очередь\n    }\n    return list\n}\n
        binary_tree_bfs.rb
        ### Обход в ширину ###\ndef level_order(root)\n  # Инициализировать очередь и добавить корневой узел\n  queue = [root]\n  # Инициализировать список для хранения последовательности обхода\n  res = []\n  while !queue.empty?\n    node = queue.shift # Извлечение из очереди\n    res << node.val # Сохранить значение узла\n    queue << node.left unless node.left.nil? # Поместить левый дочерний узел в очередь\n    queue << node.right unless node.right.nil? # Поместить правый дочерний узел в очередь\n  end\n  res\nend\n
        Визуализация кода

        Во весь экран >

        ","path":["Глава 7. Деревья","7.2   Обход двоичного дерева"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#2","level":3,"title":"2.   Анализ сложности","text":"
        • Временная сложность равна \\(O(n)\\) : все узлы посещаются по одному разу, поэтому требуется \\(O(n)\\) времени, где \\(n\\) - число узлов.
        • Пространственная сложность равна \\(O(n)\\) : в худшем случае, то есть для полной двоичной деревообразной структуры, до достижения самого нижнего уровня в очереди одновременно может находиться до \\((n + 1) / 2\\) узлов, что требует \\(O(n)\\) памяти.
        ","path":["Глава 7. Деревья","7.2   Обход двоичного дерева"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#722","level":2,"title":"7.2.2   Прямой, симметричный и обратный обходы","text":"

        Соответственно, прямой, симметричный и обратный обходы относятся к обходу в глубину (depth-first traversal), также называемому поиском в глубину (depth-first search, DFS). Он отражает идею «сначала идти до конца, затем возвращаться и продолжать».

        На рисунке 7-10 показан принцип работы обхода двоичного дерева в глубину. Обход в глубину можно представить как обход всей двоичной структуры по внешнему контуру , и у каждого узла встречаются три позиции, соответствующие прямому, симметричному и обратному обходам.

        Рисунок 7-10   Прямой, симметричный и обратный обходы двоичного дерева поиска

        ","path":["Глава 7. Деревья","7.2   Обход двоичного дерева"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#1_1","level":3,"title":"1.   Код реализации","text":"

        Поиск в глубину обычно реализуется через рекурсию:

        PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree_dfs.py
        def pre_order(root: TreeNode | None):\n    \"\"\"Предварительный обход\"\"\"\n    if root is None:\n        return\n    # Порядок обхода: корень -> левое поддерево -> правое поддерево\n    res.append(root.val)\n    pre_order(root=root.left)\n    pre_order(root=root.right)\n\ndef in_order(root: TreeNode | None):\n    \"\"\"Симметричный обход\"\"\"\n    if root is None:\n        return\n    # Порядок обхода: левое поддерево -> корень -> правое поддерево\n    in_order(root=root.left)\n    res.append(root.val)\n    in_order(root=root.right)\n\ndef post_order(root: TreeNode | None):\n    \"\"\"Обратный обход\"\"\"\n    if root is None:\n        return\n    # Порядок обхода: левое поддерево -> правое поддерево -> корень\n    post_order(root=root.left)\n    post_order(root=root.right)\n    res.append(root.val)\n
        binary_tree_dfs.cpp
        /* Предварительный обход */\nvoid preOrder(TreeNode *root) {\n    if (root == nullptr)\n        return;\n    // Порядок обхода: корень -> левое поддерево -> правое поддерево\n    vec.push_back(root->val);\n    preOrder(root->left);\n    preOrder(root->right);\n}\n\n/* Симметричный обход */\nvoid inOrder(TreeNode *root) {\n    if (root == nullptr)\n        return;\n    // Порядок обхода: левое поддерево -> корень -> правое поддерево\n    inOrder(root->left);\n    vec.push_back(root->val);\n    inOrder(root->right);\n}\n\n/* Обратный обход */\nvoid postOrder(TreeNode *root) {\n    if (root == nullptr)\n        return;\n    // Порядок обхода: левое поддерево -> правое поддерево -> корень\n    postOrder(root->left);\n    postOrder(root->right);\n    vec.push_back(root->val);\n}\n
        binary_tree_dfs.java
        /* Предварительный обход */\nvoid preOrder(TreeNode root) {\n    if (root == null)\n        return;\n    // Порядок обхода: корень -> левое поддерево -> правое поддерево\n    list.add(root.val);\n    preOrder(root.left);\n    preOrder(root.right);\n}\n\n/* Симметричный обход */\nvoid inOrder(TreeNode root) {\n    if (root == null)\n        return;\n    // Порядок обхода: левое поддерево -> корень -> правое поддерево\n    inOrder(root.left);\n    list.add(root.val);\n    inOrder(root.right);\n}\n\n/* Обратный обход */\nvoid postOrder(TreeNode root) {\n    if (root == null)\n        return;\n    // Порядок обхода: левое поддерево -> правое поддерево -> корень\n    postOrder(root.left);\n    postOrder(root.right);\n    list.add(root.val);\n}\n
        binary_tree_dfs.cs
        /* Предварительный обход */\nvoid PreOrder(TreeNode? root) {\n    if (root == null) return;\n    // Порядок обхода: корень -> левое поддерево -> правое поддерево\n    list.Add(root.val!.Value);\n    PreOrder(root.left);\n    PreOrder(root.right);\n}\n\n/* Симметричный обход */\nvoid InOrder(TreeNode? root) {\n    if (root == null) return;\n    // Порядок обхода: левое поддерево -> корень -> правое поддерево\n    InOrder(root.left);\n    list.Add(root.val!.Value);\n    InOrder(root.right);\n}\n\n/* Обратный обход */\nvoid PostOrder(TreeNode? root) {\n    if (root == null) return;\n    // Порядок обхода: левое поддерево -> правое поддерево -> корень\n    PostOrder(root.left);\n    PostOrder(root.right);\n    list.Add(root.val!.Value);\n}\n
        binary_tree_dfs.go
        /* Предварительный обход */\nfunc preOrder(node *TreeNode) {\n    if node == nil {\n        return\n    }\n    // Порядок обхода: корень -> левое поддерево -> правое поддерево\n    nums = append(nums, node.Val)\n    preOrder(node.Left)\n    preOrder(node.Right)\n}\n\n/* Симметричный обход */\nfunc inOrder(node *TreeNode) {\n    if node == nil {\n        return\n    }\n    // Порядок обхода: левое поддерево -> корень -> правое поддерево\n    inOrder(node.Left)\n    nums = append(nums, node.Val)\n    inOrder(node.Right)\n}\n\n/* Обратный обход */\nfunc postOrder(node *TreeNode) {\n    if node == nil {\n        return\n    }\n    // Порядок обхода: левое поддерево -> правое поддерево -> корень\n    postOrder(node.Left)\n    postOrder(node.Right)\n    nums = append(nums, node.Val)\n}\n
        binary_tree_dfs.swift
        /* Предварительный обход */\nfunc preOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    // Порядок обхода: корень -> левое поддерево -> правое поддерево\n    list.append(root.val)\n    preOrder(root: root.left)\n    preOrder(root: root.right)\n}\n\n/* Симметричный обход */\nfunc inOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    // Порядок обхода: левое поддерево -> корень -> правое поддерево\n    inOrder(root: root.left)\n    list.append(root.val)\n    inOrder(root: root.right)\n}\n\n/* Обратный обход */\nfunc postOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    // Порядок обхода: левое поддерево -> правое поддерево -> корень\n    postOrder(root: root.left)\n    postOrder(root: root.right)\n    list.append(root.val)\n}\n
        binary_tree_dfs.js
        /* Предварительный обход */\nfunction preOrder(root) {\n    if (root === null) return;\n    // Порядок обхода: корень -> левое поддерево -> правое поддерево\n    list.push(root.val);\n    preOrder(root.left);\n    preOrder(root.right);\n}\n\n/* Симметричный обход */\nfunction inOrder(root) {\n    if (root === null) return;\n    // Порядок обхода: левое поддерево -> корень -> правое поддерево\n    inOrder(root.left);\n    list.push(root.val);\n    inOrder(root.right);\n}\n\n/* Обратный обход */\nfunction postOrder(root) {\n    if (root === null) return;\n    // Порядок обхода: левое поддерево -> правое поддерево -> корень\n    postOrder(root.left);\n    postOrder(root.right);\n    list.push(root.val);\n}\n
        binary_tree_dfs.ts
        /* Предварительный обход */\nfunction preOrder(root: TreeNode | null): void {\n    if (root === null) {\n        return;\n    }\n    // Порядок обхода: корень -> левое поддерево -> правое поддерево\n    list.push(root.val);\n    preOrder(root.left);\n    preOrder(root.right);\n}\n\n/* Симметричный обход */\nfunction inOrder(root: TreeNode | null): void {\n    if (root === null) {\n        return;\n    }\n    // Порядок обхода: левое поддерево -> корень -> правое поддерево\n    inOrder(root.left);\n    list.push(root.val);\n    inOrder(root.right);\n}\n\n/* Обратный обход */\nfunction postOrder(root: TreeNode | null): void {\n    if (root === null) {\n        return;\n    }\n    // Порядок обхода: левое поддерево -> правое поддерево -> корень\n    postOrder(root.left);\n    postOrder(root.right);\n    list.push(root.val);\n}\n
        binary_tree_dfs.dart
        /* Предварительный обход */\nvoid preOrder(TreeNode? node) {\n  if (node == null) return;\n  // Порядок обхода: корень -> левое поддерево -> правое поддерево\n  list.add(node.val);\n  preOrder(node.left);\n  preOrder(node.right);\n}\n\n/* Симметричный обход */\nvoid inOrder(TreeNode? node) {\n  if (node == null) return;\n  // Порядок обхода: левое поддерево -> корень -> правое поддерево\n  inOrder(node.left);\n  list.add(node.val);\n  inOrder(node.right);\n}\n\n/* Обратный обход */\nvoid postOrder(TreeNode? node) {\n  if (node == null) return;\n  // Порядок обхода: левое поддерево -> правое поддерево -> корень\n  postOrder(node.left);\n  postOrder(node.right);\n  list.add(node.val);\n}\n
        binary_tree_dfs.rs
        /* Предварительный обход */\nfn pre_order(root: Option<&Rc<RefCell<TreeNode>>>) -> Vec<i32> {\n    let mut result = vec![];\n\n    fn dfs(root: Option<&Rc<RefCell<TreeNode>>>, res: &mut Vec<i32>) {\n        if let Some(node) = root {\n            // Порядок обхода: корень -> левое поддерево -> правое поддерево\n            let node = node.borrow();\n            res.push(node.val);\n            dfs(node.left.as_ref(), res);\n            dfs(node.right.as_ref(), res);\n        }\n    }\n    dfs(root, &mut result);\n\n    result\n}\n\n/* Симметричный обход */\nfn in_order(root: Option<&Rc<RefCell<TreeNode>>>) -> Vec<i32> {\n    let mut result = vec![];\n\n    fn dfs(root: Option<&Rc<RefCell<TreeNode>>>, res: &mut Vec<i32>) {\n        if let Some(node) = root {\n            // Порядок обхода: левое поддерево -> корень -> правое поддерево\n            let node = node.borrow();\n            dfs(node.left.as_ref(), res);\n            res.push(node.val);\n            dfs(node.right.as_ref(), res);\n        }\n    }\n    dfs(root, &mut result);\n\n    result\n}\n\n/* Обратный обход */\nfn post_order(root: Option<&Rc<RefCell<TreeNode>>>) -> Vec<i32> {\n    let mut result = vec![];\n\n    fn dfs(root: Option<&Rc<RefCell<TreeNode>>>, res: &mut Vec<i32>) {\n        if let Some(node) = root {\n            // Порядок обхода: левое поддерево -> правое поддерево -> корень\n            let node = node.borrow();\n            dfs(node.left.as_ref(), res);\n            dfs(node.right.as_ref(), res);\n            res.push(node.val);\n        }\n    }\n\n    dfs(root, &mut result);\n\n    result\n}\n
        binary_tree_dfs.c
        /* Предварительный обход */\nvoid preOrder(TreeNode *root, int *size) {\n    if (root == NULL)\n        return;\n    // Порядок обхода: корень -> левое поддерево -> правое поддерево\n    arr[(*size)++] = root->val;\n    preOrder(root->left, size);\n    preOrder(root->right, size);\n}\n\n/* Симметричный обход */\nvoid inOrder(TreeNode *root, int *size) {\n    if (root == NULL)\n        return;\n    // Порядок обхода: левое поддерево -> корень -> правое поддерево\n    inOrder(root->left, size);\n    arr[(*size)++] = root->val;\n    inOrder(root->right, size);\n}\n\n/* Обратный обход */\nvoid postOrder(TreeNode *root, int *size) {\n    if (root == NULL)\n        return;\n    // Порядок обхода: левое поддерево -> правое поддерево -> корень\n    postOrder(root->left, size);\n    postOrder(root->right, size);\n    arr[(*size)++] = root->val;\n}\n
        binary_tree_dfs.kt
        /* Предварительный обход */\nfun preOrder(root: TreeNode?) {\n    if (root == null) return\n    // Порядок обхода: корень -> левое поддерево -> правое поддерево\n    list.add(root._val)\n    preOrder(root.left)\n    preOrder(root.right)\n}\n\n/* Симметричный обход */\nfun inOrder(root: TreeNode?) {\n    if (root == null) return\n    // Порядок обхода: левое поддерево -> корень -> правое поддерево\n    inOrder(root.left)\n    list.add(root._val)\n    inOrder(root.right)\n}\n\n/* Обратный обход */\nfun postOrder(root: TreeNode?) {\n    if (root == null) return\n    // Порядок обхода: левое поддерево -> правое поддерево -> корень\n    postOrder(root.left)\n    postOrder(root.right)\n    list.add(root._val)\n}\n
        binary_tree_dfs.rb
        ### Предварительный обход ###\ndef pre_order(root)\n  return if root.nil?\n\n  # Порядок обхода: корень -> левое поддерево -> правое поддерево\n  $res << root.val\n  pre_order(root.left)\n  pre_order(root.right)\nend\n\n### Симметричный обход ###\ndef in_order(root)\n  return if root.nil?\n\n  # Порядок обхода: левое поддерево -> корень -> правое поддерево\n  in_order(root.left)\n  $res << root.val\n  in_order(root.right)\nend\n\n### Обратный обход ###\ndef post_order(root)\n  return if root.nil?\n\n  # Порядок обхода: левое поддерево -> правое поддерево -> корень\n  post_order(root.left)\n  post_order(root.right)\n  $res << root.val\nend\n
        Визуализация кода

        Во весь экран >

        Tip

        Поиск в глубину можно реализовать и итеративно. Заинтересованные читатели могут изучить это самостоятельно.

        На рисунке 7-11 показан рекурсивный процесс прямого обхода двоичного дерева. Его можно разделить на две противоположные части: «вход в рекурсию» и «возврат».

        1. «Вход в рекурсию» означает запуск нового вызова функции. В этом процессе программа переходит к следующему узлу.
        2. «Возврат» означает завершение вызова функции и возврат назад, то есть текущий узел уже полностью обработан.
        <1><2><3><4><5><6><7><8><9><10><11>

        Рисунок 7-11   Рекурсивный процесс прямого обхода

        ","path":["Глава 7. Деревья","7.2   Обход двоичного дерева"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#2_1","level":3,"title":"2.   Анализ сложности","text":"
        • Временная сложность равна \\(O(n)\\) : все узлы посещаются по одному разу, поэтому требуется \\(O(n)\\) времени.
        • Пространственная сложность равна \\(O(n)\\) : в худшем случае, когда дерево вырождается в связный список, глубина рекурсии достигает \\(n\\) , и система тратит \\(O(n)\\) памяти на стек вызовов.
        ","path":["Глава 7. Деревья","7.2   Обход двоичного дерева"],"tags":[]},{"location":"chapter_tree/summary/","level":1,"title":"7.6   Краткие итоги","text":"","path":["Глава 7. Деревья","7.6   Краткие итоги"],"tags":[]},{"location":"chapter_tree/summary/#1","level":3,"title":"1.   Основные моменты","text":"
        • Двоичное дерево - это нелинейная структура данных, отражающая логику «разделяй и властвуй». Каждый узел двоичного дерева содержит значение и два указателя, которые соответственно ведут к левому и правому дочерним узлам.
        • Для любого узла двоичного дерева дерево, образованное его левым (правым) дочерним узлом и всеми нижележащими узлами, называется левым (правым) поддеревом этого узла.
        • К связанным с двоичным деревом терминам относятся корневой узел, листовой узел, уровень, степень, ребро, высота, глубина и так далее.
        • Инициализация двоичного дерева, вставка узлов и удаление узлов аналогичны операциям со связным списком.
        • К распространенным видам двоичного дерева относятся идеальное двоичное дерево, полное двоичное дерево, строгое двоичное дерево и сбалансированное двоичное дерево. Идеальное двоичное дерево - наиболее желательное состояние, а связный список - худший случай после вырождения.
        • Двоичное дерево можно представить массивом: значения узлов и пустые позиции располагаются в порядке обхода по уровням, а связи между родителем и детьми реализуются через индексацию.
        • Обход двоичного дерева по уровням является методом поиска в ширину. Он отражает идею «расширяться от центра к периферии слой за слоем» и обычно реализуется через очередь.
        • Прямой, симметричный и обратный обходы относятся к поиску в глубину. Они отражают идею «сначала дойти до конца, затем вернуться и продолжить» и обычно реализуются рекурсивно.
        • Двоичное дерево поиска - это эффективная структура данных для поиска элементов. Его поиск, вставка и удаление имеют временную сложность \\(O(\\log n)\\) . Когда двоичное дерево поиска вырождается в связный список, все эти сложности деградируют до \\(O(n)\\) .
        • AVL-дерево, также называемое сбалансированным двоичным деревом поиска, с помощью вращений гарантирует, что после постоянных вставок и удалений узлов дерево остается сбалансированным.
        • Вращения AVL-дерева включают правое вращение, левое вращение, сначала правое затем левое и сначала левое затем правое. После вставки или удаления узла AVL-дерево выполняет вращения снизу вверх, чтобы снова восстановить баланс.
        ","path":["Глава 7. Деревья","7.6   Краткие итоги"],"tags":[]},{"location":"chapter_tree/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

        Q: Для двоичного дерева, состоящего из одного узла, высота дерева и глубина корня обе равны \\(0\\) ?

        Да, потому что высота и глубина обычно определяются как «число пройденных ребер».

        Q: Вставка и удаление в двоичном дереве обычно выполняются в составе набора операций. Что именно означает этот «набор операций»? Можно ли понимать это как освобождение ресурсов у дочерних узлов ресурса?

        Возьмем в качестве примера двоичное дерево поиска: операция удаления узла делится на три случая, и каждый из этих случаев требует нескольких последовательных шагов работы с узлами.

        Q: Почему у DFS для двоичного дерева есть три порядка: прямой, симметричный и обратный? Для чего они нужны?

        Подобно прямому и обратному обходу массива, прямой, симметричный и обратный обходы - это три способа обхода двоичного дерева, с помощью которых можно получить результаты в определенном порядке. Например, в двоичном дереве поиска, где соблюдается отношение значение левого дочернего узла < значение корня < значение правого дочернего узла , если обходить дерево с приоритетом «лево \\(\\rightarrow\\) корень \\(\\rightarrow\\) право», то получится упорядоченная последовательность узлов.

        Q: Правое вращение работает с отношениями между node , child и grand_child . А связь между node и его исходным родителем разве не нужно поддерживать? После правого вращения она ведь не оборвется?

        На это нужно смотреть с точки зрения рекурсии. В правое вращение right_rotate(root) передается корень поддерева, а затем через return child возвращается корень этого поддерева уже после вращения. Соединение между новым корнем поддерева и его родителем восстанавливается после возврата функции и не входит в обязанности самой операции правого вращения.

        Q: В C++ функции делятся на private и public . Какая логика стоит за этим? Почему height() и updateHeight() помещают в разные области видимости?

        Главный критерий - область использования метода. Если метод нужен только внутри класса, его следует проектировать как private . Например, самостоятельный вызов updateHeight() пользователем не имеет смысла: это лишь один из шагов внутри вставки или удаления. А height() используется для чтения высоты узла, подобно vector.size() , поэтому его разумно делать public .

        Q: Как построить двоичное дерево поиска из набора входных данных? Важен ли выбор корневого узла?

        Да, важен. Способ построения дерева уже показан в методе build_tree() в коде двоичного дерева поиска. Что касается выбора корня, обычно входные данные сортируют, берут средний элемент как корень, а затем рекурсивно строят левое и правое поддеревья. Это позволяет в наибольшей степени сохранить баланс дерева.

        Q: Нужно ли в Java всегда использовать equals() для сравнения строк?

        В Java для базовых типов == используется, чтобы сравнивать, равны ли значения двух переменных. Для ссылочных типов логика у этих двух способов уже разная.

        • == : сравнивает, ссылаются ли две переменные на один и тот же объект, то есть совпадает ли их адрес в памяти.
        • equals(): сравнивает, равны ли значения двух объектов.

        Поэтому если нужно сравнить значения, то следует использовать equals() . Но строки, инициализированные как String a = \"hi\"; String b = \"hi\"; , хранятся в строковом пуле констант и указывают на один и тот же объект, поэтому в таком случае a == b тоже может дать истинный результат при сравнении содержимого.

        Q: До достижения самого нижнего уровня при обходе в ширину число узлов в очереди равно \\(2^h\\) ?

        Да. Например, для полного двоичного дерева высоты \\(h = 2\\) общее число узлов равно \\(n = 7\\) , а число узлов на нижнем уровне равно \\(4 = 2^h = (n + 1) / 2\\) .

        ","path":["Глава 7. Деревья","7.6   Краткие итоги"],"tags":[]}]} \ No newline at end of file diff --git a/ru/stylesheets/animation_player.css b/ru/stylesheets/animation_player.css index 1ab382b2e..98c1221b1 100644 --- a/ru/stylesheets/animation_player.css +++ b/ru/stylesheets/animation_player.css @@ -176,4 +176,4 @@ font-size: 0.7rem; } } -/*! update cache: 20260410225948 */ +/*! update cache: 20260414173637 */ diff --git a/ru/stylesheets/extra.css b/ru/stylesheets/extra.css index b4b93e761..5d1cfbbf9 100644 --- a/ru/stylesheets/extra.css +++ b/ru/stylesheets/extra.css @@ -806,4 +806,4 @@ a:hover .device-on-hover { margin: 0 0 1em; } } -/*! update cache: 20260410225948 */ +/*! update cache: 20260414173637 */ diff --git a/ru/stylesheets/giscus-dark.css b/ru/stylesheets/giscus-dark.css index d333035fb..95ed52d8a 100644 --- a/ru/stylesheets/giscus-dark.css +++ b/ru/stylesheets/giscus-dark.css @@ -122,4 +122,4 @@ main .gsc-loading-image { .gsc-reply-content::-webkit-scrollbar-track { background: transparent; } -/*! update cache: 20260410225948 */ +/*! update cache: 20260414173637 */ diff --git a/ru/stylesheets/giscus-light.css b/ru/stylesheets/giscus-light.css index fe22bbf94..e7ae5f184 100644 --- a/ru/stylesheets/giscus-light.css +++ b/ru/stylesheets/giscus-light.css @@ -153,4 +153,4 @@ main { .gsc-reply-content::-webkit-scrollbar-track { background: transparent; } -/*! update cache: 20260410225948 */ +/*! update cache: 20260414173637 */ diff --git a/stylesheets/animation_player.css b/stylesheets/animation_player.css index 8580e464c..32da9fb77 100644 --- a/stylesheets/animation_player.css +++ b/stylesheets/animation_player.css @@ -176,4 +176,4 @@ font-size: 0.7rem; } } -/*! update cache: 20260410225905 */ +/*! update cache: 20260414173552 */ diff --git a/stylesheets/extra.css b/stylesheets/extra.css index bf48d75a4..349180f79 100644 --- a/stylesheets/extra.css +++ b/stylesheets/extra.css @@ -806,4 +806,4 @@ a:hover .device-on-hover { margin: 0 0 1em; } } -/*! update cache: 20260410225905 */ +/*! update cache: 20260414173552 */ diff --git a/stylesheets/giscus-dark.css b/stylesheets/giscus-dark.css index 464f51d23..a289132b9 100644 --- a/stylesheets/giscus-dark.css +++ b/stylesheets/giscus-dark.css @@ -122,4 +122,4 @@ main .gsc-loading-image { .gsc-reply-content::-webkit-scrollbar-track { background: transparent; } -/*! update cache: 20260410225905 */ +/*! update cache: 20260414173552 */ diff --git a/stylesheets/giscus-light.css b/stylesheets/giscus-light.css index 3c3d77fcd..b2a664490 100644 --- a/stylesheets/giscus-light.css +++ b/stylesheets/giscus-light.css @@ -153,4 +153,4 @@ main { .gsc-reply-content::-webkit-scrollbar-track { background: transparent; } -/*! update cache: 20260410225905 */ +/*! update cache: 20260414173552 */ diff --git a/zh-hant/assets/javascripts/bundle.c2b142ea.min.js b/zh-hant/assets/javascripts/bundle.c2b142ea.min.js index 8f8723f10..889f3f2ea 100644 --- a/zh-hant/assets/javascripts/bundle.c2b142ea.min.js +++ b/zh-hant/assets/javascripts/bundle.c2b142ea.min.js @@ -1,4 +1,4 @@ "use strict";(()=>{var xc=Object.create;var kn=Object.defineProperty,wc=Object.defineProperties,Ec=Object.getOwnPropertyDescriptor,Tc=Object.getOwnPropertyDescriptors,Sc=Object.getOwnPropertyNames,Dr=Object.getOwnPropertySymbols,Oc=Object.getPrototypeOf,An=Object.prototype.hasOwnProperty,Fo=Object.prototype.propertyIsEnumerable;var jo=(e,t,r)=>t in e?kn(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,H=(e,t)=>{for(var r in t||(t={}))An.call(t,r)&&jo(e,r,t[r]);if(Dr)for(var r of Dr(t))Fo.call(t,r)&&jo(e,r,t[r]);return e},He=(e,t)=>wc(e,Tc(t));var gr=(e,t)=>{var r={};for(var n in e)An.call(e,n)&&t.indexOf(n)<0&&(r[n]=e[n]);if(e!=null&&Dr)for(var n of Dr(e))t.indexOf(n)<0&&Fo.call(e,n)&&(r[n]=e[n]);return r};var Cn=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var Lc=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of Sc(t))!An.call(e,o)&&o!==r&&kn(e,o,{get:()=>t[o],enumerable:!(n=Ec(t,o))||n.enumerable});return e};var _r=(e,t,r)=>(r=e!=null?xc(Oc(e)):{},Lc(t||!e||!e.__esModule?kn(r,"default",{value:e,enumerable:!0}):r,e));var Uo=(e,t,r)=>new Promise((n,o)=>{var i=c=>{try{s(r.next(c))}catch(l){o(l)}},a=c=>{try{s(r.throw(c))}catch(l){o(l)}},s=c=>c.done?n(c.value):Promise.resolve(c.value).then(i,a);s((r=r.apply(e,t)).next())});var Do=Cn((Hn,No)=>{(function(e,t){typeof Hn=="object"&&typeof No!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(Hn,(function(){"use strict";function e(r){var n=!0,o=!1,i=null,a={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function s(_){return!!(_&&_!==document&&_.nodeName!=="HTML"&&_.nodeName!=="BODY"&&"classList"in _&&"contains"in _.classList)}function c(_){var de=_.type,be=_.tagName;return!!(be==="INPUT"&&a[de]&&!_.readOnly||be==="TEXTAREA"&&!_.readOnly||_.isContentEditable)}function l(_){_.classList.contains("focus-visible")||(_.classList.add("focus-visible"),_.setAttribute("data-focus-visible-added",""))}function u(_){_.hasAttribute("data-focus-visible-added")&&(_.classList.remove("focus-visible"),_.removeAttribute("data-focus-visible-added"))}function p(_){_.metaKey||_.altKey||_.ctrlKey||(s(r.activeElement)&&l(r.activeElement),n=!0)}function d(_){n=!1}function m(_){s(_.target)&&(n||c(_.target))&&l(_.target)}function h(_){s(_.target)&&(_.target.classList.contains("focus-visible")||_.target.hasAttribute("data-focus-visible-added"))&&(o=!0,window.clearTimeout(i),i=window.setTimeout(function(){o=!1},100),u(_.target))}function v(_){document.visibilityState==="hidden"&&(o&&(n=!0),x())}function x(){document.addEventListener("mousemove",E),document.addEventListener("mousedown",E),document.addEventListener("mouseup",E),document.addEventListener("pointermove",E),document.addEventListener("pointerdown",E),document.addEventListener("pointerup",E),document.addEventListener("touchmove",E),document.addEventListener("touchstart",E),document.addEventListener("touchend",E)}function w(){document.removeEventListener("mousemove",E),document.removeEventListener("mousedown",E),document.removeEventListener("mouseup",E),document.removeEventListener("pointermove",E),document.removeEventListener("pointerdown",E),document.removeEventListener("pointerup",E),document.removeEventListener("touchmove",E),document.removeEventListener("touchstart",E),document.removeEventListener("touchend",E)}function E(_){_.target.nodeName&&_.target.nodeName.toLowerCase()==="html"||(n=!1,w())}document.addEventListener("keydown",p,!0),document.addEventListener("mousedown",d,!0),document.addEventListener("pointerdown",d,!0),document.addEventListener("touchstart",d,!0),document.addEventListener("visibilitychange",v,!0),x(),r.addEventListener("focus",m,!0),r.addEventListener("blur",h,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)}))});var So=Cn((M0,vs)=>{"use strict";var Gu=/["'&<>]/;vs.exports=Ju;function Ju(e){var t=""+e,r=Gu.exec(t);if(!r)return t;var n,o="",i=0,a=0;for(i=r.index;i{(function(t,r){typeof jr=="object"&&typeof Lo=="object"?Lo.exports=r():typeof define=="function"&&define.amd?define([],r):typeof jr=="object"?jr.ClipboardJS=r():t.ClipboardJS=r()})(jr,function(){return(function(){var e={686:(function(n,o,i){"use strict";i.d(o,{default:function(){return vr}});var a=i(279),s=i.n(a),c=i(370),l=i.n(c),u=i(817),p=i.n(u);function d(B){try{return document.execCommand(B)}catch(C){return!1}}var m=function(C){var k=p()(C);return d("cut"),k},h=m;function v(B){var C=document.documentElement.getAttribute("dir")==="rtl",k=document.createElement("textarea");k.style.fontSize="12pt",k.style.border="0",k.style.padding="0",k.style.margin="0",k.style.position="absolute",k.style[C?"right":"left"]="-9999px";var D=window.pageYOffset||document.documentElement.scrollTop;return k.style.top="".concat(D,"px"),k.setAttribute("readonly",""),k.value=B,k}var x=function(C,k){var D=v(C);k.container.appendChild(D);var W=p()(D);return d("copy"),D.remove(),W},w=function(C){var k=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},D="";return typeof C=="string"?D=x(C,k):C instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(C==null?void 0:C.type)?D=x(C.value,k):(D=p()(C),d("copy")),D},E=w;function _(B){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?_=function(k){return typeof k}:_=function(k){return k&&typeof Symbol=="function"&&k.constructor===Symbol&&k!==Symbol.prototype?"symbol":typeof k},_(B)}var de=function(){var C=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},k=C.action,D=k===void 0?"copy":k,W=C.container,Z=C.target,We=C.text;if(D!=="copy"&&D!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(Z!==void 0)if(Z&&_(Z)==="object"&&Z.nodeType===1){if(D==="copy"&&Z.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(D==="cut"&&(Z.hasAttribute("readonly")||Z.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if(We)return E(We,{container:W});if(Z)return D==="cut"?h(Z):E(Z,{container:W})},be=de;function M(B){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?M=function(k){return typeof k}:M=function(k){return k&&typeof Symbol=="function"&&k.constructor===Symbol&&k!==Symbol.prototype?"symbol":typeof k},M(B)}function O(B,C){if(!(B instanceof C))throw new TypeError("Cannot call a class as a function")}function N(B,C){for(var k=0;k0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof W.action=="function"?W.action:this.defaultAction,this.target=typeof W.target=="function"?W.target:this.defaultTarget,this.text=typeof W.text=="function"?W.text:this.defaultText,this.container=M(W.container)==="object"?W.container:document.body}},{key:"listenClick",value:function(W){var Z=this;this.listener=l()(W,"click",function(We){return Z.onClick(We)})}},{key:"onClick",value:function(W){var Z=W.delegateTarget||W.currentTarget,We=this.action(Z)||"copy",Gt=be({action:We,container:this.container,target:this.target(Z),text:this.text(Z)});this.emit(Gt?"success":"error",{action:We,text:Gt,trigger:Z,clearSelection:function(){Z&&Z.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(W){return Yt("action",W)}},{key:"defaultTarget",value:function(W){var Z=Yt("target",W);if(Z)return document.querySelector(Z)}},{key:"defaultText",value:function(W){return Yt("text",W)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(W){var Z=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return E(W,Z)}},{key:"cut",value:function(W){return h(W)}},{key:"isSupported",value:function(){var W=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],Z=typeof W=="string"?[W]:W,We=!!document.queryCommandSupported;return Z.forEach(function(Gt){We=We&&!!document.queryCommandSupported(Gt)}),We}}]),k})(s()),vr=Mt}),828:(function(n){var o=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function a(s,c){for(;s&&s.nodeType!==o;){if(typeof s.matches=="function"&&s.matches(c))return s;s=s.parentNode}}n.exports=a}),438:(function(n,o,i){var a=i(828);function s(u,p,d,m,h){var v=l.apply(this,arguments);return u.addEventListener(d,v,h),{destroy:function(){u.removeEventListener(d,v,h)}}}function c(u,p,d,m,h){return typeof u.addEventListener=="function"?s.apply(null,arguments):typeof d=="function"?s.bind(null,document).apply(null,arguments):(typeof u=="string"&&(u=document.querySelectorAll(u)),Array.prototype.map.call(u,function(v){return s(v,p,d,m,h)}))}function l(u,p,d,m){return function(h){h.delegateTarget=a(h.target,p),h.delegateTarget&&m.call(u,h)}}n.exports=c}),879:(function(n,o){o.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},o.nodeList=function(i){var a=Object.prototype.toString.call(i);return i!==void 0&&(a==="[object NodeList]"||a==="[object HTMLCollection]")&&"length"in i&&(i.length===0||o.node(i[0]))},o.string=function(i){return typeof i=="string"||i instanceof String},o.fn=function(i){var a=Object.prototype.toString.call(i);return a==="[object Function]"}}),370:(function(n,o,i){var a=i(879),s=i(438);function c(d,m,h){if(!d&&!m&&!h)throw new Error("Missing required arguments");if(!a.string(m))throw new TypeError("Second argument must be a String");if(!a.fn(h))throw new TypeError("Third argument must be a Function");if(a.node(d))return l(d,m,h);if(a.nodeList(d))return u(d,m,h);if(a.string(d))return p(d,m,h);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function l(d,m,h){return d.addEventListener(m,h),{destroy:function(){d.removeEventListener(m,h)}}}function u(d,m,h){return Array.prototype.forEach.call(d,function(v){v.addEventListener(m,h)}),{destroy:function(){Array.prototype.forEach.call(d,function(v){v.removeEventListener(m,h)})}}}function p(d,m,h){return s(document.body,d,m,h)}n.exports=c}),817:(function(n){function o(i){var a;if(i.nodeName==="SELECT")i.focus(),a=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var s=i.hasAttribute("readonly");s||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),s||i.removeAttribute("readonly"),a=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var c=window.getSelection(),l=document.createRange();l.selectNodeContents(i),c.removeAllRanges(),c.addRange(l),a=c.toString()}return a}n.exports=o}),279:(function(n){function o(){}o.prototype={on:function(i,a,s){var c=this.e||(this.e={});return(c[i]||(c[i]=[])).push({fn:a,ctx:s}),this},once:function(i,a,s){var c=this;function l(){c.off(i,l),a.apply(s,arguments)}return l._=a,this.on(i,l,s)},emit:function(i){var a=[].slice.call(arguments,1),s=((this.e||(this.e={}))[i]||[]).slice(),c=0,l=s.length;for(c;c0&&i[i.length-1])&&(l[0]===6||l[0]===2)){r=0;continue}if(l[0]===3&&(!i||l[1]>i[0]&&l[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function te(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var n=r.call(e),o,i=[],a;try{for(;(t===void 0||t-- >0)&&!(o=n.next()).done;)i.push(o.value)}catch(s){a={error:s}}finally{try{o&&!o.done&&(r=n.return)&&r.call(n)}finally{if(a)throw a.error}}return i}function ne(e,t,r){if(r||arguments.length===2)for(var n=0,o=t.length,i;n1||c(m,v)})},h&&(o[m]=h(o[m])))}function c(m,h){try{l(n[m](h))}catch(v){d(i[0][3],v)}}function l(m){m.value instanceof kt?Promise.resolve(m.value.v).then(u,p):d(i[0][2],m)}function u(m){c("next",m)}function p(m){c("throw",m)}function d(m,h){m(h),i.shift(),i.length&&c(i[0][0],i[0][1])}}function zo(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],r;return t?t.call(e):(e=typeof $e=="function"?$e(e):e[Symbol.iterator](),r={},n("next"),n("throw"),n("return"),r[Symbol.asyncIterator]=function(){return this},r);function n(i){r[i]=e[i]&&function(a){return new Promise(function(s,c){a=e[i](a),o(s,c,a.done,a.value)})}}function o(i,a,s,c){Promise.resolve(c).then(function(l){i({value:l,done:s})},a)}}function F(e){return typeof e=="function"}function Jt(e){var t=function(n){Error.call(n),n.stack=new Error().stack},r=e(t);return r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r}var Vr=Jt(function(e){return function(r){e(this),this.message=r?r.length+` errors occurred during unsubscription: `+r.map(function(n,o){return o+1+") "+n.toString()}).join(` `):"",this.name="UnsubscriptionError",this.errors=r}});function ct(e,t){if(e){var r=e.indexOf(t);0<=r&&e.splice(r,1)}}var rt=(function(){function e(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var t,r,n,o,i;if(!this.closed){this.closed=!0;var a=this._parentage;if(a)if(this._parentage=null,Array.isArray(a))try{for(var s=$e(a),c=s.next();!c.done;c=s.next()){var l=c.value;l.remove(this)}}catch(v){t={error:v}}finally{try{c&&!c.done&&(r=s.return)&&r.call(s)}finally{if(t)throw t.error}}else a.remove(this);var u=this.initialTeardown;if(F(u))try{u()}catch(v){i=v instanceof Vr?v.errors:[v]}var p=this._finalizers;if(p){this._finalizers=null;try{for(var d=$e(p),m=d.next();!m.done;m=d.next()){var h=m.value;try{qo(h)}catch(v){i=i!=null?i:[],v instanceof Vr?i=ne(ne([],te(i)),te(v.errors)):i.push(v)}}}catch(v){n={error:v}}finally{try{m&&!m.done&&(o=d.return)&&o.call(d)}finally{if(n)throw n.error}}}if(i)throw new Vr(i)}},e.prototype.add=function(t){var r;if(t&&t!==this)if(this.closed)qo(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(r=this._finalizers)!==null&&r!==void 0?r:[]).push(t)}},e.prototype._hasParent=function(t){var r=this._parentage;return r===t||Array.isArray(r)&&r.includes(t)},e.prototype._addParent=function(t){var r=this._parentage;this._parentage=Array.isArray(r)?(r.push(t),r):r?[r,t]:t},e.prototype._removeParent=function(t){var r=this._parentage;r===t?this._parentage=null:Array.isArray(r)&&ct(r,t)},e.prototype.remove=function(t){var r=this._finalizers;r&&ct(r,t),t instanceof e&&t._removeParent(this)},e.EMPTY=(function(){var t=new e;return t.closed=!0,t})(),e})();var Pn=rt.EMPTY;function zr(e){return e instanceof rt||e&&"closed"in e&&F(e.remove)&&F(e.add)&&F(e.unsubscribe)}function qo(e){F(e)?e():e.unsubscribe()}var Je={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var Xt={setTimeout:function(e,t){for(var r=[],n=2;n0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var n=this,o=this,i=o.hasError,a=o.isStopped,s=o.observers;return i||a?Pn:(this.currentObservers=null,s.push(r),new rt(function(){n.currentObservers=null,ct(s,r)}))},t.prototype._checkFinalizedStatuses=function(r){var n=this,o=n.hasError,i=n.thrownError,a=n.isStopped;o?r.error(i):a&&r.complete()},t.prototype.asObservable=function(){var r=new U;return r.source=this,r},t.create=function(r,n){return new Qo(r,n)},t})(U);var Qo=(function(e){ue(t,e);function t(r,n){var o=e.call(this)||this;return o.destination=r,o.source=n,o}return t.prototype.next=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.next)===null||o===void 0||o.call(n,r)},t.prototype.error=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.error)===null||o===void 0||o.call(n,r)},t.prototype.complete=function(){var r,n;(n=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||n===void 0||n.call(r)},t.prototype._subscribe=function(r){var n,o;return(o=(n=this.source)===null||n===void 0?void 0:n.subscribe(r))!==null&&o!==void 0?o:Pn},t})(I);var Un=(function(e){ue(t,e);function t(r){var n=e.call(this)||this;return n._value=r,n}return Object.defineProperty(t.prototype,"value",{get:function(){return this.getValue()},enumerable:!1,configurable:!0}),t.prototype._subscribe=function(r){var n=e.prototype._subscribe.call(this,r);return!n.closed&&r.next(this._value),n},t.prototype.getValue=function(){var r=this,n=r.hasError,o=r.thrownError,i=r._value;if(n)throw o;return this._throwIfClosed(),i},t.prototype.next=function(r){e.prototype.next.call(this,this._value=r)},t})(I);var xr={now:function(){return(xr.delegate||Date).now()},delegate:void 0};var wr=(function(e){ue(t,e);function t(r,n,o){r===void 0&&(r=1/0),n===void 0&&(n=1/0),o===void 0&&(o=xr);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=n,i._timestampProvider=o,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=n===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,n),i}return t.prototype.next=function(r){var n=this,o=n.isStopped,i=n._buffer,a=n._infiniteTimeWindow,s=n._timestampProvider,c=n._windowTime;o||(i.push(r),!a&&i.push(s.now()+c)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var n=this._innerSubscribe(r),o=this,i=o._infiniteTimeWindow,a=o._buffer,s=a.slice(),c=0;c0?e.prototype.schedule.call(this,r,n):(this.delay=n,this.state=r,this.scheduler.flush(this),this)},t.prototype.execute=function(r,n){return n>0||this.closed?e.prototype.execute.call(this,r,n):this._execute(r,n)},t.prototype.requestAsyncId=function(r,n,o){return o===void 0&&(o=0),o!=null&&o>0||o==null&&this.delay>0?e.prototype.requestAsyncId.call(this,r,n,o):(r.flush(this),0)},t})(tr);var ri=(function(e){ue(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t})(rr);var Wn=new ri(ti);var ni=(function(e){ue(t,e);function t(r,n){var o=e.call(this,r,n)||this;return o.scheduler=r,o.work=n,o}return t.prototype.requestAsyncId=function(r,n,o){return o===void 0&&(o=0),o!==null&&o>0?e.prototype.requestAsyncId.call(this,r,n,o):(r.actions.push(this),r._scheduled||(r._scheduled=er.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,n,o){var i;if(o===void 0&&(o=0),o!=null?o>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,n,o);var a=r.actions;n!=null&&n===r._scheduled&&((i=a[a.length-1])===null||i===void 0?void 0:i.id)!==n&&(er.cancelAnimationFrame(n),r._scheduled=void 0)},t})(tr);var oi=(function(e){ue(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var n;r?n=r.id:(n=this._scheduled,this._scheduled=void 0);var o=this.actions,i;r=r||o.shift();do if(i=r.execute(r.state,r.delay))break;while((r=o[0])&&r.id===n&&o.shift());if(this._active=!1,i){for(;(r=o[0])&&r.id===n&&o.shift();)r.unsubscribe();throw i}},t})(rr);var je=new oi(ni);var y=new U(function(e){return e.complete()});function Br(e){return e&&F(e.schedule)}function Vn(e){return e[e.length-1]}function _t(e){return F(Vn(e))?e.pop():void 0}function qe(e){return Br(Vn(e))?e.pop():void 0}function Yr(e,t){return typeof Vn(e)=="number"?e.pop():t}var nr=(function(e){return e&&typeof e.length=="number"&&typeof e!="function"});function Gr(e){return F(e==null?void 0:e.then)}function Jr(e){return F(e[Qt])}function Xr(e){return Symbol.asyncIterator&&F(e==null?void 0:e[Symbol.asyncIterator])}function Zr(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function Rc(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var Qr=Rc();function en(e){return F(e==null?void 0:e[Qr])}function tn(e){return Vo(this,arguments,function(){var r,n,o,i;return Wr(this,function(a){switch(a.label){case 0:r=e.getReader(),a.label=1;case 1:a.trys.push([1,,9,10]),a.label=2;case 2:return[4,kt(r.read())];case 3:return n=a.sent(),o=n.value,i=n.done,i?[4,kt(void 0)]:[3,5];case 4:return[2,a.sent()];case 5:return[4,kt(o)];case 6:return[4,a.sent()];case 7:return a.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function rn(e){return F(e==null?void 0:e.getReader)}function q(e){if(e instanceof U)return e;if(e!=null){if(Jr(e))return jc(e);if(nr(e))return Fc(e);if(Gr(e))return Uc(e);if(Xr(e))return ii(e);if(en(e))return Nc(e);if(rn(e))return Dc(e)}throw Zr(e)}function jc(e){return new U(function(t){var r=e[Qt]();if(F(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function Fc(e){return new U(function(t){for(var r=0;r=2;return function(n){return n.pipe(e?L(function(o,i){return e(o,i,n)}):Oe,Me(1),r?ot(t):wi(function(){return new on}))}}function Gn(e){return e<=0?function(){return y}:S(function(t,r){var n=[];t.subscribe(T(r,function(o){n.push(o),e=2,!0))}function xe(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new I}:t,n=e.resetOnError,o=n===void 0?!0:n,i=e.resetOnComplete,a=i===void 0?!0:i,s=e.resetOnRefCountZero,c=s===void 0?!0:s;return function(l){var u,p,d,m=0,h=!1,v=!1,x=function(){p==null||p.unsubscribe(),p=void 0},w=function(){x(),u=d=void 0,h=v=!1},E=function(){var _=u;w(),_==null||_.unsubscribe()};return S(function(_,de){m++,!v&&!h&&x();var be=d=d!=null?d:r();de.add(function(){m--,m===0&&!v&&!h&&(p=Jn(E,c))}),be.subscribe(de),!u&&m>0&&(u=new Ct({next:function(M){return be.next(M)},error:function(M){v=!0,x(),p=Jn(w,o,M),be.error(M)},complete:function(){h=!0,x(),p=Jn(w,a),be.complete()}}),q(_).subscribe(u))})(l)}}function Jn(e,t){for(var r=[],n=2;ne.next(document)),e}function P(e,t=document){return Array.from(t.querySelectorAll(e))}function G(e,t=document){let r=Le(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function Le(e,t=document){return t.querySelector(e)||void 0}function xt(){var e,t,r,n;return(n=(r=(t=(e=document.activeElement)==null?void 0:e.shadowRoot)==null?void 0:t.activeElement)!=null?r:document.activeElement)!=null?n:void 0}var il=R(b(document.body,"focusin"),b(document.body,"focusout")).pipe(Be(1),J(void 0),f(()=>xt()||document.body),se(1));function ir(e){return il.pipe(f(t=>e.contains(t)),ie())}function Ft(e,t){let{matches:r}=matchMedia("(hover)");return j(()=>(r?R(b(e,"mouseenter").pipe(f(()=>!0)),b(e,"mouseleave").pipe(f(()=>!1))):R(b(e,"touchstart").pipe(f(()=>!0)),b(e,"touchend").pipe(f(()=>!1)),b(e,"touchcancel").pipe(f(()=>!1)))).pipe(t?Tr(o=>Ve(+!o*t)):Oe,J(!0,e.matches(":hover"))))}function Oi(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)Oi(e,r)}function A(e,t,...r){let n=document.createElement(e);if(t)for(let o of Object.keys(t))typeof t[o]!="undefined"&&(typeof t[o]!="boolean"?n.setAttribute(o,t[o]):n.setAttribute(o,""));for(let o of r)Oi(n,o);return n}function Li(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function ar(e){let t=A("script",{src:e});return j(()=>(document.head.appendChild(t),R(b(t,"load"),b(t,"error").pipe(g(()=>zn(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(f(()=>{}),V(()=>document.head.removeChild(t)),Me(1))))}var Mi=new I,al=j(()=>typeof ResizeObserver=="undefined"?ar("https://unpkg.com/resize-observer-polyfill"):Y(void 0)).pipe(f(()=>new ResizeObserver(e=>e.forEach(t=>Mi.next(t)))),g(e=>R(Ke,Y(e)).pipe(V(()=>e.disconnect()))),se(1));function Ae(e){return{width:e.offsetWidth,height:e.offsetHeight}}function Re(e){let t=e;for(;t.clientWidth===0&&t.parentElement;)t=t.parentElement;return al.pipe($(r=>r.observe(t)),g(r=>Mi.pipe(L(n=>n.target===t),V(()=>r.unobserve(t)))),f(()=>Ae(e)),J(Ae(e)))}function Mr(e){return{width:e.scrollWidth,height:e.scrollHeight}}function ki(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}function Ai(e){let t=[],r=e.parentElement;for(;r;)(e.clientWidth>r.clientWidth||e.clientHeight>r.clientHeight)&&t.push(r),r=(e=r).parentElement;return t.length===0&&t.push(document.documentElement),t}function wt(e){return{x:e.offsetLeft,y:e.offsetTop}}function Ci(e){let t=e.getBoundingClientRect();return{x:t.x+window.scrollX,y:t.y+window.scrollY}}function Hi(e){return R(b(window,"load"),b(window,"resize")).pipe(Xe(0,je),f(()=>wt(e)),J(wt(e)))}function ln(e){return{x:e.scrollLeft,y:e.scrollTop}}function Ut(e){return R(b(e,"scroll"),b(window,"scroll"),b(window,"resize")).pipe(Xe(0,je),f(()=>ln(e)),J(ln(e)))}var $i=new I,sl=j(()=>Y(new IntersectionObserver(e=>{for(let t of e)$i.next(t)},{threshold:0}))).pipe(g(e=>R(Ke,Y(e)).pipe(V(()=>e.disconnect()))),se(1));function Et(e){return sl.pipe($(t=>t.observe(e)),g(t=>$i.pipe(L(({target:r})=>r===e),V(()=>t.unobserve(e)),f(({isIntersecting:r})=>r))))}var cl=Object.create,la=Object.defineProperty,ll=Object.getOwnPropertyDescriptor,ul=Object.getOwnPropertyNames,pl=Object.getPrototypeOf,fl=Object.prototype.hasOwnProperty,ml=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),dl=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of ul(t))!fl.call(e,o)&&o!==r&&la(e,o,{get:()=>t[o],enumerable:!(n=ll(t,o))||n.enumerable});return e},hl=(e,t,r)=>(r=e!=null?cl(pl(e)):{},dl(t||!e||!e.__esModule?la(r,"default",{value:e,enumerable:!0}):r,e)),vl=ml((e,t)=>{var r="Expected a function",n=NaN,o="[object Symbol]",i=/^\s+|\s+$/g,a=/^[-+]0x[0-9a-f]+$/i,s=/^0b[01]+$/i,c=/^0o[0-7]+$/i,l=parseInt,u=typeof global=="object"&&global&&global.Object===Object&&global,p=typeof self=="object"&&self&&self.Object===Object&&self,d=u||p||Function("return this")(),m=Object.prototype,h=m.toString,v=Math.max,x=Math.min,w=function(){return d.Date.now()};function E(O,N,ee){var le,ce,Ne,bt,De,st,tt=0,Yt=!1,Mt=!1,vr=!0;if(typeof O!="function")throw new TypeError(r);N=M(N)||0,_(ee)&&(Yt=!!ee.leading,Mt="maxWait"in ee,Ne=Mt?v(M(ee.maxWait)||0,N):Ne,vr="trailing"in ee?!!ee.trailing:vr);function B(Te){var gt=le,br=ce;return le=ce=void 0,tt=Te,bt=O.apply(br,gt),bt}function C(Te){return tt=Te,De=setTimeout(W,N),Yt?B(Te):bt}function k(Te){var gt=Te-st,br=Te-tt,Ro=N-gt;return Mt?x(Ro,Ne-br):Ro}function D(Te){var gt=Te-st,br=Te-tt;return st===void 0||gt>=N||gt<0||Mt&&br>=Ne}function W(){var Te=w();if(D(Te))return Z(Te);De=setTimeout(W,k(Te))}function Z(Te){return De=void 0,vr&&le?B(Te):(le=ce=void 0,bt)}function We(){De!==void 0&&clearTimeout(De),tt=0,le=st=ce=De=void 0}function Gt(){return De===void 0?bt:Z(w())}function Nr(){var Te=w(),gt=D(Te);if(le=arguments,ce=this,st=Te,gt){if(De===void 0)return C(st);if(Mt)return De=setTimeout(W,N),B(st)}return De===void 0&&(De=setTimeout(W,N)),bt}return Nr.cancel=We,Nr.flush=Gt,Nr}function _(O){var N=typeof O;return!!O&&(N=="object"||N=="function")}function de(O){return!!O&&typeof O=="object"}function be(O){return typeof O=="symbol"||de(O)&&h.call(O)==o}function M(O){if(typeof O=="number")return O;if(be(O))return n;if(_(O)){var N=typeof O.valueOf=="function"?O.valueOf():O;O=_(N)?N+"":N}if(typeof O!="string")return O===0?O:+O;O=O.replace(i,"");var ee=s.test(O);return ee||c.test(O)?l(O.slice(2),ee?2:8):a.test(O)?n:+O}t.exports=E}),yn,K,ua,pa,Nt,Pi,fa,ma,da,lo,to,ro,bl,Ar={},ha=[],gl=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,Pr=Array.isArray;function pt(e,t){for(var r in t)e[r]=t[r];return e}function uo(e){e&&e.parentNode&&e.parentNode.removeChild(e)}function Wt(e,t,r){var n,o,i,a={};for(i in t)i=="key"?n=t[i]:i=="ref"?o=t[i]:a[i]=t[i];if(arguments.length>2&&(a.children=arguments.length>3?yn.call(arguments,2):r),typeof e=="function"&&e.defaultProps!=null)for(i in e.defaultProps)a[i]===void 0&&(a[i]=e.defaultProps[i]);return fn(e,a,n,o,null)}function fn(e,t,r,n,o){var i={type:e,props:t,key:r,ref:n,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:o!=null?o:++ua,__i:-1,__u:0};return o==null&&K.vnode!=null&&K.vnode(i),i}function ft(e){return e.children}function at(e,t){this.props=e,this.context=t}function cr(e,t){if(t==null)return e.__?cr(e.__,e.__i+1):null;for(var r;ts&&Nt.sort(ma),e=Nt.shift(),s=Nt.length,e.__d&&(r=void 0,n=void 0,o=(n=(t=e).__v).__e,i=[],a=[],t.__P&&((r=pt({},n)).__v=n.__v+1,K.vnode&&K.vnode(r),po(t.__P,r,n,t.__n,t.__P.namespaceURI,32&n.__u?[o]:null,i,o!=null?o:cr(n),!!(32&n.__u),a),r.__v=n.__v,r.__.__k[r.__i]=r,_a(i,r,a),n.__e=n.__=null,r.__e!=o&&va(r)));vn.__r=0}function ba(e,t,r,n,o,i,a,s,c,l,u){var p,d,m,h,v,x,w,E=n&&n.__k||ha,_=t.length;for(c=_l(r,t,E,c,_),p=0;p<_;p++)(m=r.__k[p])!=null&&(d=m.__i==-1?Ar:E[m.__i]||Ar,m.__i=p,x=po(e,m,d,o,i,a,s,c,l,u),h=m.__e,m.ref&&d.ref!=m.ref&&(d.ref&&fo(d.ref,null,m),u.push(m.ref,m.__c||h,m)),v==null&&h!=null&&(v=h),(w=!!(4&m.__u))||d.__k===m.__k?c=ga(m,c,e,w):typeof m.type=="function"&&x!==void 0?c=x:h&&(c=h.nextSibling),m.__u&=-7);return r.__e=v,c}function _l(e,t,r,n,o){var i,a,s,c,l,u=r.length,p=u,d=0;for(e.__k=new Array(o),i=0;i0?fn(a.type,a.props,a.key,a.ref?a.ref:null,a.__v):a).__=e,a.__b=e.__b+1,s=null,(l=a.__i=yl(a,r,c,p))!=-1&&(p--,(s=r[l])&&(s.__u|=2)),s==null||s.__v==null?(l==-1&&(o>u?d--:oc?d--:d++,a.__u|=4))):e.__k[i]=null;if(p)for(i=0;i(u?1:0)){for(o=r-1,i=r+1;o>=0||i=0?o--:i++])!=null&&!(2&l.__u)&&s==l.key&&c==l.type)return a}return-1}function Ri(e,t,r){t[0]=="-"?e.setProperty(t,r!=null?r:""):e[t]=r==null?"":typeof r!="number"||gl.test(t)?r:r+"px"}function un(e,t,r,n,o){var i,a;e:if(t=="style")if(typeof r=="string")e.style.cssText=r;else{if(typeof n=="string"&&(e.style.cssText=n=""),n)for(t in n)r&&t in r||Ri(e.style,t,"");if(r)for(t in r)n&&r[t]==n[t]||Ri(e.style,t,r[t])}else if(t[0]=="o"&&t[1]=="n")i=t!=(t=t.replace(da,"$1")),a=t.toLowerCase(),t=a in e||t=="onFocusOut"||t=="onFocusIn"?a.slice(2):t.slice(2),e.l||(e.l={}),e.l[t+i]=r,r?n?r.u=n.u:(r.u=lo,e.addEventListener(t,i?ro:to,i)):e.removeEventListener(t,i?ro:to,i);else{if(o=="http://www.w3.org/2000/svg")t=t.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if(t!="width"&&t!="height"&&t!="href"&&t!="list"&&t!="form"&&t!="tabIndex"&&t!="download"&&t!="rowSpan"&&t!="colSpan"&&t!="role"&&t!="popover"&&t in e)try{e[t]=r!=null?r:"";break e}catch(s){}typeof r=="function"||(r==null||r===!1&&t[4]!="-"?e.removeAttribute(t):e.setAttribute(t,t=="popover"&&r==1?"":r))}}function ji(e){return function(t){if(this.l){var r=this.l[t.type+e];if(t.t==null)t.t=lo++;else if(t.t0?e:Pr(e)?e.map(ya):pt({},e)}function xl(e,t,r,n,o,i,a,s,c){var l,u,p,d,m,h,v,x=r.props,w=t.props,E=t.type;if(E=="svg"?o="http://www.w3.org/2000/svg":E=="math"?o="http://www.w3.org/1998/Math/MathML":o||(o="http://www.w3.org/1999/xhtml"),i!=null){for(l=0;l=r.__.length&&r.__.push({}),r.__[e]}function bn(e){return $r=1,Tl(Ta,e)}function Tl(e,t,r){var n=mo(Hr++,2);if(n.t=e,!n.__c&&(n.__=[r?r(t):Ta(void 0,t),function(s){var c=n.__N?n.__N[0]:n.__[0],l=n.t(c,s);c!==l&&(n.__N=[l,n.__[1]],n.__c.setState({}))}],n.__c=ve,!ve.__f)){var o=function(s,c,l){if(!n.__c.__H)return!0;var u=n.__c.__H.__.filter(function(d){return!!d.__c});if(u.every(function(d){return!d.__N}))return!i||i.call(this,s,c,l);var p=n.__c.props!==s;return u.forEach(function(d){if(d.__N){var m=d.__[0];d.__=d.__N,d.__N=void 0,m!==d.__[0]&&(p=!0)}}),i&&i.call(this,s,c,l)||p};ve.__f=!0;var i=ve.shouldComponentUpdate,a=ve.componentWillUpdate;ve.componentWillUpdate=function(s,c,l){if(this.__e){var u=i;i=void 0,o(s,c,l),i=u}a&&a.call(this,s,c,l)},ve.shouldComponentUpdate=o}return n.__N||n.__}function mt(e,t){var r=mo(Hr++,3);!we.__s&&Ea(r.__H,t)&&(r.__=e,r.u=t,ve.__H.__h.push(r))}function Vt(e){return $r=5,ur(function(){return{current:e}},[])}function ur(e,t){var r=mo(Hr++,7);return Ea(r.__H,t)&&(r.__=e(),r.__H=t,r.__h=e),r.__}function Sl(e,t){return $r=8,ur(function(){return e},t)}function Ol(){for(var e;e=wa.shift();)if(e.__P&&e.__H)try{e.__H.__h.forEach(mn),e.__H.__h.forEach(oo),e.__H.__h=[]}catch(t){e.__H.__h=[],we.__e(t,e.__v)}}we.__b=function(e){ve=null,Ui&&Ui(e)},we.__=function(e,t){e&&t.__k&&t.__k.__m&&(e.__m=t.__k.__m),zi&&zi(e,t)},we.__r=function(e){Ni&&Ni(e),Hr=0;var t=(ve=e.__c).__H;t&&(Zn===ve?(t.__h=[],ve.__h=[],t.__.forEach(function(r){r.__N&&(r.__=r.__N),r.u=r.__N=void 0})):(t.__h.forEach(mn),t.__h.forEach(oo),t.__h=[],Hr=0)),Zn=ve},we.diffed=function(e){Di&&Di(e);var t=e.__c;t&&t.__H&&(t.__H.__h.length&&(wa.push(t)!==1&&Fi===we.requestAnimationFrame||((Fi=we.requestAnimationFrame)||Ll)(Ol)),t.__H.__.forEach(function(r){r.u&&(r.__H=r.u),r.u=void 0})),Zn=ve=null},we.__c=function(e,t){t.some(function(r){try{r.__h.forEach(mn),r.__h=r.__h.filter(function(n){return!n.__||oo(n)})}catch(n){t.some(function(o){o.__h&&(o.__h=[])}),t=[],we.__e(n,r.__v)}}),Wi&&Wi(e,t)},we.unmount=function(e){Vi&&Vi(e);var t,r=e.__c;r&&r.__H&&(r.__H.__.forEach(function(n){try{mn(n)}catch(o){t=o}}),r.__H=void 0,t&&we.__e(t,r.__v))};var qi=typeof requestAnimationFrame=="function";function Ll(e){var t,r=function(){clearTimeout(n),qi&&cancelAnimationFrame(t),setTimeout(e)},n=setTimeout(r,35);qi&&(t=requestAnimationFrame(r))}function mn(e){var t=ve,r=e.__c;typeof r=="function"&&(e.__c=void 0,r()),ve=t}function oo(e){var t=ve;e.__c=e.__(),ve=t}function Ea(e,t){return!e||e.length!==t.length||t.some(function(r,n){return r!==e[n]})}function Ta(e,t){return typeof t=="function"?t(e):t}function Ml(e,t){for(var r in t)e[r]=t[r];return e}function Ki(e,t){for(var r in e)if(r!=="__source"&&!(r in t))return!0;for(var n in t)if(n!=="__source"&&e[n]!==t[n])return!0;return!1}function Bi(e,t){this.props=e,this.context=t}(Bi.prototype=new at).isPureReactComponent=!0,Bi.prototype.shouldComponentUpdate=function(e,t){return Ki(this.props,e)||Ki(this.state,t)};var Yi=K.__b;K.__b=function(e){e.type&&e.type.__f&&e.ref&&(e.props.ref=e.ref,e.ref=null),Yi&&Yi(e)};var Yx=typeof Symbol<"u"&&Symbol.for&&Symbol.for("react.forward_ref")||3911,kl=K.__e;K.__e=function(e,t,r,n){if(e.then){for(var o,i=t;i=i.__;)if((o=i.__c)&&o.__c)return t.__e==null&&(t.__e=r.__e,t.__k=r.__k),o.__c(e,t)}kl(e,t,r,n)};var Gi=K.unmount;function Sa(e,t,r){return e&&(e.__c&&e.__c.__H&&(e.__c.__H.__.forEach(function(n){typeof n.__c=="function"&&n.__c()}),e.__c.__H=null),(e=Ml({},e)).__c!=null&&(e.__c.__P===r&&(e.__c.__P=t),e.__c.__e=!0,e.__c=null),e.__k=e.__k&&e.__k.map(function(n){return Sa(n,t,r)})),e}function Oa(e,t,r){return e&&r&&(e.__v=null,e.__k=e.__k&&e.__k.map(function(n){return Oa(n,t,r)}),e.__c&&e.__c.__P===t&&(e.__e&&r.appendChild(e.__e),e.__c.__e=!0,e.__c.__P=r)),e}function Qn(){this.__u=0,this.o=null,this.__b=null}function La(e){var t=e.__.__c;return t&&t.__a&&t.__a(e)}function pn(){this.i=null,this.l=null}K.unmount=function(e){var t=e.__c;t&&t.__R&&t.__R(),t&&32&e.__u&&(e.type=null),Gi&&Gi(e)},(Qn.prototype=new at).__c=function(e,t){var r=t.__c,n=this;n.o==null&&(n.o=[]),n.o.push(r);var o=La(n.__v),i=!1,a=function(){i||(i=!0,r.__R=null,o?o(s):s())};r.__R=a;var s=function(){if(!--n.__u){if(n.state.__a){var c=n.state.__a;n.__v.__k[0]=Oa(c,c.__c.__P,c.__c.__O)}var l;for(n.setState({__a:n.__b=null});l=n.o.pop();)l.forceUpdate()}};n.__u++||32&t.__u||n.setState({__a:n.__b=n.__v.__k[0]}),e.then(a,a)},Qn.prototype.componentWillUnmount=function(){this.o=[]},Qn.prototype.render=function(e,t){if(this.__b){if(this.__v.__k){var r=document.createElement("div"),n=this.__v.__k[0].__c;this.__v.__k[0]=Sa(this.__b,r,n.__O=n.__P)}this.__b=null}var o=t.__a&&Wt(ft,null,e.fallback);return o&&(o.__u&=-33),[Wt(ft,null,t.__a?null:e.children),o]};var Ji=function(e,t,r){if(++r[1]===r[0]&&e.l.delete(t),e.props.revealOrder&&(e.props.revealOrder[0]!=="t"||!e.l.size))for(r=e.i;r;){for(;r.length>3;)r.pop()();if(r[1]Object.freeze({get current(){return t.current}}),[])}var Nl=typeof globalThis<"u"&&typeof navigator<"u"&&typeof document<"u";function Dl(e,...t){var r;(r=e==null?void 0:e.addEventListener)==null||r.call(e,...t)}function Wl(e,...t){var r;(r=e==null?void 0:e.removeEventListener)==null||r.call(e,...t)}var Vl=(e,t)=>Object.hasOwn(e,t),zl=()=>!0,ql=()=>!1;function Kl(e=!1){let t=Vt(e),r=Sl(()=>t.current,[]);return mt(()=>(t.current=!0,()=>{t.current=!1}),[]),r}function Bl(e,...t){let r=Kl(),n=ka(t[1]),o=ur(()=>function(...i){r()&&(typeof n.current=="function"?n.current.apply(this,i):typeof n.current.handleEvent=="function"&&n.current.handleEvent.apply(this,i))},[]);mt(()=>{let i=Yl(e)?e.current:e;if(!i)return;let a=t.slice(2);return Dl(i,t[0],o,...a),()=>{Wl(i,t[0],o,...a)}},[e,t[0]])}function Yl(e){return e!==null&&typeof e=="object"&&Vl(e,"current")}var Gl=e=>typeof e=="function"?e:typeof e=="string"?t=>t.key===e:e?zl:ql,Jl=Nl?globalThis:null;function Aa(e,t,r=[],n={}){let{event:o="keydown",target:i=Jl,eventOptions:a}=n,s=ka(t),c=ur(()=>{let l=Gl(e);return function(u){l(u)&&s.current.call(this,u)}},r);Bl(i,o,c,a)}function Ca(e){var t,r,n="";if(typeof e=="string"||typeof e=="number")n+=e;else if(typeof e=="object")if(Array.isArray(e)){var o=e.length;for(t=0;t1)St--;else{for(var e,t=!1;kr!==void 0;){var r=kr;for(kr=void 0,io++;r!==void 0;){var n=r.o;if(r.o=void 0,r.f&=-3,!(8&r.f)&&Pa(r))try{r.c()}catch(o){t||(e=o,t=!0)}r=n}}if(io=0,St--,t)throw e}}function Ql(e){if(St>0)return e();St++;try{return e()}finally{xn()}}var ae=void 0;function Ha(e){var t=ae;ae=void 0;try{return e()}finally{ae=t}}var kr=void 0,St=0,io=0,gn=0;function $a(e){if(ae!==void 0){var t=e.n;if(t===void 0||t.t!==ae)return t={i:0,S:e,p:ae.s,n:void 0,t:ae,e:void 0,x:void 0,r:t},ae.s!==void 0&&(ae.s.n=t),ae.s=t,e.n=t,32&ae.f&&e.S(t),t;if(t.i===-1)return t.i=0,t.n!==void 0&&(t.n.p=t.p,t.p!==void 0&&(t.p.n=t.n),t.p=ae.s,t.n=void 0,ae.s.n=t,ae.s=t),t}}function Ce(e,t){this.v=e,this.i=0,this.n=void 0,this.t=void 0,this.W=t==null?void 0:t.watched,this.Z=t==null?void 0:t.unwatched,this.name=t==null?void 0:t.name}Ce.prototype.brand=Zl;Ce.prototype.h=function(){return!0};Ce.prototype.S=function(e){var t=this,r=this.t;r!==e&&e.e===void 0&&(e.x=r,this.t=e,r!==void 0?r.e=e:Ha(function(){var n;(n=t.W)==null||n.call(t)}))};Ce.prototype.U=function(e){var t=this;if(this.t!==void 0){var r=e.e,n=e.x;r!==void 0&&(r.x=n,e.e=void 0),n!==void 0&&(n.e=r,e.x=void 0),e===this.t&&(this.t=n,n===void 0&&Ha(function(){var o;(o=t.Z)==null||o.call(t)}))}};Ce.prototype.subscribe=function(e){var t=this;return qt(function(){var r=t.value,n=ae;ae=void 0;try{e(r)}finally{ae=n}},{name:"sub"})};Ce.prototype.valueOf=function(){return this.value};Ce.prototype.toString=function(){return this.value+""};Ce.prototype.toJSON=function(){return this.value};Ce.prototype.peek=function(){var e=ae;ae=void 0;try{return this.value}finally{ae=e}};Object.defineProperty(Ce.prototype,"value",{get:function(){var e=$a(this);return e!==void 0&&(e.i=this.i),this.v},set:function(e){if(e!==this.v){if(io>100)throw new Error("Cycle detected");this.v=e,this.i++,gn++,St++;try{for(var t=this.t;t!==void 0;t=t.x)t.t.N()}finally{xn()}}}});function Ot(e,t){return new Ce(e,t)}function Pa(e){for(var t=e.s;t!==void 0;t=t.n)if(t.S.i!==t.i||!t.S.h()||t.S.i!==t.i)return!0;return!1}function Ia(e){for(var t=e.s;t!==void 0;t=t.n){var r=t.S.n;if(r!==void 0&&(t.r=r),t.S.n=t,t.i=-1,t.n===void 0){e.s=t;break}}}function Ra(e){for(var t=e.s,r=void 0;t!==void 0;){var n=t.p;t.i===-1?(t.S.U(t),n!==void 0&&(n.n=t.n),t.n!==void 0&&(t.n.p=n)):r=t,t.S.n=t.r,t.r!==void 0&&(t.r=void 0),t=n}e.s=r}function Kt(e,t){Ce.call(this,void 0),this.x=e,this.s=void 0,this.g=gn-1,this.f=4,this.W=t==null?void 0:t.watched,this.Z=t==null?void 0:t.unwatched,this.name=t==null?void 0:t.name}Kt.prototype=new Ce;Kt.prototype.h=function(){if(this.f&=-3,1&this.f)return!1;if((36&this.f)==32||(this.f&=-5,this.g===gn))return!0;if(this.g=gn,this.f|=1,this.i>0&&!Pa(this))return this.f&=-2,!0;var e=ae;try{Ia(this),ae=this;var t=this.x();(16&this.f||this.v!==t||this.i===0)&&(this.v=t,this.f&=-17,this.i++)}catch(r){this.v=r,this.f|=16,this.i++}return ae=e,Ra(this),this.f&=-2,!0};Kt.prototype.S=function(e){if(this.t===void 0){this.f|=36;for(var t=this.s;t!==void 0;t=t.n)t.S.S(t)}Ce.prototype.S.call(this,e)};Kt.prototype.U=function(e){if(this.t!==void 0&&(Ce.prototype.U.call(this,e),this.t===void 0)){this.f&=-33;for(var t=this.s;t!==void 0;t=t.n)t.S.U(t)}};Kt.prototype.N=function(){if(!(2&this.f)){this.f|=6;for(var e=this.t;e!==void 0;e=e.x)e.t.N()}};Object.defineProperty(Kt.prototype,"value",{get:function(){if(1&this.f)throw new Error("Cycle detected");var e=$a(this);if(this.h(),e!==void 0&&(e.i=this.i),16&this.f)throw this.v;return this.v}});function ta(e,t){return new Kt(e,t)}function ja(e){var t=e.u;if(e.u=void 0,typeof t=="function"){St++;var r=ae;ae=void 0;try{t()}catch(n){throw e.f&=-2,e.f|=8,ho(e),n}finally{ae=r,xn()}}}function ho(e){for(var t=e.s;t!==void 0;t=t.n)t.S.U(t);e.x=void 0,e.s=void 0,ja(e)}function eu(e){if(ae!==this)throw new Error("Out-of-order effect");Ra(this),ae=e,this.f&=-2,8&this.f&&ho(this),xn()}function pr(e,t){this.x=e,this.u=void 0,this.s=void 0,this.o=void 0,this.f=32,this.name=t==null?void 0:t.name}pr.prototype.c=function(){var e=this.S();try{if(8&this.f||this.x===void 0)return;var t=this.x();typeof t=="function"&&(this.u=t)}finally{e()}};pr.prototype.S=function(){if(1&this.f)throw new Error("Cycle detected");this.f|=1,this.f&=-9,ja(this),Ia(this),St++;var e=ae;return ae=this,eu.bind(this,e)};pr.prototype.N=function(){2&this.f||(this.f|=2,this.o=kr,kr=this)};pr.prototype.d=function(){this.f|=8,1&this.f||ho(this)};pr.prototype.dispose=function(){this.d()};function qt(e,t){var r=new pr(e,t);try{r.c()}catch(o){throw r.d(),o}var n=r.d.bind(r);return n[Symbol.dispose]=n,n}var Fa,vo,eo,Ua=[];qt(function(){Fa=this.N})();function fr(e,t){K[e]=t.bind(null,K[e]||function(){})}function _n(e){eo&&eo(),eo=e&&e.S()}function Na(e){var t=this,r=e.data,n=ru(r);n.value=r;var o=ur(function(){for(var s=t,c=t.__v;c=c.__;)if(c.__c){c.__c.__$f|=4;break}var l=ta(function(){var m=n.value.value;return m===0?0:m===!0?"":m||""}),u=ta(function(){return!Array.isArray(l.value)&&!pa(l.value)}),p=qt(function(){if(this.N=Da,u.value){var m=l.value;s.__v&&s.__v.__e&&s.__v.__e.nodeType===3&&(s.__v.__e.data=m)}}),d=t.__$u.d;return t.__$u.d=function(){p(),d.call(this)},[u,l]},[]),i=o[0],a=o[1];return i.value?a.peek():a.value}Na.displayName="ReactiveTextNode";Object.defineProperties(Ce.prototype,{constructor:{configurable:!0,value:void 0},type:{configurable:!0,value:Na},props:{configurable:!0,get:function(){return{data:this}}},__b:{configurable:!0,value:1}});fr("__b",function(e,t){if(typeof t.type=="function"&&typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.exitComponent(),typeof t.type=="string"){var r,n=t.props;for(var o in n)if(o!=="children"){var i=n[o];i instanceof Ce&&(r||(t.__np=r={}),r[o]=i,n[o]=i.peek())}}e(t)});fr("__r",function(e,t){if(typeof t.type=="function"&&typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.enterComponent(t),t.type!==ft){_n();var r,n=t.__c;n&&(n.__$f&=-2,(r=n.__$u)===void 0&&(n.__$u=r=(function(o){var i;return qt(function(){i=this}),i.c=function(){n.__$f|=1,n.setState({})},i})())),vo=n,_n(r)}e(t)});fr("__e",function(e,t,r,n){typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.exitComponent(),_n(),vo=void 0,e(t,r,n)});fr("diffed",function(e,t){typeof t.type=="function"&&typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.exitComponent(),_n(),vo=void 0;var r;if(typeof t.type=="string"&&(r=t.__e)){var n=t.__np,o=t.props;if(n){var i=r.U;if(i)for(var a in i){var s=i[a];s!==void 0&&!(a in n)&&(s.d(),i[a]=void 0)}else i={},r.U=i;for(var c in n){var l=i[c],u=n[c];l===void 0?(l=tu(r,c,u,o),i[c]=l):l.o(u,o)}}}e(t)});function tu(e,t,r,n){var o=t in e&&e.ownerSVGElement===void 0,i=Ot(r);return{o:function(a,s){i.value=a,n=s},d:qt(function(){this.N=Da;var a=i.value.value;n[t]!==a&&(n[t]=a,o?e[t]=a:a?e.setAttribute(t,a):e.removeAttribute(t))})}}fr("unmount",function(e,t){if(typeof t.type=="string"){var r=t.__e;if(r){var n=r.U;if(n){r.U=void 0;for(var o in n){var i=n[o];i&&i.d()}}}}else{var a=t.__c;if(a){var s=a.__$u;s&&(a.__$u=void 0,s.d())}}e(t)});fr("__h",function(e,t,r,n){(n<3||n===9)&&(t.__$f|=2),e(t,r,n)});at.prototype.shouldComponentUpdate=function(e,t){var r=this.__$u,n=r&&r.s!==void 0;for(var o in t)return!0;if(this.__f||typeof this.u=="boolean"&&this.u===!0){var i=2&this.__$f;if(!(n||i||4&this.__$f)||1&this.__$f)return!0}else if(!(n||4&this.__$f)||3&this.__$f)return!0;for(var a in e)if(a!=="__source"&&e[a]!==this.props[a])return!0;for(var s in this.props)if(!(s in e))return!0;return!1};function ru(e,t){return bn(function(){return Ot(e,t)})[0]}var nu=function(e){queueMicrotask(function(){queueMicrotask(e)})};function ou(){Ql(function(){for(var e;e=Ua.shift();)Fa.call(e)})}function Da(){Ua.push(this)===1&&(K.requestAnimationFrame||nu)(ou)}var ao=[0];for(let e=0;e<32;e++)ao.push(ao[e]|1<>>5]>>>e&1}set(e){this.data[e>>>5]|=1<<(e&31)}forEach(e){let t=this.size&31;for(let r=0;r{var r;return(r=t.tags)==null?void 0:r.length})&&(matchMedia("(max-width: 768px)").matches||Wa())}function Dt(){Qe.value=He(H({},Qe.value),{hideSearch:!Qe.value.hideSearch})}function Wa(){Qe.value=He(H({},Qe.value),{hideFilters:!Qe.value.hideFilters})}function dn(){return Qe.value.selectedItem}function so(e){Qe.value=He(H({},Qe.value),{selectedItem:e})}function su(){var e,t;return(t=(e=lr.value)==null?void 0:e.items)!=null?t:[]}function wn(){return typeof Se.value.input=="string"?Se.value.input:""}function Va(e){let t=za();e.length&&!t.length?Se.value=He(H({},Se.value),{page:void 0,input:e}):!e.length&&t.length?Se.value=He(H({},Se.value),{page:void 0,input:{type:"operator",data:{operator:"not",operands:[]}}}):Se.value=He(H({},Se.value),{page:void 0,input:e})}function cu(){typeof it.value.pagination.next<"u"&&(Se.value=He(H({},Se.value),{page:it.value.pagination.next}))}function lu(e){let t=Se.value.filter.input;if("type"in t&&t.type==="operator"){for(let r of t.data.operands)if("type"in r&&r.type==="value"&&typeof r.data.value=="string"&&r.data.value===e)return!0}return!1}function za(){let e=Se.value.filter.input,t=[];if("type"in e&&e.type==="operator")for(let r of e.data.operands)"type"in r&&r.type==="value"&&typeof r.data.value=="string"&&t.push(r.data.value);return t}function uu(e){let t=Se.value.filter.input,r=[];if("type"in t&&t.type==="operator")for(let n of t.data.operands)"type"in n&&n.type==="value"&&typeof n.data.value=="string"&&r.push(n.data.value);if(r.includes(e)){let n=r.indexOf(e);n>-1&&r.splice(n,1)}else r.push(e);Se.value=He(H({},Se.value),{page:void 0,filter:He(H({},Se.value.filter),{input:{type:"operator",data:{operator:"and",operands:r.map(n=>({type:"value",data:{field:"tags",value:n}}))}}})}),Va(wn())}function pu(){return it.value.items}function fu(){return it.value.total}function mu(){var e;for(let t of(e=it.value.aggregations)!=null?e:[])if(t.type==="term")return t.data.value;return[]}function sr(){return Qe.value.hideSearch}function du(){return Qe.value.hideFilters}function qa(){var e;return(e=Ka.value.highlight)!=null?e:!1}var Qe=Ot({hideSearch:!0,hideFilters:!0,selectedItem:0}),Ka=Ot({}),lr=Ot(),na=Ot(),Se=Ot({input:"",filter:{input:{type:"operator",data:{operator:"and",operands:[]}},aggregation:{input:[{type:"term",data:{field:"tags"}}]}}}),it=Ot({items:[],query:{select:{documents:new ra(0),terms:new ra(0)},values:[]},pagination:{total:0}});function hu(e,t,r){for(let n=0;tr&&t(0,o,r,r=i);continue;case 62:e.charCodeAt(r+1)===47?t(2,--o,r,r=i+1):hu(e,r,n)?t(3,o,r,r=i+1):t(1,o++,r,r=i+1)}i>r&&t(0,o,r,i)}function bu(e,t=0,r=e.length){let n=++t;e:for(let l=0;n{let i=[],a=[],{onElement:s,onText:c=gu}=typeof r=="function"?{onElement:r}:r,l=0,u=0;return e(t,(p,d,m,h)=>{if(p===0)i[l++]=c(t,m,h),a[u++]={value:null,depth:d};else if(p&1&&(a[u++]={value:bu(t,m,h),depth:d}),p&2)for(let v=0;u>=0;v++){let{value:x,depth:w}=a[--u];if(w>d)continue;let E=i.slice(l-=v,l+v);i[l++]=s(x,E),u++;break}},n,o),i.slice(0,l)}}function yu(e){return e.replace(/[&<>]/g,t=>{switch(t.charCodeAt(0)){case 38:return"&";case 60:return"<";case 62:return">"}})}function hn(e){return e.replace(/&(amp|[lg]t);/g,t=>{switch(t.charCodeAt(1)){case 97:return"&";case 108:return"<";case 103:return">"}})}function xu(e,t){return{start:e.start+t,end:e.end+t,value:e.value}}function wu(e,t,r){return e.slice(t,r)}function Eu(e){let{onHighlight:t,onText:r=wu}=typeof e=="function"?{onHighlight:e}:e;return(n,o,i=0,a=n.length)=>{var l;let s=[],c=(l=o==null?void 0:o.ranges)!=null?l:[];for(let u=0,p=i;ua)break;let m=c[u].end;if(mi&&s.push(r(n,i,d));let{value:h}=c[u];s.push(t(n,{start:d,end:i=m,value:h}))}return i{let o=n.data;switch(o.type){case 1:na.value=!0;break;case 3:typeof o.data.pagination.prev<"u"?it.value=He(H({},it.value),{pagination:o.data.pagination,items:[...it.value.items,...o.data.items]}):(it.value=o.data,so(0));break}},qt(()=>{lr.value&&r.postMessage({type:0,data:lr.value})}),qt(()=>{na.value&&r.postMessage({type:2,data:Se.value})})}var oa={container:"p",hidden:"m"};function ku(e){return z("div",{class:zt(oa.container,{[oa.hidden]:e.hidden}),onClick:()=>Dt()})}var ia={container:"r",disabled:"c"};function co(e){return z("button",{class:zt(ia.container,{[ia.disabled]:!e.onClick}),onClick:e.onClick,children:e.children})}var aa=e=>e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase(),Au=e=>e.replace(/^([A-Z])|[\s-_]+(\w)/g,(t,r,n)=>n?n.toUpperCase():r.toLowerCase()),sa=e=>{let t=Au(e);return t.charAt(0).toUpperCase()+t.slice(1)},Cu=(...e)=>e.filter((t,r,n)=>!!t&&t.trim()!==""&&n.indexOf(t)===r).join(" ").trim(),Hu={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"},$u=c=>{var l=c,{color:e="currentColor",size:t=24,strokeWidth:r=2,absoluteStrokeWidth:n,children:o,iconNode:i,class:a=""}=l,s=gr(l,["color","size","strokeWidth","absoluteStrokeWidth","children","iconNode","class"]);return Wt("svg",H(He(H({},Hu),{width:String(t),height:t,stroke:e,"stroke-width":n?Number(r)*24/Number(t):r,class:["lucide",a].join(" ")}),s),[...i.map(([u,p])=>Wt(u,p)),...Cr(o)])},bo=(e,t)=>{let r=a=>{var s=a,{class:n="",children:o}=s,i=gr(s,["class","children"]);return Wt($u,He(H({},i),{iconNode:t,class:Cu(`lucide-${aa(sa(e))}`,`lucide-${aa(e)}`,n)}),o)};return r.displayName=sa(e),r},Pu=bo("corner-down-left",[["path",{d:"M20 4v7a4 4 0 0 1-4 4H4",key:"6o5b7l"}],["path",{d:"m9 10-5 5 5 5",key:"1kshq7"}]]),Iu=bo("list-filter",[["path",{d:"M2 5h20",key:"1fs1ex"}],["path",{d:"M6 12h12",key:"8npq4p"}],["path",{d:"M9 19h6",key:"456am0"}]]),Ru=bo("search",[["path",{d:"m21 21-4.34-4.34",key:"14j7rj"}],["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}]]),Gx=hl(vl(),1);function ju({threshold:e=0,root:t=null,rootMargin:r="0%",freezeOnceVisible:n=!1,initialIsIntersecting:o=!1,onChange:i}={}){var a;let[s,c]=bn(null),[l,u]=bn(()=>({isIntersecting:o,entry:void 0})),p=Vt();p.current=i;let d=((a=l.entry)==null?void 0:a.isIntersecting)&&n;mt(()=>{if(!s||!("IntersectionObserver"in window)||d)return;let v,x=new IntersectionObserver(w=>{let E=Array.isArray(x.thresholds)?x.thresholds:[x.thresholds];w.forEach(_=>{let de=_.isIntersecting&&E.some(be=>_.intersectionRatio>=be);u({isIntersecting:de,entry:_}),p.current&&p.current(de,_),de&&n&&v&&(v(),v=void 0)})},{threshold:e,root:t,rootMargin:r});return x.observe(s),()=>{x.disconnect()}},[s,JSON.stringify(e),t,r,d,n]);let m=Vt(null);mt(()=>{var v;!s&&(v=l.entry)!=null&&v.target&&!n&&!d&&m.current!==l.entry.target&&(m.current=l.entry.target,u({isIntersecting:o,entry:void 0}))},[s,l.entry,n,d,o]);let h=[c,!!l.isIntersecting,l.entry];return h.ref=h[0],h.isIntersecting=h[1],h.entry=h[2],h}var lt={container:"n",hidden:"l",content:"u",pop:"d",badge:"y",sidebar:"i",controls:"w",results:"k",loadmore:"z"};function Fu(e){let{isIntersecting:t,ref:r}=ju({threshold:0});mt(()=>{t&&cu()},[t]);let n=Vt(null);mt(()=>{n.current&&typeof Se.value.page>"u"&&n.current.scrollTo({top:0,behavior:"smooth"})},[Se.value]);let o=za();return z("div",{class:zt(lt.container,{[lt.hidden]:e.hidden}),children:[z("div",{class:lt.content,children:[z("div",{class:lt.controls,children:[z(co,{onClick:Dt,children:z(Ru,{})}),z(Nu,{focus:!e.hidden}),z(co,{onClick:Wa,children:[z(Iu,{}),o.length>0&&z("span",{class:lt.badge,children:o.length})]})]}),z("div",{class:lt.results,ref:n,children:[z(Du,{keyboard:!e.hidden}),z("div",{class:lt.loadmore,ref:r})]})]}),z("div",{class:zt(lt.sidebar,{[lt.hidden]:du()}),children:z(Uu,{})})]})}var Tt={container:"X",list:"j",heading:"F",title:"I",item:"o",active:"g",value:"R",count:"q"};function Uu(e){let t=mu();return t.sort((r,n)=>n.node.count-r.node.count),z("div",{class:Tt.container,children:[z("h3",{class:Tt.heading,children:"Filters"}),z("h4",{class:Tt.title,children:"Tags"}),z("ol",{class:Tt.list,children:t.map(r=>z("li",{class:zt(Tt.item,{[Tt.active]:lu(r.node.value)}),onClick:()=>uu(r.node.value),children:[z("span",{class:Tt.value,children:r.node.value}),z("span",{class:Tt.count,children:r.node.count})]}))})]})}var ca={container:"f"};function Nu(e){let t=Vt(null);return mt(()=>{var r,n;e.focus?(r=t.current)==null||r.focus():(n=t.current)==null||n.blur()},[e.focus]),z("div",{class:ca.container,children:z("input",{ref:t,type:"text",class:ca.content,value:hn(wn()),onInput:r=>Va(yu(r.currentTarget.value)),autocapitalize:"off",autocomplete:"off",autocorrect:"off",placeholder:"Search",spellcheck:!1,role:"combobox"})})}var ut={container:"b",heading:"A",item:"a",active:"h",wrapper:"B",actions:"s",title:"x",path:"t"};function Ga(){let[e,t]=bn(!1);return mt(()=>{let r=()=>t(!0),n=()=>t(!1);return document.addEventListener("compositionstart",r),document.addEventListener("compositionend",n),()=>{document.removeEventListener("compositionstart",r),document.removeEventListener("compositionend",n)}},[]),e}function Du(e){var s;let t=su(),r=pu(),n=dn(),o=Vt([]),i=Ga();mt(()=>{let c=o.current[n];c&&c.scrollIntoView({block:"center",behavior:"smooth"})},[n]),Aa(e.keyboard,c=>{if(i)return;let l=dn();c.key==="ArrowDown"?(c.preventDefault(),so(Math.min(l+1,r.length-1))):c.key==="ArrowUp"&&(c.preventDefault(),so(Math.max(l-1,0)))},[e.keyboard,i]);let a=(s=fu())!=null?s:0;return z(ft,{children:[r.length>0&&z("h3",{class:ut.heading,children:[z("span",{class:ut.bubble,children:new Intl.NumberFormat("en-US").format(a)})," ","results"]}),z("ol",{class:ut.container,children:r.map((c,l)=>{var m;let u=Ba(t[c.id].title,c.matches.find(({field:h})=>h==="title")),p=Mu((m=t[c.id].path)!=null?m:[],c.matches.find(({field:h})=>h==="path")),d=t[c.id].location;if(qa()){let h=encodeURIComponent(wn()),[v,x]=d.split("#",2);d=`${v}?h=${h.replace(/%20/g,"+")}`,typeof x<"u"&&(d+=`#${x}`)}return z("li",{children:z("a",{ref:h=>{o.current[l]=h},href:d,onClick:()=>Dt(),class:zt(ut.item,{[ut.active]:l===dn()}),children:[z("div",{class:ut.wrapper,children:[z("h2",{class:ut.title,children:u}),z("menu",{class:ut.path,children:p.map(h=>z("li",{children:h}))})]}),z("nav",{class:ut.actions,children:z(co,{children:z(Pu,{})})})]})})})})]})}var Wu={container:"e"};function Vu(e){let t=Ga();return Aa(!0,r=>{var n,o,i,a,s;if(!t)if((r.metaKey||r.ctrlKey)&&r.key==="k")r.preventDefault(),Dt();else if((r.metaKey||r.ctrlKey)&&r.key==="j")document.body.classList.toggle("dark");else if(r.key==="Enter"&&!sr()){r.preventDefault();let c=dn(),l=(o=(n=it.value)==null?void 0:n.items[c])==null?void 0:o.id;if((a=(i=lr.value)==null?void 0:i.items[l])!=null&&a.location){Dt();let u=(s=lr.value)==null?void 0:s.items[l].location;if(qa()){let p=encodeURIComponent(wn()),[d,m]=u.split("#",2);u=`${d}?h=${p.replace(/%20/g,"+")}`,typeof m<"u"&&(u+=`#${m}`)}window.location.href=u}}else r.key==="Escape"&&!sr()&&(r.preventDefault(),Dt())},[t]),z("div",{class:Wu.container,children:[z(ku,{hidden:sr()}),z(Fu,{hidden:sr()})]})}function Ja(e,t){au(e),El(z(Vu,{}),t)}function go(){Dt()}function zu(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function qu(){return R(b(window,"compositionstart").pipe(f(()=>!0)),b(window,"compositionend").pipe(f(()=>!1))).pipe(J(!1))}function Xa(){let e=b(window,"keydown").pipe(f(t=>({mode:sr()?"global":"search",type:t.key,meta:t.ctrlKey||t.metaKey,claim(){t.preventDefault(),t.stopPropagation()}})),L(({mode:t,type:r})=>{if(t==="global"){let n=xt();if(typeof n!="undefined")return!zu(n,r)}return!0}),xe());return qu().pipe(g(t=>t?y:e))}function Ye(){return new URL(location.href)}function dt(e,t=!1){if(X("navigation.instant")&&!t){let r=A("a",{href:e.href});document.body.appendChild(r),r.click(),r.remove()}else location.href=e.href}function Za(){return new I}function Qa(){return location.hash.slice(1)}function es(e){let t=A("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function _o(e){return R(b(window,"hashchange"),e).pipe(f(Qa),J(Qa()),L(t=>t.length>0),se(1))}function ts(e){return _o(e).pipe(f(t=>Le(`[id="${t}"]`)),L(t=>typeof t!="undefined"))}function Ir(e){let t=matchMedia(e);return an(r=>t.addListener(()=>r(t.matches))).pipe(J(t.matches))}function rs(){let e=matchMedia("print");return R(b(window,"beforeprint").pipe(f(()=>!0)),b(window,"afterprint").pipe(f(()=>!1))).pipe(J(e.matches))}function yo(e,t){return e.pipe(g(r=>r?t():y))}function xo(e,t){return new U(r=>{let n=new XMLHttpRequest;return n.open("GET",`${e}`),n.responseType="blob",n.addEventListener("load",()=>{n.status>=200&&n.status<300?(r.next(n.response),r.complete()):r.error(new Error(n.statusText))}),n.addEventListener("error",()=>{r.error(new Error("Network error"))}),n.addEventListener("abort",()=>{r.complete()}),typeof(t==null?void 0:t.progress$)!="undefined"&&(n.addEventListener("progress",o=>{var i;if(o.lengthComputable)t.progress$.next(o.loaded/o.total*100);else{let a=(i=n.getResponseHeader("Content-Length"))!=null?i:0;t.progress$.next(o.loaded/+a*100)}}),t.progress$.next(5)),n.send(),()=>n.abort()})}function et(e,t){return xo(e,t).pipe(g(r=>r.text()),f(r=>JSON.parse(r)),se(1))}function En(e,t){let r=new DOMParser;return xo(e,t).pipe(g(n=>n.text()),f(n=>r.parseFromString(n,"text/html")),se(1))}function ns(e,t){let r=new DOMParser;return xo(e,t).pipe(g(n=>n.text()),f(n=>r.parseFromString(n,"text/xml")),se(1))}var wo={drawer:G("[data-md-toggle=drawer]"),search:G("[data-md-toggle=search]")};function Eo(e,t){wo[e].checked!==t&&wo[e].click()}function Tn(e){let t=wo[e];return b(t,"change").pipe(f(()=>t.checked),J(t.checked))}function os(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function is(){return R(b(window,"scroll",{passive:!0}),b(window,"resize",{passive:!0})).pipe(f(os),J(os()))}function as(){return{width:innerWidth,height:innerHeight}}function ss(){return b(window,"resize",{passive:!0}).pipe(f(as),J(as()))}function cs(){return re([is(),ss()]).pipe(f(([e,t])=>({offset:e,size:t})),se(1))}function Sn(e,{viewport$:t,header$:r}){let n=t.pipe(fe("size")),o=re([n,r]).pipe(f(()=>wt(e)));return re([r,t,o]).pipe(f(([{height:i},{offset:a,size:s},{x:c,y:l}])=>({offset:{x:a.x-c,y:a.y-l+i},size:s})))}var Ku=G("#__config"),mr=JSON.parse(Ku.textContent);mr.base=`${new URL(mr.base,Ye())}`;function Ue(){return mr}function X(e){return mr.features.includes(e)}function Bt(e,t){return typeof t!="undefined"?mr.translations[e].replace("#",t.toString()):mr.translations[e]}function ht(e,t=document){return G(`[data-md-component=${e}]`,t)}function Ee(e,t=document){return P(`[data-md-component=${e}]`,t)}function Bu(e){let t=G(".md-typeset > :first-child",e);return b(t,"click",{once:!0}).pipe(f(()=>G(".md-typeset",e)),f(r=>({hash:__md_hash(r.innerHTML)})))}function ls(e){if(!X("announce.dismiss")||!e.childElementCount)return y;if(!e.hidden){let t=G(".md-typeset",e);__md_hash(t.innerHTML)===__md_get("__announce")&&(e.hidden=!0)}return j(()=>{let t=new I;return t.subscribe(({hash:r})=>{e.hidden=!0,__md_set("__announce",r)}),Bu(e).pipe($(r=>t.next(r)),V(()=>t.complete()),f(r=>H({ref:e},r)))})}function Yu(e,{target$:t}){return t.pipe(f(r=>({hidden:r!==e})))}function us(e,t){let r=new I;return r.subscribe(({hidden:n})=>{e.hidden=n}),Yu(e,t).pipe($(n=>r.next(n)),V(()=>r.complete()),f(n=>H({ref:e},n)))}function To(e,t){return t==="inline"?A("div",{class:"md-tooltip md-tooltip--inline",id:e,role:"tooltip"},A("div",{class:"md-tooltip__inner md-typeset"})):A("div",{class:"md-tooltip",id:e,role:"tooltip"},A("div",{class:"md-tooltip__inner md-typeset"}))}function On(...e){return A("div",{class:"md-tooltip2",role:"dialog"},A("div",{class:"md-tooltip2__inner md-typeset"},e))}function ps(...e){return A("div",{class:"md-tooltip2",role:"tooltip"},A("div",{class:"md-tooltip2__inner md-typeset"},e))}function fs(e,t){if(t=t?`${t}_annotation_${e}`:void 0,t){let r=t?`#${t}`:void 0;return A("aside",{class:"md-annotation",tabIndex:0},To(t),A("a",{href:r,class:"md-annotation__index",tabIndex:-1},A("span",{"data-md-annotation-id":e})))}else return A("aside",{class:"md-annotation",tabIndex:0},To(t),A("span",{class:"md-annotation__index",tabIndex:-1},A("span",{"data-md-annotation-id":e})))}function ms(e){return A("button",{class:"md-code__button",title:Bt("clipboard.copy"),"data-clipboard-target":`#${e} > code`,"data-md-type":"copy"})}function ds(){return A("button",{class:"md-code__button",title:"Toggle line selection","data-md-type":"select"})}function hs(){return A("nav",{class:"md-code__nav"})}var Xu=_r(So());function bs(e){return A("ul",{class:"md-source__facts"},Object.entries(e).map(([t,r])=>A("li",{class:`md-source__fact md-source__fact--${t}`},typeof r=="number"?Li(r):r)))}function Oo(e){let t=`tabbed-control tabbed-control--${e}`;return A("div",{class:t,hidden:!0},A("button",{class:"tabbed-button",tabIndex:-1,"aria-hidden":"true"}))}function gs(e){return A("div",{class:"md-typeset__scrollwrap"},A("div",{class:"md-typeset__table"},e))}function Zu(e){var n;let t=Ue(),r=new URL(`../${e.version}/`,t.base);return A("li",{class:"md-version__item"},A("a",{href:`${r}`,class:"md-version__link"},e.title,((n=t.version)==null?void 0:n.alias)&&e.aliases.length>0&&A("span",{class:"md-version__alias"},e.aliases[0])))}function _s(e,t){var n;let r=Ue();return e=e.filter(o=>{var i;return!((i=o.properties)!=null&&i.hidden)}),A("div",{class:"md-version"},A("button",{class:"md-version__current","aria-label":Bt("select.version")},t.title,((n=r.version)==null?void 0:n.alias)&&t.aliases.length>0&&A("span",{class:"md-version__alias"},t.aliases[0])),A("ul",{class:"md-version__list"},e.map(Zu)))}var Qu=0;function ep(e,t=250){let r=re([ir(e),Ft(e,t)]).pipe(f(([o,i])=>o||i),ie()),n=j(()=>Ai(e)).pipe(oe(Ut),Lr(1),Ze(r),f(()=>Ci(e)));return r.pipe(Sr(o=>o),g(()=>re([r,n])),f(([o,i])=>({active:o,offset:i})),xe())}function Rr(e,t,r=250){let{content$:n,viewport$:o}=t,i=`__tooltip2_${Qu++}`;return j(()=>{let a=new I,s=new Un(!1);a.pipe(he(),ye(!1)).subscribe(s);let c=s.pipe(Tr(u=>Ve(+!u*250,Wn)),ie(),g(u=>u?n:y),$(u=>u.id=i),xe());re([a.pipe(f(({active:u})=>u)),c.pipe(g(u=>Ft(u,250)),J(!1))]).pipe(f(u=>u.some(p=>p))).subscribe(s);let l=s.pipe(L(u=>u),pe(c,o),f(([u,p,{size:d}])=>{let m=e.getBoundingClientRect(),h=m.width/2;if(p.role==="tooltip")return{x:h,y:8+m.height};if(m.y>=d.height/2){let{height:v}=Ae(p);return{x:h,y:-16-v}}else return{x:h,y:16+m.height}}));return re([c,a,l]).subscribe(([u,{offset:p},d])=>{u.style.setProperty("--md-tooltip-host-x",`${p.x}px`),u.style.setProperty("--md-tooltip-host-y",`${p.y}px`),u.style.setProperty("--md-tooltip-x",`${d.x}px`),u.style.setProperty("--md-tooltip-y",`${d.y}px`),u.classList.toggle("md-tooltip2--top",d.y<0),u.classList.toggle("md-tooltip2--bottom",d.y>=0)}),s.pipe(L(u=>u),pe(c,(u,p)=>p),L(u=>u.role==="tooltip")).subscribe(u=>{let p=Ae(G(":scope > *",u));u.style.setProperty("--md-tooltip-width",`${p.width}px`),u.style.setProperty("--md-tooltip-tail","0px")}),s.pipe(ie(),Ie(je),pe(c)).subscribe(([u,p])=>{p.classList.toggle("md-tooltip2--active",u)}),re([s.pipe(L(u=>u)),c]).subscribe(([u,p])=>{p.role==="dialog"?(e.setAttribute("aria-controls",i),e.setAttribute("aria-haspopup","dialog")):e.setAttribute("aria-describedby",i)}),s.pipe(L(u=>!u)).subscribe(()=>{e.removeAttribute("aria-controls"),e.removeAttribute("aria-describedby"),e.removeAttribute("aria-haspopup")}),ep(e,r).pipe($(u=>a.next(u)),V(()=>a.complete()),f(u=>H({ref:e},u)))})}function Ge(e,{viewport$:t},r=document.body){return Rr(e,{content$:new U(n=>{let o=e.title,i=ps(o);return n.next(i),e.removeAttribute("title"),r.append(i),()=>{i.remove(),e.setAttribute("title",o)}}),viewport$:t},0)}function tp(e,t){let r=j(()=>re([Hi(e),Ut(t)])).pipe(f(([{x:n,y:o},i])=>{let{width:a,height:s}=Ae(e);return{x:n-i.x+a/2,y:o-i.y+s/2}}));return ir(e).pipe(g(n=>r.pipe(f(o=>({active:n,offset:o})),Me(+!n||1/0))))}function ys(e,t,{target$:r}){let[n,o]=Array.from(e.children);return j(()=>{let i=new I,a=i.pipe(he(),ye(!0));return i.subscribe({next({offset:s}){e.style.setProperty("--md-tooltip-x",`${s.x}px`),e.style.setProperty("--md-tooltip-y",`${s.y}px`)},complete(){e.style.removeProperty("--md-tooltip-x"),e.style.removeProperty("--md-tooltip-y")}}),Et(e).pipe(Q(a)).subscribe(s=>{e.toggleAttribute("data-md-visible",s)}),R(i.pipe(L(({active:s})=>s)),i.pipe(Be(250),L(({active:s})=>!s))).subscribe({next({active:s}){s?e.prepend(n):n.remove()},complete(){e.prepend(n)}}),i.pipe(Xe(16,je)).subscribe(({active:s})=>{n.classList.toggle("md-tooltip--active",s)}),i.pipe(Lr(125,je),L(()=>!!e.offsetParent),f(()=>e.offsetParent.getBoundingClientRect()),f(({x:s})=>s)).subscribe({next(s){s?e.style.setProperty("--md-tooltip-0",`${-s}px`):e.style.removeProperty("--md-tooltip-0")},complete(){e.style.removeProperty("--md-tooltip-0")}}),b(o,"click").pipe(Q(a),L(s=>!(s.metaKey||s.ctrlKey))).subscribe(s=>{s.stopPropagation(),s.preventDefault()}),b(o,"mousedown").pipe(Q(a),pe(i)).subscribe(([s,{active:c}])=>{var l;if(s.button!==0||s.metaKey||s.ctrlKey)s.preventDefault();else if(c){s.preventDefault();let u=e.parentElement.closest(".md-annotation");u instanceof HTMLElement?u.focus():(l=xt())==null||l.blur()}}),r.pipe(Q(a),L(s=>s===n),It(125)).subscribe(()=>e.focus()),tp(e,t).pipe($(s=>i.next(s)),V(()=>i.complete()),f(s=>H({ref:e},s)))})}function rp(e){let t=Ue();if(e.tagName!=="CODE")return[e];let r=[".c",".c1",".cm"];if(t.annotate){let n=e.closest("[class|=language]");if(n)for(let o of Array.from(n.classList)){if(!o.startsWith("language-"))continue;let[,i]=o.split("-");i in t.annotate&&r.push(...t.annotate[i])}}return P(r.join(", "),e)}function np(e){let t=[];for(let r of rp(e)){let n=[],o=document.createNodeIterator(r,NodeFilter.SHOW_TEXT);for(let i=o.nextNode();i;i=o.nextNode())n.push(i);for(let i of n){let a;for(;a=/(\(\d+\))(!)?/.exec(i.textContent);){let[,s,c]=a;if(typeof c=="undefined"){let l=i.splitText(a.index);i=l.splitText(s.length),t.push(l)}else{i.textContent=s,t.push(i);break}}}}return t}function xs(e,t){t.append(...Array.from(e.childNodes))}function Ln(e,t,{target$:r,print$:n}){let o=t.closest("[id]"),i=o==null?void 0:o.id,a=new Map;for(let s of np(t)){let[,c]=s.textContent.match(/\((\d+)\)/);Le(`:scope > li:nth-child(${c})`,e)&&(a.set(c,fs(c,i)),s.replaceWith(a.get(c)))}return a.size===0?y:j(()=>{let s=new I,c=s.pipe(he(),ye(!0)),l=[];for(let[u,p]of a)l.push([G(".md-typeset",p),G(`:scope > li:nth-child(${u})`,e)]);return n.pipe(Q(c)).subscribe(u=>{e.hidden=!u,e.classList.toggle("md-annotation-list",u);for(let[p,d]of l)u?xs(p,d):xs(d,p)}),R(...[...a].map(([,u])=>ys(u,t,{target$:r}))).pipe(V(()=>s.complete()),xe())})}function ws(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return ws(t)}}function Es(e,t){return j(()=>{let r=ws(e);return typeof r!="undefined"?Ln(r,e,t):y})}var Ss=_r(Mo());var op=0,Ts=R(b(window,"keydown").pipe(f(()=>!0)),R(b(window,"keyup"),b(window,"contextmenu")).pipe(f(()=>!1))).pipe(J(!1),se(1));function Os(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return Os(t)}}function ip(e){return Re(e).pipe(f(({width:t})=>({scrollable:Mr(e).width>t})),fe("scrollable"))}function Ls(e,t){let{matches:r}=matchMedia("(hover)"),n=j(()=>{let o=new I,i=o.pipe(Gn(1));o.subscribe(({scrollable:m})=>{m&&r?e.setAttribute("tabindex","0"):e.removeAttribute("tabindex")});let a=[],s=e.closest("pre"),c=s.closest("[id]"),l=c?c.id:op++;s.id=`__code_${l}`;let u=[],p=e.closest(".highlight");if(p instanceof HTMLElement){let m=Os(p);if(typeof m!="undefined"&&(p.classList.contains("annotate")||X("content.code.annotate"))){let h=Ln(m,e,t);u.push(Re(p).pipe(Q(i),f(({width:v,height:x})=>v&&x),ie(),g(v=>v?h:y)))}}let d=P(":scope > span[id]",e);if(d.length&&(e.classList.add("md-code__content"),e.closest(".select")||X("content.code.select")&&!e.closest(".no-select"))){let m=+d[0].id.split("-").pop(),h=ds();a.push(h),X("content.tooltips")&&u.push(Ge(h,{viewport$}));let v=b(h,"click").pipe(Or(M=>!M,!1),$(()=>h.blur()),xe());v.subscribe(M=>{h.classList.toggle("md-code__button--active",M)});let x=me(d).pipe(oe(M=>Ft(M).pipe(f(O=>[M,O]))));v.pipe(g(M=>M?x:y)).subscribe(([M,O])=>{let N=Le(".hll.select",M);if(N&&!O)N.replaceWith(...Array.from(N.childNodes));else if(!N&&O){let ee=document.createElement("span");ee.className="hll select",ee.append(...Array.from(M.childNodes).slice(1)),M.append(ee)}});let w=me(d).pipe(oe(M=>b(M,"mousedown").pipe($(O=>O.preventDefault()),f(()=>M)))),E=v.pipe(g(M=>M?w:y),pe(Ts),f(([M,O])=>{var ee;let N=d.indexOf(M)+m;if(O===!1)return[N,N];{let le=P(".hll",e).map(ce=>d.indexOf(ce.parentElement)+m);return(ee=window.getSelection())==null||ee.removeAllRanges(),[Math.min(N,...le),Math.max(N,...le)]}})),_=_o(y).pipe(L(M=>M.startsWith(`__codelineno-${l}-`)));_.subscribe(M=>{let[,,O]=M.split("-"),N=O.split(":").map(le=>+le-m+1);N.length===1&&N.push(N[0]);for(let le of P(".hll:not(.select)",e))le.replaceWith(...Array.from(le.childNodes));let ee=d.slice(N[0]-1,N[1]);for(let le of ee){let ce=document.createElement("span");ce.className="hll",ce.append(...Array.from(le.childNodes).slice(1)),le.append(ce)}}),_.pipe(Me(1),Ie(ge)).subscribe(M=>{if(M.includes(":")){let O=document.getElementById(M.split(":")[0]);O&&setTimeout(()=>{let N=O,ee=-64;for(;N!==document.body;)ee+=N.offsetTop,N=N.offsetParent;window.scrollTo({top:ee})},1)}});let be=me(P('a[href^="#__codelineno"]',p)).pipe(oe(M=>b(M,"click").pipe($(O=>O.preventDefault()),f(()=>M)))).pipe(Q(i),pe(Ts),f(([M,O])=>{let ee=+G(`[id="${M.hash.slice(1)}"]`).parentElement.id.split("-").pop();if(O===!1)return[ee,ee];{let le=P(".hll",e).map(ce=>+ce.parentElement.id.split("-").pop());return[Math.min(ee,...le),Math.max(ee,...le)]}}));R(E,be).subscribe(M=>{let O=`#__codelineno-${l}-`;M[0]===M[1]?O+=M[0]:O+=`${M[0]}:${M[1]}`,history.replaceState({},"",O),window.dispatchEvent(new HashChangeEvent("hashchange",{newURL:window.location.origin+window.location.pathname+O,oldURL:window.location.href}))})}if(Ss.default.isSupported()&&(e.closest(".copy")||X("content.code.copy")&&!e.closest(".no-copy"))){let m=ms(s.id);a.push(m),X("content.tooltips")&&u.push(Ge(m,{viewport$}))}if(a.length){let m=hs();m.append(...a),s.insertBefore(m,e)}return ip(e).pipe($(m=>o.next(m)),V(()=>o.complete()),f(m=>H({ref:e},m)),Rt(R(...u).pipe(Q(i))))});return X("content.lazy")?Et(e).pipe(L(o=>o),Me(1),g(()=>n)):n}function ap(e,{target$:t,print$:r}){let n=!0;return R(t.pipe(f(o=>o.closest("details:not([open])")),L(o=>e===o),f(()=>({action:"open",reveal:!0}))),r.pipe(L(o=>o||!n),$(()=>n=e.open),f(o=>({action:o?"open":"close"}))))}function Ms(e,t){return j(()=>{let r=new I;return r.subscribe(({action:n,reveal:o})=>{e.toggleAttribute("open",n==="open"),o&&e.scrollIntoView()}),ap(e,t).pipe($(n=>r.next(n)),V(()=>r.complete()),f(n=>H({ref:e},n)))})}var ks=0,As=new Map;function sp(e){let t=document.createElement("h3");t.innerHTML=e.innerHTML;let r=[t],n=e.nextElementSibling;for(;n&&!(n instanceof HTMLHeadingElement);)r.push(n.cloneNode(!0)),n=n.nextElementSibling;return r}function cp(e,t){for(let r of P("[href], [src]",e))for(let n of["href","src"]){let o=r.getAttribute(n);if(o&&!/^(?:[a-z]+:)?\/\//i.test(o)){r[n]=new URL(r.getAttribute(n),t).toString();break}}for(let r of P("[name^=__], [for]",e))for(let n of["id","for","name"]){let o=r.getAttribute(n);o&&r.setAttribute(n,`${o}$preview_${ks}`)}return ks++,Y(e)}function lp(e){let t=As.get(e.toString());return t?Y(t):En(e).pipe(g(r=>cp(r,e)),f(r=>(As.set(e.toString(),r),r)))}function Cs(e,t){let{sitemap$:r}=t;if(!(e instanceof HTMLAnchorElement))return y;if(!(X("navigation.instant.preview")||e.hasAttribute("data-preview")))return y;e.removeAttribute("title");let n=re([ir(e),Ft(e).pipe(ke(1))]).pipe(f(([i,a])=>i||a),ie(),L(i=>i));return $t([r,n]).pipe(g(([i])=>{let a=new URL(e.href);return a.search=a.hash="",i.has(`${a}`)?Y(a):y}),g(i=>lp(i)),g(i=>{let a=e.hash?`article [id="${decodeURIComponent(e.hash.slice(1))}"]`:"article h1",s=Le(a,i);return typeof s=="undefined"?y:Y(sp(s))})).pipe(g(i=>{let a=new U(s=>{let c=On(...i);return s.next(c),document.body.append(c),()=>c.remove()});return Rr(e,H({content$:a},t))}))}var Hs=".node circle,.node ellipse,.node path,.node polygon,.node rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}marker{fill:var(--md-mermaid-edge-color)!important}.edgeLabel .label rect{fill:#0000}.flowchartTitleText{fill:var(--md-mermaid-label-fg-color)}.label{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.label foreignObject{line-height:normal;overflow:visible}.label div .edgeLabel{color:var(--md-mermaid-label-fg-color)}.edgeLabel,.edgeLabel p,.label div .edgeLabel{background-color:var(--md-mermaid-label-bg-color)}.edgeLabel,.edgeLabel p{fill:var(--md-mermaid-label-bg-color);color:var(--md-mermaid-edge-color)}.edgePath .path,.flowchart-link{stroke:var(--md-mermaid-edge-color)}.edgePath .arrowheadPath{fill:var(--md-mermaid-edge-color);stroke:none}.cluster rect{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}.cluster span{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}g #flowchart-circleEnd,g #flowchart-circleStart,g #flowchart-crossEnd,g #flowchart-crossStart,g #flowchart-pointEnd,g #flowchart-pointStart{stroke:none}.classDiagramTitleText{fill:var(--md-mermaid-label-fg-color)}g.classGroup line,g.classGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.classGroup text{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.classLabel .box{fill:var(--md-mermaid-label-bg-color);background-color:var(--md-mermaid-label-bg-color);opacity:1}.classLabel .label{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.node .divider{stroke:var(--md-mermaid-node-fg-color)}.relation{stroke:var(--md-mermaid-edge-color)}.cardinality{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.cardinality text{fill:inherit!important}defs marker.marker.composition.class path,defs marker.marker.dependency.class path,defs marker.marker.extension.class path{fill:var(--md-mermaid-edge-color)!important;stroke:var(--md-mermaid-edge-color)!important}defs marker.marker.aggregation.class path{fill:var(--md-mermaid-label-bg-color)!important;stroke:var(--md-mermaid-edge-color)!important}.statediagramTitleText{fill:var(--md-mermaid-label-fg-color)}g.stateGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.stateGroup .state-title{fill:var(--md-mermaid-label-fg-color)!important;font-family:var(--md-mermaid-font-family)}g.stateGroup .composit{fill:var(--md-mermaid-label-bg-color)}.nodeLabel,.nodeLabel p{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}a .nodeLabel{text-decoration:underline}.node circle.state-end,.node circle.state-start,.start-state{fill:var(--md-mermaid-edge-color);stroke:none}.end-state-inner,.end-state-outer{fill:var(--md-mermaid-edge-color)}.end-state-inner,.node circle.state-end{stroke:var(--md-mermaid-label-bg-color)}.transition{stroke:var(--md-mermaid-edge-color)}[id^=state-fork] rect,[id^=state-join] rect{fill:var(--md-mermaid-edge-color)!important;stroke:none!important}.statediagram-cluster.statediagram-cluster .inner{fill:var(--md-default-bg-color)}.statediagram-cluster rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.statediagram-state rect.divider{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}defs #statediagram-barbEnd{stroke:var(--md-mermaid-edge-color)}[id^=entity] path,[id^=entity] rect{fill:var(--md-default-bg-color)}.relationshipLine{stroke:var(--md-mermaid-edge-color)}defs .marker.oneOrMore.er *,defs .marker.onlyOne.er *,defs .marker.zeroOrMore.er *,defs .marker.zeroOrOne.er *{stroke:var(--md-mermaid-edge-color)!important}text:not([class]):last-child{fill:var(--md-mermaid-label-fg-color)}.actor{fill:var(--md-mermaid-sequence-actor-bg-color);stroke:var(--md-mermaid-sequence-actor-border-color)}text.actor>tspan{fill:var(--md-mermaid-sequence-actor-fg-color);font-family:var(--md-mermaid-font-family)}line{stroke:var(--md-mermaid-sequence-actor-line-color)}.actor-man circle,.actor-man line{fill:var(--md-mermaid-sequence-actorman-bg-color);stroke:var(--md-mermaid-sequence-actorman-line-color)}.messageLine0,.messageLine1{stroke:var(--md-mermaid-sequence-message-line-color)}.note{fill:var(--md-mermaid-sequence-note-bg-color);stroke:var(--md-mermaid-sequence-note-border-color)}.loopText,.loopText>tspan,.messageText,.noteText>tspan{stroke:none;font-family:var(--md-mermaid-font-family)!important}.messageText{fill:var(--md-mermaid-sequence-message-fg-color)}.loopText,.loopText>tspan{fill:var(--md-mermaid-sequence-loop-fg-color)}.noteText>tspan{fill:var(--md-mermaid-sequence-note-fg-color)}#arrowhead path{fill:var(--md-mermaid-sequence-message-line-color);stroke:none}.loopLine{fill:var(--md-mermaid-sequence-loop-bg-color);stroke:var(--md-mermaid-sequence-loop-border-color)}.labelBox{fill:var(--md-mermaid-sequence-label-bg-color);stroke:none}.labelText,.labelText>span{fill:var(--md-mermaid-sequence-label-fg-color);font-family:var(--md-mermaid-font-family)}.sequenceNumber{fill:var(--md-mermaid-sequence-number-fg-color)}rect.rect{fill:var(--md-mermaid-sequence-box-bg-color);stroke:none}rect.rect+text.text{fill:var(--md-mermaid-sequence-box-fg-color)}defs #sequencenumber{fill:var(--md-mermaid-sequence-number-bg-color)!important}";var ko,pp=0;function fp(){return typeof mermaid=="undefined"||mermaid instanceof Element?ar("https://unpkg.com/mermaid@11/dist/mermaid.min.js"):Y(void 0)}function $s(e){return e.classList.remove("mermaid"),ko||(ko=fp().pipe($(()=>mermaid.initialize({startOnLoad:!1,themeCSS:Hs,sequence:{actorFontSize:"16px",messageFontSize:"16px",noteFontSize:"16px"}})),f(()=>{}),se(1))),ko.subscribe(()=>Uo(null,null,function*(){e.classList.add("mermaid");let t=`__mermaid_${pp++}`,r=A("div",{class:"mermaid"}),n=e.textContent,{svg:o,fn:i}=yield mermaid.render(t,n),a=r.attachShadow({mode:"closed"});a.innerHTML=o,e.replaceWith(r),i==null||i(a)})),ko.pipe(f(()=>({ref:e})))}var Ps=A("table");function Is(e){return e.replaceWith(Ps),Ps.replaceWith(gs(e)),Y({ref:e})}function mp(e){let t=e.find(r=>r.checked)||e[0];return R(...e.map(r=>b(r,"change").pipe(f(()=>G(`label[for="${r.id}"]`))))).pipe(J(G(`label[for="${t.id}"]`)),f(r=>({active:r})))}function Rs(e,{viewport$:t,target$:r}){let n=G(".tabbed-labels",e),o=P(":scope > input",e),i=Oo("prev");e.append(i);let a=Oo("next");return e.append(a),j(()=>{let s=new I,c=s.pipe(he(),ye(!0));re([s,Re(e),Et(e)]).pipe(Q(c),Xe(1,je)).subscribe({next([{active:l},u]){let p=wt(l),{width:d}=Ae(l);e.style.setProperty("--md-indicator-x",`${p.x}px`),e.style.setProperty("--md-indicator-width",`${d}px`);let m=ln(n);(p.xm.x+u.width)&&n.scrollTo({left:Math.max(0,p.x-16),behavior:"smooth"})},complete(){e.style.removeProperty("--md-indicator-x"),e.style.removeProperty("--md-indicator-width")}}),re([Ut(n),Re(n)]).pipe(Q(c)).subscribe(([l,u])=>{let p=Mr(n);i.hidden=l.x<16,a.hidden=l.x>p.width-u.width-16}),R(b(i,"click").pipe(f(()=>-1)),b(a,"click").pipe(f(()=>1))).pipe(Q(c)).subscribe(l=>{let{width:u}=Ae(n);n.scrollBy({left:u*l,behavior:"smooth"})}),r.pipe(Q(c),L(l=>o.includes(l))).subscribe(l=>l.click()),n.classList.add("tabbed-labels--linked");for(let l of o){let u=G(`label[for="${l.id}"]`);u.replaceChildren(A("a",{href:`#${u.htmlFor}`,tabIndex:-1},...Array.from(u.childNodes))),b(u.firstElementChild,"click").pipe(Q(c),L(p=>!(p.metaKey||p.ctrlKey)),$(p=>{p.preventDefault(),p.stopPropagation()})).subscribe(()=>{history.replaceState({},"",`#${u.htmlFor}`),u.click()})}return X("content.tabs.link")&&s.pipe(ke(1),pe(t)).subscribe(([{active:l},{offset:u}])=>{let p=l.innerText.trim();if(l.hasAttribute("data-md-switching"))l.removeAttribute("data-md-switching");else{let d=e.offsetTop-u.y;for(let h of P("[data-tabs]"))for(let v of P(":scope > input",h)){let x=G(`label[for="${v.id}"]`);if(x!==l&&x.innerText.trim()===p){x.setAttribute("data-md-switching",""),v.click();break}}window.scrollTo({top:e.offsetTop-d});let m=__md_get("__tabs")||[];__md_set("__tabs",[...new Set([p,...m])])}}),s.pipe(Q(c)).subscribe(()=>{for(let l of P("audio, video",e))l.offsetWidth&&l.autoplay?l.play().catch(()=>{}):l.pause()}),mp(o).pipe($(l=>s.next(l)),V(()=>s.complete()),f(l=>H({ref:e},l)))}).pipe(Ht(ge))}function js(e,t){let{viewport$:r,target$:n,print$:o}=t;return R(...P(".annotate:not(.highlight)",e).map(i=>Es(i,{target$:n,print$:o})),...P("pre:not(.mermaid) > code",e).map(i=>Ls(i,{target$:n,print$:o})),...P("a",e).map(i=>Cs(i,t)),...P("pre.mermaid",e).map(i=>$s(i)),...P("table:not([class])",e).map(i=>Is(i)),...P("details",e).map(i=>Ms(i,{target$:n,print$:o})),...P("[data-tabs]",e).map(i=>Rs(i,{viewport$:r,target$:n})),...P("[title]:not([data-preview])",e).filter(()=>X("content.tooltips")).map(i=>Ge(i,{viewport$:r})),...P(".footnote-ref",e).filter(()=>X("content.footnote.tooltips")).map(i=>Rr(i,{content$:new U(a=>{let s=new URL(i.href).hash.slice(1),c=Array.from(document.getElementById(s).cloneNode(!0).children),l=On(...c);return a.next(l),document.body.append(l),()=>l.remove()}),viewport$:r})))}function dp(e,{alert$:t}){return t.pipe(g(r=>R(Y(!0),Y(!1).pipe(It(2e3))).pipe(f(n=>({message:r,active:n})))))}function Fs(e,t){let r=G(".md-typeset",e);return j(()=>{let n=new I;return n.subscribe(({message:o,active:i})=>{e.classList.toggle("md-dialog--active",i),r.textContent=o}),dp(e,t).pipe($(o=>n.next(o)),V(()=>n.complete()),f(o=>H({ref:e},o)))})}function hp({viewport$:e}){if(!X("header.autohide"))return Y(!1);let t=e.pipe(f(({offset:{y:o}})=>o),Pt(2,1),f(([o,i])=>[oMath.abs(i-o.y)>100),f(([,[o]])=>o),ie()),n=Tn("search");return re([e,n]).pipe(f(([{offset:o},i])=>o.y>400&&!i),ie(),g(o=>o?r:Y(!1)),J(!1))}function Us(e,t){return j(()=>re([Re(e),hp(t)])).pipe(f(([{height:r},n])=>({height:r,hidden:n})),ie((r,n)=>r.height===n.height&&r.hidden===n.hidden),se(1))}function Ns(e,{viewport$:t,header$:r,main$:n}){return j(()=>{let o=new I,i=o.pipe(he(),ye(!0));o.pipe(fe("active"),Ze(r)).subscribe(([{active:s},{hidden:c}])=>{e.classList.toggle("md-header--shadow",s&&!c),e.hidden=c});let a=me(P("[title]",e)).pipe(L(()=>X("content.tooltips")),oe(s=>Ge(s,{viewport$:t})));return n.subscribe(o),r.pipe(Q(i),f(s=>H({ref:e},s)),Rt(a.pipe(Q(i))))})}function vp(e,{viewport$:t,header$:r}){return Sn(e,{viewport$:t,header$:r}).pipe(f(({offset:{y:n}})=>{let{height:o}=Ae(e);return{active:o>0&&n>=o}}),fe("active"))}function Ds(e,t){return j(()=>{let r=new I;r.subscribe({next({active:o}){e.classList.toggle("md-header__title--active",o)},complete(){e.classList.remove("md-header__title--active")}});let n=Le(".md-content h1");return typeof n=="undefined"?y:vp(n,t).pipe($(o=>r.next(o)),V(()=>r.complete()),f(o=>H({ref:e},o)))})}function Ws(e,{viewport$:t,header$:r}){let n=r.pipe(f(({height:i})=>i),ie()),o=n.pipe(g(()=>Re(e).pipe(f(({height:i})=>({top:e.offsetTop,bottom:e.offsetTop+i})),fe("bottom"))));return re([n,o,t]).pipe(f(([i,{top:a,bottom:s},{offset:{y:c},size:{height:l}}])=>(l=Math.max(0,l-Math.max(0,a-c,i)-Math.max(0,l+c-s)),{offset:a-i,height:l,active:a-i<=c})),ie((i,a)=>i.offset===a.offset&&i.height===a.height&&i.active===a.active))}function bp(e){let t=__md_get("__palette")||{index:e.findIndex(n=>matchMedia(n.getAttribute("data-md-color-media")).matches)},r=Math.max(0,Math.min(t.index,e.length-1));return Y(...e).pipe(oe(n=>b(n,"change").pipe(f(()=>n))),J(e[r]),f(n=>({index:e.indexOf(n),color:{media:n.getAttribute("data-md-color-media"),scheme:n.getAttribute("data-md-color-scheme"),primary:n.getAttribute("data-md-color-primary"),accent:n.getAttribute("data-md-color-accent")}})),se(1))}function Vs(e){let t=P("input",e),r=A("meta",{name:"theme-color"});document.head.appendChild(r);let n=A("meta",{name:"color-scheme"});document.head.appendChild(n);let o=Ir("(prefers-color-scheme: light)");return j(()=>{let i=new I;return i.subscribe(a=>{if(document.body.setAttribute("data-md-color-switching",""),a.color.media==="(prefers-color-scheme)"){let s=matchMedia("(prefers-color-scheme: light)"),c=document.querySelector(s.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");a.color.scheme=c.getAttribute("data-md-color-scheme"),a.color.primary=c.getAttribute("data-md-color-primary"),a.color.accent=c.getAttribute("data-md-color-accent")}for(let[s,c]of Object.entries(a.color))document.body.setAttribute(`data-md-color-${s}`,c);for(let s=0;sa.key==="Enter"),pe(i,(a,s)=>s)).subscribe(({index:a})=>{a=(a+1)%t.length,t[a].click(),t[a].focus()}),i.pipe(f(()=>{let a=ht("header"),s=window.getComputedStyle(a);return n.content=s.colorScheme,s.backgroundColor.match(/\d+/g).map(c=>(+c).toString(16).padStart(2,"0")).join("")})).subscribe(a=>r.content=`#${a}`),i.pipe(Ie(ge)).subscribe(()=>{document.body.removeAttribute("data-md-color-switching")}),bp(t).pipe(Q(o.pipe(ke(1))),jt(),$(a=>i.next(a)),V(()=>i.complete()),f(a=>H({ref:e},a)))})}function zs(e,{progress$:t}){return j(()=>{let r=new I;return r.subscribe(({value:n})=>{e.style.setProperty("--md-progress-value",`${n}`)}),t.pipe($(n=>r.next({value:n})),V(()=>r.complete()),f(n=>({ref:e,value:n})))})}var qs='.v u{text-decoration:underline!important;text-decoration-style:wavy!important;text-decoration-thickness:1px!important}.p{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);background-color:rgba(var(--color-backdrop)/var(--alpha-lighter));cursor:pointer;height:100%;pointer-events:auto;position:absolute;transition:opacity .25s;width:100%}.p.m{opacity:0;pointer-events:none;transition:opacity .35s}.r{align-items:center;background-color:initial;border:none;border-radius:var(--space-2);cursor:pointer;display:flex;flex-shrink:0;font-family:var(--font-family);height:36px;justify-content:center;outline:none;padding:0;position:relative;transition:background-color .25s,color .25s;width:36px;z-index:1}.r svg{stroke:rgb(var(--color-foreground));height:18px;opacity:.5;width:18px}.r:before{background-color:rgb(var(--color-background-subtle));border-radius:var(--border-radius-2);content:"";inset:0;opacity:0;position:absolute;transform:scale(.75);transition:transform 125ms,opacity 125ms;z-index:0}.r:hover:before{opacity:1;transform:scale(1)}.r.c{cursor:auto}.r.c:before{display:none}.n{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);background-color:rgba(var(--color-background)/var(--alpha-light));border-radius:var(--space-3);box-shadow:0 0 60px #0000000d;display:flex;height:480px;overflow:hidden;pointer-events:auto;position:absolute;transition:transform .25s cubic-bezier(.16,1,.3,1),opacity .25s;width:640px}.n.l{opacity:0;pointer-events:none;transform:scale(1.1);transition:transform .25s .15s,opacity .15s}@media (max-width:680px){.n{border-radius:0;height:100%;width:100%}}.u{display:flex;flex-basis:min-content;flex-direction:column;flex-grow:1;flex-shrink:0}@keyframes d{0%{transform:scale(0)}50%{transform:scale(1.2)}to{transform:scale(1)}}.y{animation:d .25s ease-in-out;background:var(--color-highlight);border-radius:100%;color:#fff;font-size:8px;font-weight:700;height:12px;padding-top:1px;position:absolute;right:4px;top:4px;width:12px}.i{background-color:rgb(var(--color-background-subtle)/var(--alpha-lighter));flex-shrink:0;overflow:scroll;position:relative;transition:width .35s cubic-bezier(.16,1,.3,1),opacity .25s;width:200px}.i>*{transform:translate(0);transition:transform .25s cubic-bezier(.16,1,.3,1)}.i.l{opacity:0;width:0}.i.l>*{transform:translate(-48px)}@media (max-width:680px){.i{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);background-color:rgba(var(--color-background-subtle)/var(--alpha-light));box-shadow:0 0 60px #00000026;height:100%;position:absolute;right:0;top:0}}.w{border-bottom:1px solid rgb(var(--color-foreground)/var(--alpha-lightest));display:flex;gap:var(--space-1);padding:var(--space-2)}.k{-webkit-overflow-scrolling:touch;overflow:auto;overscroll-behavior:contain}.z{padding:8px 10px}.X{color:rgb(var(--color-foreground)/var(--alpha-light));padding:var(--space-2);position:absolute;width:200px}.X,.j{display:flex;flex-direction:column}.j{gap:2px;list-style:none;padding:0}.F,.j{margin:0}.F{font-size:16px;font-weight:400}.F,.I{padding:8px}.I{font-size:14px;margin:4px 0 0;opacity:.5}.I,.o{font-size:12px}.o{cursor:pointer;display:flex;padding:4px 8px;position:relative}.o:before{background-color:var(--color-highlight-transparent);border-radius:var(--space-1);content:"";inset:0;opacity:0;position:absolute;transform:scale(.75);transition:transform 125ms,opacity 125ms;z-index:0}.o.g:before,.o:hover:before{opacity:1;transform:scale(1)}.o.g,.o:hover{color:var(--color-highlight)}.R{flex-grow:1}.R,.q{position:relative}.q{font-weight:700}.f{flex-grow:1}.f input{background:#0000;border:none;color:rgb(var(--color-foreground));font-family:var(--font-family);font-size:16px;height:100%;letter-spacing:-.25px;outline:none;width:100%}.b{color:rgb(var(--color-foreground)/var(--alpha-light));display:flex;flex-direction:column;gap:2px;line-height:1.3;list-style:none;margin:var(--space-2);margin-top:0;padding:0}.A,.b li{margin:0}.A{color:rgb(var(--color-foreground)/var(--alpha-lighter));font-size:12px;margin-top:var(--space-2);padding:0 18px}.a{border-radius:var(--space-2);color:inherit;cursor:pointer;display:flex;flex-direction:row;flex-grow:1;padding:8px 10px;position:relative;text-decoration:none}.a:before{background-color:rgb(var(--color-background-subtle));border-radius:var(--border-radius-2);content:"";display:block;inset:0;opacity:0;position:absolute;transform:scale(.9);transition:transform 125ms,opacity 125ms;z-index:0}@media (pointer:fine){.a.h:before,.a:hover:before{opacity:1;transform:scale(1)}}.a mark{background:#0000;color:var(--color-highlight)}.a u{background-color:var(--color-highlight-transparent);border-radius:2px;box-shadow:0 0 0 1px var(--color-highlight-transparent);text-decoration:none}.B{flex-grow:1}.s{margin-right:-8px;opacity:0;position:relative;transform:translate(-2px);transition:transform 125ms,opacity 125ms;z-index:0}@media (pointer:fine){.h>.s,:hover>.s{opacity:1;transform:none}}.x{font-size:14px;margin:0;position:relative}.x code{background:rgb(var(--color-background-subtle));border-radius:var(--space-1);font-size:13px;padding:2px 4px}.t{color:rgb(var(--color-foreground)/var(--alpha-lighter));display:inline-flex;flex-wrap:wrap;font-size:12px;gap:var(--space-1);list-style:none;margin:0;padding:0;position:relative}.t li{white-space:nowrap}.t li:after{content:"/";display:inline;margin-left:var(--space-1)}.t li:last-child:after{content:"";display:none}.e{--space-1:4px;--space-2:calc(var(--space-1)*2);--space-3:calc(var(--space-2)*2);--space-4:calc(var(--space-3)*2);--space-5:calc(var(--space-4)*2);--alpha-light:.7;--alpha-lighter:.54;--alpha-lightest:.1;--color-highlight:var(--md-accent-fg-color,#526cfe);--color-highlight-transparent:var(--md-accent-fg-color--transparent,#526cfe1a);--border-radius-1:var(--space-1);--border-radius-2:var(--space-2);--border-radius-3:calc(var(--space-1) + var(--space-2));--font-family:var(--md-text-font-family,Inter,Roboto Flex,system-ui,sans-serif);--font-size:16px;--line-height:1.5;--letter-spacing:-.5px;-webkit-font-smoothing:antialiased;align-items:center;display:flex;font-family:var(--font-family);font-size:var(--font-size);height:100vh;justify-content:center;letter-spacing:var(--letter-spacing);line-height:var(--line-height);pointer-events:none;position:absolute;width:100vw}@media (pointer:coarse){.e{height:-webkit-fill-available}}.e *,.e :after,.e :before{box-sizing:border-box}';function Ks(e,{index$:t}){let r=Ue(),n=document.createElement("div");document.body.appendChild(n),n.style.position="fixed",n.style.height="100%",n.style.top="0",n.style.zIndex="4";let o=n.attachShadow({mode:"open"});o.appendChild(A("style",{},qs.toString()));try{Ya(r.search,{highlight:r.features.includes("search.highlight")}),me(t).subscribe(i=>{for(let a of i.items)a.location=new URL(a.location,r.base).toString();Ja(i,o)}),b(e,"click").subscribe(()=>{go()}),Tn("search").pipe(ke(1)).subscribe(()=>go())}catch(i){e.hidden=!0;let a=G("label[for=__search]");a.hidden=!0}return Ke}var Bs=_r(So());function Ys(e,{index$:t,location$:r}){return re([t,r.pipe(J(Ye()),L(n=>!!n.searchParams.get("h")))]).pipe(f(([n,o])=>_p(n.config)(o.searchParams.get("h"))),f(n=>{var a;let o=new Map,i=document.createNodeIterator(e,NodeFilter.SHOW_TEXT);for(let s=i.nextNode();s;s=i.nextNode())if((a=s.parentElement)!=null&&a.offsetHeight){let c=s.textContent,l=n(c);l.length>c.length&&o.set(s,l)}for(let[s,c]of o){let{childNodes:l}=A("span",null,c);s.replaceWith(...Array.from(l))}return{ref:e,nodes:o}}))}function _p(e){let t=e.separator.split("|").map(o=>o.replace(/(\(\?[!=<][^)]+\))/g,"").length===0?"\uFFFD":o).join("|"),r=new RegExp(t,"img"),n=(o,i,a)=>`${i}${a}`;return o=>{o=o.replace(/\s+/g," ").replace(/&/g,"&").trim();let i=new RegExp(`(^|${e.separator}|)(${o.split(r).map(a=>a.replace(/[|\\{}()[\]^$+*?.-]/g,"\\$&")).filter(a=>a.length>0).join("|")})`,"img");return a=>(0,Bs.default)(a).replace(i,n).replace(/<\/mark>(\s+)]*>/img,"$1")}}function yp(e,{viewport$:t,main$:r}){let n=e.closest(".md-grid"),o=n.offsetTop-n.parentElement.offsetTop;return re([r,t]).pipe(f(([{offset:i,height:a},{offset:{y:s}}])=>(a=a+Math.min(o,Math.max(0,s-i))-o,{height:a,locked:s>=i+o})),ie((i,a)=>i.height===a.height&&i.locked===a.locked))}function Ao(e,n){var o=n,{header$:t}=o,r=gr(o,["header$"]);let i=G(".md-sidebar__scrollwrap",e),{y:a}=wt(i);return j(()=>{let s=new I,c=s.pipe(he(),ye(!0)),l=s.pipe(Xe(0,je));return l.pipe(pe(t)).subscribe({next([{height:u},{height:p}]){i.style.height=`${u-2*a}px`,e.style.top=`${p}px`},complete(){i.style.height="",e.style.top=""}}),l.pipe(Sr()).subscribe(()=>{for(let u of P(".md-nav__link--active[href]",e)){if(!u.clientHeight)continue;let p=u.closest(".md-sidebar__scrollwrap");if(typeof p!="undefined"){let d=u.offsetTop-p.offsetTop,{height:m}=Ae(p);p.scrollTo({top:d-m/2})}}}),me(P("label[tabindex]",e)).pipe(oe(u=>b(u,"click").pipe(Ie(ge),f(()=>u),Q(c)))).subscribe(u=>{let p=G(`[id="${u.htmlFor}"]`);G(`[aria-labelledby="${u.id}"]`).setAttribute("aria-expanded",`${p.checked}`)}),X("content.tooltips")&&me(P("abbr[title]",e)).pipe(oe(u=>Ge(u,{viewport$})),Q(c)).subscribe(),yp(e,r).pipe($(u=>s.next(u)),V(()=>s.complete()),f(u=>H({ref:e},u)))})}function Gs(e,t){if(typeof t!="undefined"){let r=`https://api.github.com/repos/${e}/${t}`;return $t(et(`${r}/releases/latest`).pipe(_e(()=>y),f(n=>({version:n.tag_name})),ot({})),et(r).pipe(_e(()=>y),f(n=>({stars:n.stargazers_count,forks:n.forks_count})),ot({}))).pipe(f(([n,o])=>H(H({},n),o)))}else{let r=`https://api.github.com/users/${e}`;return et(r).pipe(f(n=>({repositories:n.public_repos})),ot({}))}}function Js(e,t){let r=`https://${e}/api/v4/projects/${encodeURIComponent(t)}`;return $t(et(`${r}/releases/permalink/latest`).pipe(_e(()=>y),f(({tag_name:n})=>({version:n})),ot({})),et(r).pipe(_e(()=>y),f(({star_count:n,forks_count:o})=>({stars:n,forks:o})),ot({}))).pipe(f(([n,o])=>H(H({},n),o)))}function Xs(e){let t=e.match(/^.+github\.com\/([^/]+)\/?([^/]+)?/i);if(t){let[,r,n]=t;return Gs(r,n)}if(t=e.match(/^.+?([^/]*gitlab[^/]+)\/(.+?)\/?$/i),t){let[,r,n]=t;return Js(r,n)}return y}var xp;function wp(e){return xp||(xp=j(()=>{let t=__md_get("__source",sessionStorage);if(t)return Y(t);if(Ee("consent").length){let n=__md_get("__consent");if(!(n&&n.github))return y}return Xs(e.href).pipe($(n=>__md_set("__source",n,sessionStorage)))}).pipe(_e(()=>y),L(t=>Object.keys(t).length>0),f(t=>({facts:t})),se(1)))}function Zs(e){let t=G(":scope > :last-child",e);return j(()=>{let r=new I;return r.subscribe(({facts:n})=>{t.appendChild(bs(n)),t.classList.add("md-source__repository--active")}),wp(e).pipe($(n=>r.next(n)),V(()=>r.complete()),f(n=>H({ref:e},n)))})}function Ep(e,{viewport$:t,header$:r}){return Re(document.body).pipe(g(()=>Sn(e,{header$:r,viewport$:t})),f(({offset:{y:n}})=>({hidden:n>=10})),fe("hidden"))}function Qs(e,t){return j(()=>{let r=new I;return r.subscribe({next({hidden:n}){e.hidden=n},complete(){e.hidden=!1}}),(X("navigation.tabs.sticky")?Y({hidden:!1}):Ep(e,t)).pipe($(n=>r.next(n)),V(()=>r.complete()),f(n=>H({ref:e},n)))})}function Tp(e,{viewport$:t,header$:r}){let n=new Map,o=P(".md-nav__link",e);for(let s of o){let c=decodeURIComponent(s.hash.substring(1)),l=Le(`[id="${c}"]`);typeof l!="undefined"&&n.set(s,l)}let i=r.pipe(fe("height"),f(({height:s})=>{let c=ht("main"),l=G(":scope > :first-child",c);return s+.9*(l.offsetTop-c.offsetTop)}),xe());return Re(document.body).pipe(fe("height"),g(s=>j(()=>{let c=[];return Y([...n].reduce((l,[u,p])=>{for(;c.length&&n.get(c[c.length-1]).tagName>=p.tagName;)c.pop();let d=p.offsetTop;for(;!d&&p.parentElement;)p=p.parentElement,d=p.offsetTop;let m=p.offsetParent;for(;m;m=m.offsetParent)d+=m.offsetTop;return l.set([...c=[...c,u]].reverse(),d)},new Map))}).pipe(f(c=>new Map([...c].sort(([,l],[,u])=>l-u))),Ze(i),g(([c,l])=>t.pipe(Or(([u,p],{offset:{y:d},size:m})=>{let h=d+m.height>=Math.floor(s.height);for(;p.length;){let[,v]=p[0];if(v-l=d&&!h)p=[u.pop(),...p];else break}return[u,p]},[[],[...c]]),ie((u,p)=>u[0]===p[0]&&u[1]===p[1])))))).pipe(f(([s,c])=>({prev:s.map(([l])=>l),next:c.map(([l])=>l)})),J({prev:[],next:[]}),Pt(2,1),f(([s,c])=>s.prev.length{let i=new I,a=i.pipe(he(),ye(!0));if(i.subscribe(({prev:s,next:c})=>{for(let[l]of c)l.classList.remove("md-nav__link--passed"),l.classList.remove("md-nav__link--active");for(let[l,[u]]of s.entries())u.classList.add("md-nav__link--passed"),u.classList.toggle("md-nav__link--active",l===s.length-1)}),X("toc.follow")){let s=R(t.pipe(Be(1),f(()=>{})),t.pipe(Be(250),f(()=>"smooth")));i.pipe(L(({prev:c})=>c.length>0),Ze(n.pipe(Ie(ge))),pe(s)).subscribe(([[{prev:c}],l])=>{let[u]=c[c.length-1];if(u.offsetHeight){let p=ki(u);if(typeof p!="undefined"){let d=u.offsetTop-p.offsetTop,{height:m}=Ae(p);p.scrollTo({top:d-m/2,behavior:l})}}})}return X("navigation.tracking")&&t.pipe(Q(a),fe("offset"),Be(250),ke(1),Q(o.pipe(ke(1))),jt({delay:250}),pe(i)).subscribe(([,{prev:s}])=>{let c=Ye(),l=s[s.length-1];if(l&&l.length){let[u]=l,{hash:p}=new URL(u.href);c.hash!==p&&(c.hash=p,history.replaceState({},"",`${c}`))}else c.hash="",history.replaceState({},"",`${c}`)}),Tp(e,{viewport$:t,header$:r}).pipe($(s=>i.next(s)),V(()=>i.complete()),f(s=>H({ref:e},s)))})}function Sp(e,{viewport$:t,main$:r,target$:n}){let o=t.pipe(f(({offset:{y:a}})=>a),Pt(2,1),f(([a,s])=>a>s&&s>0),ie()),i=r.pipe(f(({active:a})=>a));return re([i,o]).pipe(f(([a,s])=>!(a&&s)),ie(),Q(n.pipe(ke(1))),ye(!0),jt({delay:250}),f(a=>({hidden:a})))}function tc(e,{viewport$:t,header$:r,main$:n,target$:o}){let i=new I,a=i.pipe(he(),ye(!0));return i.subscribe({next({hidden:s}){e.hidden=s,s?(e.setAttribute("tabindex","-1"),e.blur()):e.removeAttribute("tabindex")},complete(){e.style.top="",e.hidden=!0,e.removeAttribute("tabindex")}}),r.pipe(Q(a),fe("height")).subscribe(({height:s})=>{e.style.top=`${s+16}px`}),b(e,"click").subscribe(s=>{s.preventDefault(),window.scrollTo({top:0})}),Sp(e,{viewport$:t,main$:n,target$:o}).pipe($(s=>i.next(s)),V(()=>i.complete()),f(s=>H({ref:e},s)))}function rc(e,t){return e.protocol=t.protocol,e.hostname=t.hostname,t.port&&(e.port=t.port),e}function Op(e,t){let r=new Map;for(let n of P("url",e)){let o=G("loc",n),i=[rc(new URL(o.textContent),t)];r.set(`${i[0]}`,i);for(let a of P("[rel=alternate]",n)){let s=a.getAttribute("href");s!=null&&i.push(rc(new URL(s),t))}}return r}function dr(e){return ns(new URL("sitemap.xml",e)).pipe(f(t=>Op(t,new URL(e))),_e(()=>Y(new Map)),xe())}function __ha_langroot(e){let t=new URL(e),r=t.pathname.match(/^\/(zh-hant|en|ja|ru)(?:\/|$)/);return t.pathname=r?`/${r[1]}/`:"/",t.search="",t.hash="",t}function nc({document$:e}){let t=new Map;e.pipe(g(()=>P("link[rel=alternate]")),f(r=>__ha_langroot(r.href)),L(r=>!t.has(r.toString())),oe(r=>dr(r).pipe(f(n=>[r,n]),_e(()=>y)))).subscribe(([r,n])=>{t.set(r.toString().replace(/\/$/,""),n)}),b(document.body,"click").pipe(L(r=>!r.metaKey&&!r.ctrlKey),g(r=>{if(r.target instanceof Element){let n=r.target.closest("a");if(n&&!n.target){let o=[...t].find(([p])=>n.href.startsWith(`${p}/`));if(typeof o=="undefined")return y;let[i,a]=o,s=Ye();if(s.href.startsWith(i))return y;let c=Ue(),l=s.href.replace(c.base,"");l=`${i}/${l}`;let u=a.has(l.split("#")[0])?new URL(l,c.base):new URL(i);return r.preventDefault(),Y(u)}}return y})).subscribe(r=>dt(r,!0))}var Co=_r(Mo());function Lp(e){e.setAttribute("data-md-copying","");let t=e.closest("[data-copy]"),r=t?t.getAttribute("data-copy"):e.innerText;return e.removeAttribute("data-md-copying"),r.trimEnd()}function oc({alert$:e}){Co.default.isSupported()&&new U(t=>{new Co.default("[data-clipboard-target], [data-clipboard-text]",{text:r=>r.getAttribute("data-clipboard-text")||Lp(G(r.getAttribute("data-clipboard-target")))}).on("success",r=>t.next(r))}).pipe($(t=>{t.trigger.focus()}),f(()=>Bt("clipboard.copied"))).subscribe(e)}function ic(e,t){if(!(e.target instanceof Element))return y;let r=e.target.closest("a");if(r===null)return y;if(r.target||e.metaKey||e.ctrlKey)return y;let n=new URL(r.href);return n.search=n.hash="",t.has(`${n}`)?(e.preventDefault(),Y(r)):y}function ac(e){let t=new Map;for(let r of P(":scope > *",e.head))t.set(r.outerHTML,r);return t}function sc(e){for(let t of P("[href], [src]",e))for(let r of["href","src"]){let n=t.getAttribute(r);if(n&&!/^(?:[a-z]+:)?\/\//i.test(n)){t[r]=t[r];break}}return Y(e)}function Mp(e){for(let n of["[data-md-component=announce]","[data-md-component=container]","[data-md-component=header-topic]","[data-md-component=outdated]","[data-md-component=logo]","[data-md-component=skip]",...X("navigation.tabs.sticky")?["[data-md-component=tabs]"]:[]]){let o=Le(n),i=Le(n,e);typeof o!="undefined"&&typeof i!="undefined"&&o.replaceWith(i)}let t=ac(document);for(let[n,o]of ac(e))t.has(n)?t.delete(n):document.head.appendChild(o);for(let n of t.values()){let o=n.getAttribute("name");o!=="theme-color"&&o!=="color-scheme"&&n.remove()}let r=ht("container");return nt(P("script",r)).pipe(g(n=>{let o=e.createElement("script");if(n.src){for(let i of n.getAttributeNames())o.setAttribute(i,n.getAttribute(i));return n.replaceWith(o),new U(i=>{o.onload=()=>i.complete()})}else return o.textContent=n.textContent,n.replaceWith(o),y}),he(),ye(document))}function cc({sitemap$:e,location$:t,viewport$:r,progress$:n}){if(location.protocol==="file:")return Ke;Y(document).subscribe(sc);let o=b(document.body,"click").pipe(Ze(e),g(([s,c])=>ic(s,c)),f(({href:s})=>new URL(s)),xe()),i=b(window,"popstate").pipe(f(Ye),xe());o.pipe(pe(r)).subscribe(([s,{offset:c}])=>{history.replaceState(c,""),history.pushState(null,"",s)}),R(o,i).subscribe(t);let a=t.pipe(fe("pathname"),g(s=>En(s,{progress$:n}).pipe(_e(()=>(dt(s,!0),y)))),g(sc),g(Mp),xe());return R(a.pipe(pe(t,(s,c)=>c)),a.pipe(g(()=>t),fe("hash")),t.pipe(ie((s,c)=>s.pathname===c.pathname&&s.hash===c.hash),g(()=>o),$(()=>history.back()))).subscribe(s=>{var c,l;history.state!==null||!s.hash?window.scrollTo(0,(l=(c=history.state)==null?void 0:c.y)!=null?l:0):(history.scrollRestoration="auto",es(s.hash),history.scrollRestoration="manual")}),t.subscribe(()=>{history.scrollRestoration="manual"}),b(window,"beforeunload").subscribe(()=>{history.scrollRestoration="auto"}),r.pipe(fe("offset"),Be(100)).subscribe(({offset:s})=>{history.replaceState(s,"")}),X("navigation.instant.prefetch")&&R(b(document.body,"mousemove"),b(document.body,"focusin")).pipe(Ze(e),g(([s,c])=>ic(s,c)),Be(25),Yn(({href:s})=>s),cn(s=>{let c=document.createElement("link");return c.rel="prefetch",c.href=s.toString(),document.head.appendChild(c),b(c,"load").pipe(f(()=>c),Me(1))})).subscribe(s=>s.remove()),a}function lc(e){var u;let{selectedVersionSitemap:t,selectedVersionBaseURL:r,currentLocation:n,currentBaseURL:o}=e,i=(u=Ho(o))==null?void 0:u.pathname;if(i===void 0)return;let a=kp(n.pathname,i);if(a===void 0)return;let s=Cp(t.keys());if(!t.has(s))return;let c=Ho(a,s);if(!c||!t.has(c.href))return;let l=Ho(a,r);if(l)return l.hash=n.hash,l.search=n.search,l}function Ho(e,t){try{return new URL(e,t)}catch(r){return}}function kp(e,t){if(e.startsWith(t))return e.slice(t.length)}function Ap(e,t){let r=Math.min(e.length,t.length),n;for(n=0;ny)),n=r.pipe(f(o=>{let[,i]=t.base.match(/([^/]+)\/?$/);return o.find(({version:a,aliases:s})=>a===i||s.includes(i))||o[0]}));r.pipe(f(o=>new Map(o.map(i=>[`${new URL(`../${i.version}/`,t.base)}`,i]))),g(o=>b(document.body,"click").pipe(L(i=>!i.metaKey&&!i.ctrlKey),pe(n),g(([i,a])=>{if(i.target instanceof Element){let s=i.target.closest("a");if(s&&!s.target&&o.has(s.href)){let c=s.href;return!i.target.closest(".md-version")&&o.get(c)===a?y:(i.preventDefault(),Y(new URL(c)))}}return y}),g(i=>dr(i).pipe(f(a=>{var s;return(s=lc({selectedVersionSitemap:a,selectedVersionBaseURL:i,currentLocation:Ye(),currentBaseURL:t.base}))!=null?s:i})))))).subscribe(o=>dt(o,!0)),re([r,n]).subscribe(([o,i])=>{G(".md-header__topic").appendChild(_s(o,i))}),e.pipe(g(()=>n)).subscribe(o=>{var s;let i=new URL(t.base),a=__md_get("__outdated",sessionStorage,i);if(a===null){a=!0;let c=((s=t.version)==null?void 0:s.default)||"latest";Array.isArray(c)||(c=[c]);e:for(let l of c)for(let u of o.aliases.concat(o.version))if(new RegExp(l,"i").test(u)){a=!1;break e}__md_set("__outdated",a,sessionStorage,i)}if(a)for(let c of Ee("outdated"))c.hidden=!1})}function pc({document$:e,viewport$:t}){e.pipe(g(()=>P(".md-ellipsis")),oe(r=>Et(r).pipe(Q(e.pipe(ke(1))),L(n=>n),f(()=>r),Me(1))),L(r=>r.offsetWidth{let n=r.innerText,o=r.closest("a")||r;return o.title=n,X("content.tooltips")?Ge(o,{viewport$:t}).pipe(Q(e.pipe(ke(1))),V(()=>o.removeAttribute("title"))):y})).subscribe(),X("content.tooltips")&&e.pipe(g(()=>P(".md-status")),oe(r=>Ge(r,{viewport$:t}))).subscribe()}function fc({document$:e,tablet$:t}){e.pipe(g(()=>P(".md-toggle--indeterminate")),$(r=>{r.indeterminate=!0,r.checked=!1}),oe(r=>b(r,"change").pipe(Xn(()=>r.classList.contains("md-toggle--indeterminate")),f(()=>r))),pe(t)).subscribe(([r,n])=>{r.classList.remove("md-toggle--indeterminate"),n&&(r.checked=!1)})}function Hp(){return/(iPad|iPhone|iPod)/.test(navigator.userAgent)}function mc({document$:e}){e.pipe(g(()=>P("[data-md-scrollfix]")),$(t=>t.removeAttribute("data-md-scrollfix")),L(Hp),oe(t=>b(t,"touchstart").pipe(f(()=>t)))).subscribe(t=>{let r=t.scrollTop;r===0?t.scrollTop=1:r+t.offsetHeight===t.scrollHeight&&(t.scrollTop=r-1)})}Object.entries||(Object.entries=function(e){let t=[];for(let r of Object.keys(e))t.push([r,e[r]]);return t});Object.values||(Object.values=function(e){let t=[];for(let r of Object.keys(e))t.push(e[r]);return t});typeof Element!="undefined"&&(Element.prototype.scrollTo||(Element.prototype.scrollTo=function(e,t){typeof e=="object"?(this.scrollLeft=e.left,this.scrollTop=e.top):(this.scrollLeft=e,this.scrollTop=t)}),Element.prototype.replaceWith||(Element.prototype.replaceWith=function(...e){let t=this.parentNode;if(t){e.length===0&&t.removeChild(this);for(let r=e.length-1;r>=0;r--){let n=e[r];typeof n=="string"?n=document.createTextNode(n):n.parentNode&&n.parentNode.removeChild(n),r?t.insertBefore(this.previousSibling,n):t.replaceChild(n,this)}}}));function $p(){return location.protocol==="file:"?ar(`${new URL("search.js",Mn.base)}`).pipe(f(()=>__index),_e(()=>Ke),se(1)):et(new URL("search.json",Mn.base))}document.documentElement.classList.remove("no-js");document.documentElement.classList.add("js");var vt=Si(),Ur=Za(),hr=ts(Ur),hc=Xa(),ze=cs(),$o=Ir("(min-width: 60em)"),vc=Ir("(min-width: 76.25em)"),bc=rs(),Mn=Ue(),gc=Le(".md-search")?$p():Ke,Po=new I;oc({alert$:Po});nc({document$:vt});var Io=new I,_c=dr(Mn.base);X("navigation.instant")&&cc({sitemap$:_c,location$:Ur,viewport$:ze,progress$:Io}).subscribe(vt);var dc;((dc=Mn.version)==null?void 0:dc.provider)==="mike"&&uc({document$:vt});R(Ur,hr).pipe(It(125)).subscribe(()=>{Eo("drawer",!1),Eo("search",!1)});hc.pipe(L(({mode:e,meta:t})=>e==="global"&&!t)).subscribe(e=>{switch(e.type){case",":case"p":let t=document.querySelector("link[rel=prev]");t instanceof HTMLLinkElement&&dt(t);break;case".":case"n":let r=document.querySelector("link[rel=next]");r instanceof HTMLLinkElement&&dt(r);break;case"/":let n=document.querySelector("[data-md-component=search] button");n instanceof HTMLButtonElement&&n.click();break;case"Enter":let o=xt();o instanceof HTMLLabelElement&&o.click()}});pc({viewport$:ze,document$:vt});fc({document$:vt,tablet$:$o});mc({document$:vt});var Lt=Us(ht("header"),{viewport$:ze}),Fr=vt.pipe(f(()=>ht("main")),g(e=>Ws(e,{viewport$:ze,header$:Lt})),se(1)),Pp=R(...Ee("consent").map(e=>us(e,{target$:hr})),...Ee("dialog").map(e=>Fs(e,{alert$:Po})),...Ee("palette").map(e=>Vs(e)),...Ee("progress").map(e=>zs(e,{progress$:Io})),...Ee("search").map(e=>Ks(e,{index$:gc})),...Ee("source").map(e=>Zs(e))),Ip=j(()=>R(...Ee("announce").map(e=>ls(e)),...Ee("content").map(e=>js(e,{sitemap$:_c,viewport$:ze,target$:hr,print$:bc})),...Ee("content").map(e=>X("search.highlight")?Ys(e,{index$:gc,location$:Ur}):y),...Ee("header").map(e=>Ns(e,{viewport$:ze,header$:Lt,main$:Fr})),...Ee("header-title").map(e=>Ds(e,{viewport$:ze,header$:Lt})),...Ee("sidebar").map(e=>e.getAttribute("data-md-type")==="navigation"?yo(vc,()=>Ao(e,{viewport$:ze,header$:Lt,main$:Fr})):yo($o,()=>Ao(e,{viewport$:ze,header$:Lt,main$:Fr}))),...Ee("tabs").map(e=>Qs(e,{viewport$:ze,header$:Lt})),...Ee("toc").map(e=>ec(e,{viewport$:ze,header$:Lt,main$:Fr,target$:hr})),...Ee("top").map(e=>tc(e,{viewport$:ze,header$:Lt,main$:Fr,target$:hr})))),yc=vt.pipe(g(()=>Ip),Rt(Pp),se(1));yc.subscribe();window.document$=vt;window.location$=Ur;window.target$=hr;window.keyboard$=hc;window.viewport$=ze;window.tablet$=$o;window.screen$=vc;window.print$=bc;window.alert$=Po;window.progress$=Io;window.component$=yc;})(); -/*! update cache: 20260410225915 */ +/*! update cache: 20260414173603 */ diff --git a/zh-hant/javascripts/animation_player.js b/zh-hant/javascripts/animation_player.js index e5d6c16d7..c8f4a7b58 100644 --- a/zh-hant/javascripts/animation_player.js +++ b/zh-hant/javascripts/animation_player.js @@ -251,4 +251,4 @@ initAutoSlide(); } })(); -/*! update cache: 20260410225915 */ +/*! update cache: 20260414173603 */ diff --git a/zh-hant/javascripts/katex.js b/zh-hant/javascripts/katex.js index 7d68e9869..82086961d 100644 --- a/zh-hant/javascripts/katex.js +++ b/zh-hant/javascripts/katex.js @@ -8,4 +8,4 @@ document$.subscribe(({ body }) => { ], }); }); -/*! update cache: 20260410225915 */ +/*! update cache: 20260414173603 */ diff --git a/zh-hant/javascripts/mathjax.js b/zh-hant/javascripts/mathjax.js index f131901a0..17898246a 100644 --- a/zh-hant/javascripts/mathjax.js +++ b/zh-hant/javascripts/mathjax.js @@ -15,4 +15,4 @@ window.MathJax = { document$.subscribe(() => { MathJax.typesetPromise(); }); -/*! update cache: 20260410225915 */ +/*! update cache: 20260414173603 */ diff --git a/zh-hant/javascripts/starfield.js b/zh-hant/javascripts/starfield.js index b2b3963cb..68b311241 100644 --- a/zh-hant/javascripts/starfield.js +++ b/zh-hant/javascripts/starfield.js @@ -469,4 +469,4 @@ return Starfield; }); -/*! update cache: 20260410225915 */ +/*! update cache: 20260414173603 */ diff --git a/zh-hant/stylesheets/animation_player.css b/zh-hant/stylesheets/animation_player.css index 164bae6a5..443937478 100644 --- a/zh-hant/stylesheets/animation_player.css +++ b/zh-hant/stylesheets/animation_player.css @@ -176,4 +176,4 @@ font-size: 0.7rem; } } -/*! update cache: 20260410225915 */ +/*! update cache: 20260414173603 */ diff --git a/zh-hant/stylesheets/extra.css b/zh-hant/stylesheets/extra.css index cbd18d25a..3ca1893b6 100644 --- a/zh-hant/stylesheets/extra.css +++ b/zh-hant/stylesheets/extra.css @@ -806,4 +806,4 @@ a:hover .device-on-hover { margin: 0 0 1em; } } -/*! update cache: 20260410225915 */ +/*! update cache: 20260414173603 */ diff --git a/zh-hant/stylesheets/giscus-dark.css b/zh-hant/stylesheets/giscus-dark.css index ace2162db..2dc1c4b29 100644 --- a/zh-hant/stylesheets/giscus-dark.css +++ b/zh-hant/stylesheets/giscus-dark.css @@ -122,4 +122,4 @@ main .gsc-loading-image { .gsc-reply-content::-webkit-scrollbar-track { background: transparent; } -/*! update cache: 20260410225915 */ +/*! update cache: 20260414173603 */ diff --git a/zh-hant/stylesheets/giscus-light.css b/zh-hant/stylesheets/giscus-light.css index c97755cf0..c55398c7f 100644 --- a/zh-hant/stylesheets/giscus-light.css +++ b/zh-hant/stylesheets/giscus-light.css @@ -153,4 +153,4 @@ main { .gsc-reply-content::-webkit-scrollbar-track { background: transparent; } -/*! update cache: 20260410225915 */ +/*! update cache: 20260414173603 */